Commit bf3292b6 by Jonathan Thomas

Separated thumbnail generation from load actions. Improved scroll-to-bottom clip…

Separated thumbnail generation from load actions. Improved scroll-to-bottom clip logic. Improved timeline/clip style (added seconds).
parent ba033dbe
...@@ -60,18 +60,14 @@ export default { ...@@ -60,18 +60,14 @@ export default {
...mapMutations((['setPreviewFile'])) ...mapMutations((['setPreviewFile']))
}, },
computed: { computed: {
...mapState(['clips', 'previewFile']) ...mapState(['clips', 'previewFile', 'latestClip'])
}, },
watch: { watch: {
clips(newVal, oldVal) { latestClip() {
let diff = newVal.length - oldVal.length
if (diff == 1) {
// 1 clip was added (scroll to bottom)
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.bottom.scrollIntoView({behavior: 'smooth', block: 'center'}) this.$refs.bottom.scrollIntoView({behavior: 'smooth', block: 'center'})
}) })
} }
}
}, },
async mounted() { async mounted() {
await this.loadClips(this.project.id) await this.loadClips(this.project.id)
......
...@@ -7,7 +7,17 @@ ...@@ -7,7 +7,17 @@
<label for="floatingInput">Search</label> <label for="floatingInput">Search</label>
</div> </div>
</div> </div>
<div v-for="file in searchedFiles" :key="file.id" class="col-4"> <div v-for="file in searchedFiles" :key="file.id" class="col-4" style="position: relative;">
<div class="btn-group dropstart context-menu">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="dropdown-toggle bi bi-three-dots-vertical" data-bs-toggle="dropdown" aria-expanded="false" viewBox="0 0 16 16">
<path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
</svg>
<ul class="dropdown-menu small-dropdown">
<li><a class="dropdown-item" href="#" @click="deleteFileBtn(file)">Delete</a></li>
</ul>
</div>
<img @click="toggleSelection(file)" :class="getSelectedClass(file)" class="file-thumbnail img-fluid img-thumbnail" :title="file.name" :src="file.thumbnail"/> <img @click="toggleSelection(file)" :class="getSelectedClass(file)" class="file-thumbnail img-fluid img-thumbnail" :title="file.name" :src="file.thumbnail"/>
<figcaption class="figure-caption">{{ getFileName(file) }}</figcaption> <figcaption class="figure-caption">{{ getFileName(file) }}</figcaption>
</div> </div>
...@@ -61,6 +71,9 @@ export default { ...@@ -61,6 +71,9 @@ export default {
// Clear file input // Clear file input
this.$refs.fileUpload.value = null this.$refs.fileUpload.value = null
}, },
async deleteFileBtn(file) {
await this.deleteFile(file.id)
},
toggleSelection(fileObject) { toggleSelection(fileObject) {
if (fileObject != this.previewFile) { if (fileObject != this.previewFile) {
this.setPreviewFile(fileObject) this.setPreviewFile(fileObject)
...@@ -83,7 +96,7 @@ export default { ...@@ -83,7 +96,7 @@ export default {
return `${base.substr(0, 18)}...` return `${base.substr(0, 18)}...`
} }
}, },
...mapActions(['loadFiles', 'createFile']), ...mapActions(['loadFiles', 'createFile', 'deleteFile']),
...mapMutations((['setPreviewFile'])) ...mapMutations((['setPreviewFile']))
}, },
computed: { computed: {
...@@ -118,4 +131,16 @@ export default { ...@@ -118,4 +131,16 @@ export default {
.file-thumbnail { .file-thumbnail {
cursor: pointer; cursor: pointer;
} }
.context-menu {
color: #ffffff;
position: absolute;
top: 0%;
right: 0%;
padding: 10px;
cursor: pointer;
}
.small-dropdown {
min-width: 5em;
opacity: 80%;
}
</style> </style>
\ No newline at end of file
...@@ -38,7 +38,9 @@ ...@@ -38,7 +38,9 @@
<path d="M7 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/> <path d="M7 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
</svg> </svg>
</div> </div>
<div class="clip" :style="{left: markers.start * 100.0 + '%', width: (markers.end - markers.start) * 100.0 + '%'}"></div> <div class="clip" :style="{left: markers.start * 100.0 + '%', width: `calc(${(markers.end - markers.start) * 100.0}%)`}">
{{ ((markers.end - markers.start) * previewFile.json.duration).toFixed(1) }} Sec
</div>
</div> </div>
</div> </div>
<div class="col-sm-1 d-flex flex-column p-1"> <div class="col-sm-1 d-flex flex-column p-1">
...@@ -145,18 +147,18 @@ export default { ...@@ -145,18 +147,18 @@ export default {
drag(e) { drag(e) {
if (this.dragging_marker == 'start') { if (this.dragging_marker == 'start') {
this.markers.start = Math.min(this.getCursorPosition(e), this.markers.end) this.markers.start = Math.min(this.getCursorPosition(e), this.markers.end)
if (this.$refs.video) { if (this.$refs.video && isFinite(this.markers.start * this.$refs.video.duration)) {
this.$refs.video.currentTime = this.markers.start * this.$refs.video.duration this.$refs.video.currentTime = this.markers.start * this.$refs.video.duration
} }
} else if (this.dragging_marker == 'end') { } else if (this.dragging_marker == 'end') {
this.markers.end = Math.max(this.getCursorPosition(e, 12), this.markers.start) this.markers.end = Math.max(this.getCursorPosition(e, 12), this.markers.start)
if (this.$refs.video) { if (this.$refs.video && isFinite(this.markers.start * this.$refs.video.duration)) {
this.$refs.video.currentTime = this.markers.end * this.$refs.video.duration this.$refs.video.currentTime = this.markers.end * this.$refs.video.duration
} }
} else if (this.dragging_marker == 'playhead') { } else if (this.dragging_marker == 'playhead') {
let percent_x = this.getCursorPosition(e) let percent_x = this.getCursorPosition(e)
this.$refs.playhead.style.left = `${percent_x * 100}%` this.$refs.playhead.style.left = `${percent_x * 100}%`
if (this.$refs.video) { if (this.$refs.video && isFinite(this.markers.start * this.$refs.video.duration)) {
this.$refs.video.currentTime = percent_x * this.$refs.video.duration this.$refs.video.currentTime = percent_x * this.$refs.video.duration
} }
} }
...@@ -261,8 +263,11 @@ video { ...@@ -261,8 +263,11 @@ video {
background-color: #0d6efd; background-color: #0d6efd;
height:48px; height:48px;
position: absolute; position: absolute;
opacity: 50%;
z-index: 0; z-index: 0;
color: #ffffff;
font-size: 0.7em;
text-align: center;
padding-top: 15px;
} }
.marker_left { .marker_left {
border-left: rgba(255, 255, 255, 0.5) solid 1px; border-left: rgba(255, 255, 255, 0.5) solid 1px;
......
...@@ -11,6 +11,7 @@ export default createStore({ ...@@ -11,6 +11,7 @@ export default createStore({
user: null, user: null,
errors: [], errors: [],
previewFile: null, previewFile: null,
latestClip: null,
uploads: [] uploads: []
}, },
...@@ -43,12 +44,37 @@ export default createStore({ ...@@ -43,12 +44,37 @@ export default createStore({
setFiles(state, files) { setFiles(state, files) {
state.files = files state.files = files
}, },
addFile(state, fileObj) {
state.files.push(fileObj)
},
updateFileThumbnail(state, payload) {
state.files = [
...state.files = state.files.filter(file => file.id != payload.obj.id),
payload.obj
]
},
deleteFile(state, file_id) {
state.files = state.files.filter(file => file.id != file_id)
},
addClip(state, clip) {
state.clips.push(clip)
},
setClips(state, clips) { setClips(state, clips) {
state.clips = clips state.clips = clips
}, },
deleteClip(state, clip_id) { deleteClip(state, clip_id) {
state.clips = state.clips.filter(clip => clip.id != clip_id) state.clips = state.clips.filter(clip => clip.id != clip_id)
}, },
updateClipThumbnail(state, payload) {
state.clips = [
...state.clips = state.clips.filter(clip => clip.id != payload.obj.id),
payload.obj
]
if (payload.latest)
{
state.latestClip = payload.obj
}
},
setPreviewFile(state, file) { setPreviewFile(state, file) {
state.previewFile = file state.previewFile = file
}, },
...@@ -113,80 +139,55 @@ export default createStore({ ...@@ -113,80 +139,55 @@ export default createStore({
commit('addError', err.response.data) commit('addError', err.response.data)
} }
}, },
async loadFiles({commit}, project_id) { async loadFiles({commit, dispatch}, project_id) {
try { try {
const response = await instance.get(`projects/${project_id}/files/`) const response = await instance.get(`projects/${project_id}/files/`)
let files = response.data.results let files = response.data.results
// Generate thumbnail for each file object // Generate thumbnail for each file object
for (let fileObj of files) { for (let fileObj of files) {
let data = { // fix long duration on images
"frame_number": 1, if (fileObj.json.duration == 3600) {
"width": 480, fileObj.json.duration = 60 * 3
"height": 270,
"image_format": "JPEG",
"image_quality": 100
}
try {
const response = await instance.post(`${fileObj.url}thumbnail/`, data, { responseType: 'arraybuffer' })
let blob = new Blob(
[response.data],
{ type: response.headers['content-type'] }
)
// Append thumbnail attribute to each file object
let thumbnailUrl = URL.createObjectURL(blob)
fileObj.thumbnail = thumbnailUrl
} catch(err) {
commit('addError', err.response.data)
} }
let payload = { obj: fileObj, frame: 1 }
dispatch('attachThumbnail', payload)
} }
commit('setFiles', files) commit('setFiles', files)
} catch(err) { } catch(err) {
commit('addError', err.response.data) commit('addError', err.response.data)
} }
}, },
async loadClips({commit}, project_id) { async loadClips({commit, dispatch}, project_id) {
try { try {
const response = await instance.get(`projects/${project_id}/clips/`) const response = await instance.get(`projects/${project_id}/clips/`)
let clips = response.data.results let clips = response.data.results
// Attach file obj to clip
for (let clipObj of clips) { for (let clipObj of clips) {
// Get related file object
const file_response = await instance.get(clipObj.file) const file_response = await instance.get(clipObj.file)
clipObj.fileObj = file_response.data clipObj.fileObj = file_response.data
commit('addClip', clipObj)
let fps = clipObj.json.reader.fps.num / clipObj.json.reader.fps.den let fps = clipObj.json.reader.fps.num / clipObj.json.reader.fps.den
let data = { let thumbnail_payload = { obj: clipObj, frame: clipObj.start * fps, latest: false }
"frame_number": clipObj.start * fps, dispatch('attachThumbnail', thumbnail_payload)
"width": 480,
"height": 270,
"image_format": "JPEG",
"image_quality": 100
}
try {
const response = await instance.post(`${clipObj.file}thumbnail/`, data, { responseType: 'arraybuffer' })
let blob = new Blob(
[response.data],
{ type: response.headers['content-type'] }
)
// Append thumbnail attribute to each clip object
let thumbnailUrl = URL.createObjectURL(blob)
clipObj.thumbnail = thumbnailUrl
} catch(err) {
commit('addError', err.response.data)
}
} }
commit('setClips', clips)
} catch(err) { } catch(err) {
commit('addError', err.response.data) commit('addError', err.response.data)
} }
}, },
async createClip({dispatch, commit}, payload) { async createClip({dispatch, commit}, payload) {
try { try {
await instance.post(`${payload.project_url}clips/`, payload.data) const response = await instance.post(`${payload.project_url}clips/`, payload.data)
dispatch('loadClips', payload.project_id) let clipObj = response.data
const file_response = await instance.get(clipObj.file)
clipObj.fileObj = file_response.data
commit('addClip', clipObj)
let fps = clipObj.json.reader.fps.num / clipObj.json.reader.fps.den
let thumbnail_payload = { obj: clipObj, frame: clipObj.start * fps, latest: true }
dispatch('attachThumbnail', thumbnail_payload)
} catch(err) { } catch(err) {
commit('addError', err.response.data) commit('addError', err.response.data)
} }
...@@ -199,7 +200,15 @@ export default createStore({ ...@@ -199,7 +200,15 @@ export default createStore({
commit('addError', err.response.data) commit('addError', err.response.data)
} }
}, },
async createFile({dispatch, commit}, payload) { async deleteFile({commit}, file_id) {
try {
await instance.delete(`files/${file_id}/`)
commit('deleteFile', file_id)
} catch(err) {
commit('addError', err.response.data)
}
},
async createFile({commit, dispatch}, payload) {
let uploadID = uuidv4() let uploadID = uuidv4()
let uploadObject = { 'id': uploadID, progress: 0 } let uploadObject = { 'id': uploadID, progress: 0 }
commit('addUpload', uploadObject) commit('addUpload', uploadObject)
...@@ -213,13 +222,59 @@ export default createStore({ ...@@ -213,13 +222,59 @@ export default createStore({
} }
try { try {
await instance.post(`${payload.project_url}files/`, payload.data, config) let response = await instance.post(`${payload.project_url}files/`, payload.data, config)
let fileObj = response.data
// fix long duration on images
if (fileObj.json.duration == 3600) {
fileObj.json.duration = 60 * 3
}
commit('removeUpload', uploadID) commit('removeUpload', uploadID)
dispatch('loadFiles', payload.project_id)
let thumbnail_payload = { obj: fileObj, frame: 1 }
dispatch('attachThumbnail', thumbnail_payload)
} catch(err) { } catch(err) {
commit('removeUpload', uploadID) commit('removeUpload', uploadID)
commit('addError', err.response.data) commit('addError', err.response.data)
} }
},
async attachThumbnail({commit}, payload) {
let fileObj = null
let objectType = null
if (payload.obj.file) {
// Clip passed in
fileObj = payload.obj.fileObj
objectType = 'clip'
} else {
// File passed in
fileObj = payload.obj
objectType = 'file'
}
let data = {
"frame_number": payload.frame,
"width": 480,
"height": 270,
"image_format": "JPEG",
"image_quality": 100
}
try {
const response = await instance.post(`${fileObj.url}thumbnail/`, data, { responseType: 'arraybuffer' })
let blob = new Blob(
[response.data],
{ type: response.headers['content-type'] }
)
// Append thumbnail attribute to each file object
let thumbnailUrl = URL.createObjectURL(blob)
payload.obj.thumbnail = thumbnailUrl
if (objectType == 'file') {
commit('updateFileThumbnail', payload)
} else {
commit('updateClipThumbnail', payload)
}
} catch(err) {
commit('addError', err.response.data)
}
} }
}, },
modules: { modules: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment