Commit 21d1ad5c by Jonathan Thomas

Refactored preview data to live in Vuex. Made clip seconds label react different…

Refactored preview data to live in Vuex. Made clip seconds label react different when it's too small. Added drop-shadow style to context menu buttons. Added editClip action, but not connected yet.
parent f9452bb5
<template>
<div class="row mb-3 p-2 scrolling-container">
<h3>Clips</h3>
<div class="row mb-3 gx-2 p-2 scrolling-container">
<h3>Clips <button v-if="clips" type="button" class="btn btn-danger export-btn">Export</button></h3>
<div class="col-12">
<div class="row gy-2 gx-2 mb-2" v-for="clip in thumbnailedClips" :key="clip.id">
<div class="row gy-2 gx-2 mb-2" v-for="(clip, index) in thumbnailedClips" :key="clip.id">
<div class="col-12 text-center img-parent">
<span class="clip-badge badge">{{ index + 1 }}</span>
<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"/>
......@@ -18,7 +20,7 @@
</ul>
</div>
<div class="clip-label">
<div ref="clip" class="clip-label">
{{ (clip.end - clip.start).toFixed(1) }} Seconds
</div>
......@@ -45,14 +47,14 @@ export default {
this.deleteClip(clip_id)
},
toggleSelection(clipObj) {
if (clipObj.fileObj != this.previewFile) {
if (clipObj.fileObj != this.preview.file) {
this.setPreviewFile(clipObj.fileObj)
} else {
this.setPreviewFile(null)
}
},
getSelectedClass(clipObj) {
if (this.previewFile && clipObj.fileObj.id == this.previewFile.id) {
if (this.preview.file && clipObj.fileObj.id == this.preview.file.id) {
return 'selected'
} else {
return ''
......@@ -65,10 +67,10 @@ export default {
thumbnailedClips() {
return this.clips.filter(clip => clip.thumbnail)
},
...mapState(['clips', 'previewFile', 'latestClip'])
...mapState(['clips', 'preview', 'scrollToClip'])
},
watch: {
latestClip() {
scrollToClip() {
this.$nextTick(() => {
this.$refs.bottom.scrollIntoView({behavior: 'smooth', block: 'end'})
})
......@@ -116,9 +118,28 @@ export default {
.clip-label {
color: #ffffff;
position: absolute;
top: 88%;
bottom: 5%;
z-index: 999;
right: 5%;
font-size: .8em;
background-color: #000000;
border-radius: 4px;
padding: 3px;
opacity: 75%;
}
.export-btn {
float: right;
}
.dropdown-toggle {
filter: drop-shadow(0px 0px 2px #000000);
}
.clip-badge {
position: absolute;
bottom: 5%;
left: 5%;
padding: 8px;
opacity: 75%;
background-color: #000000;
border-radius: 4px;
}
</style>
\ No newline at end of file
<template>
<div class="row mb-3 gy-2 gx-2 p-2 scrolling-container">
<div class="row mb-3 gx-2 p-2 scrolling-container">
<h3>Files <button type="button" class="btn btn-primary upload-btn" @click="chooseFiles">Upload</button></h3>
<div class="col-sm-12">
<div class="form-floating mb-3">
......@@ -75,14 +75,14 @@ export default {
await this.deleteFile(file.id)
},
toggleSelection(fileObject) {
if (fileObject != this.previewFile) {
if (fileObject != this.preview.file) {
this.setPreviewFile(fileObject)
} else {
this.setPreviewFile(null)
}
},
getSelectedClass(fileObject) {
if (this.previewFile && fileObject.id == this.previewFile.id) {
if (this.preview.file && fileObject.id == this.preview.file.id) {
return 'selected'
} else {
return ''
......@@ -103,7 +103,7 @@ export default {
searchedFiles() {
return this.files.filter(file => path.basename(file.media).toLowerCase().includes(this.search.toLowerCase()) && file.thumbnail)
},
...mapState(['files', 'previewFile', 'uploads'])
...mapState(['files', 'preview', 'uploads'])
},
mounted() {
this.setFiles([])
......@@ -145,4 +145,7 @@ export default {
min-width: 5em;
opacity: 80%;
}
.dropdown-toggle {
filter: drop-shadow(0px 0px 2px #000000);
}
</style>
\ No newline at end of file
<template>
<div class="row">
<div class="col-xs-12">
<h3 class="p-2">Preview <button v-if="hasPreviewFile" type="button" class="btn btn-danger export-btn">Export</button></h3>
<h3 class="p-2">Preview</h3>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<video ref="video" v-if="!isImage && hasPreviewFile" @timeupdate="videoTimeUpdate" @pause="togglePause" @play="togglePause" preload="" poster="" class="video-responsive">
<source :type="previewFormat" :src="previewFile.media">
<source :type="previewFormat" :src="preview.file.media">
</video>
<img v-if="isImage && hasPreviewFile" :src="previewFile.media" class="img-fluid img-thumbnail" />
<img v-if="isImage && hasPreviewFile" :src="preview.file.media" class="img-fluid img-thumbnail" />
</div>
</div>
......@@ -27,19 +27,19 @@
</div>
<div class="col-sm-10 d-flex flex-column p-1">
<div class="timeline" ref="timeline" @mousedown="seekToCursor($event)" @mousemove="drag($event)" @touchmove="drag($event)" @mouseup="endDrag" @touchend="endDrag" @mouseleave="endDrag">
<div title="Drag Playback Position" ref="playhead" class="playhead" @mousedown="startDrag($event, 'playhead')" @touchstart="startDrag($event, 'playhead')" :style="{left: position * 100.0 + '%'}"></div>
<div title="Drag to Trim Start" ref="start" class="marker marker_left" @click="seekToMarker('start')" @mousedown="startDrag($event, 'start')" @touchstart="startDrag($event, 'start')" :style="{left: markers.start * 100.0 + '%'}">
<div title="Drag Playback Position" ref="playhead" class="playhead" @mousedown="startDrag($event, 'playhead')" @touchstart="startDrag($event, 'playhead')" :style="{left: preview.position * 100.0 + '%'}"></div>
<div title="Drag to Trim Start" ref="start" class="marker marker_left" @click="seekToMarker('start')" @mousedown="startDrag($event, 'start')" @touchstart="startDrag($event, 'start')" :style="{left: preview.start * 100.0 + '%'}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="48" fill="currentColor" class="bi bi-grip-vertical" viewBox="0 0 16 16">
<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>
</div>
<div title="Drag to Trim End" ref="end" class="marker marker_right" @click="seekToMarker('end')" @mousedown="startDrag($event, 'end')" @touchstart="startDrag($event, 'end')" :style="{left: (markers.end - marker_width_percent) * 100.0 + '%'}">
<div title="Drag to Trim End" ref="end" class="marker marker_right" @click="seekToMarker('end')" @mousedown="startDrag($event, 'end')" @touchstart="startDrag($event, 'end')" :style="{left: (preview.end - marker_width_percent) * 100.0 + '%'}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="48" fill="currentColor" class="bi bi-grip-vertical" viewBox="3 0 16 16">
<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>
</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 ref="clip" class="clip" :class="clipSizeClass" :style="{left: preview.start * 100.0 + '%', width: `calc(${(preview.end - preview.start) * 100.0}%)`}">
{{ ((preview.end - preview.start) * preview.file.json.duration).toFixed(1) }} Seconds
</div>
</div>
</div>
......@@ -52,7 +52,7 @@
</div>
</div>
<div v-if="!previewFile" class="mb-5 text-center" style="margin-top: 7em;">
<div v-if="!hasPreviewFile" class="mb-5 text-center" style="margin-top: 7em;">
<div class="col-lg-6 mx-auto">
<p class="lead">Upload or select a video to begin!</p>
</div>
......@@ -61,7 +61,7 @@
</template>
<script>
import {mapState, mapActions} from "vuex";
import {mapState, mapActions, mapMutations} from "vuex";
export default {
name: "Preview.vue",
......@@ -70,22 +70,18 @@ export default {
return {
dragging_marker: null,
is_paused: true,
position: 0.0,
markers: {
start: 0.0,
end: 1.0
},
marker_width: 12,
marker_width_percent: 0.0
marker_width_percent: 0.0,
clipSizeClass: ""
}
},
methods: {
async createClipBtn() {
let data = {
"file": this.previewFile.url,
"file": this.preview.file.url,
"position": 0.0,
"start": this.markers.start * this.previewFile.json.duration,
"end": this.markers.end * this.previewFile.json.duration,
"start": this.preview.start * this.preview.file.json.duration,
"end": this.preview.end * this.preview.file.json.duration,
"layer": 1,
"project": this.project.url,
"json": {}
......@@ -114,7 +110,7 @@ export default {
videoTimeUpdate() {
if (this.$refs.video) {
let playback_percent = (this.$refs.video.currentTime / this.$refs.video.duration)
this.position = playback_percent
this.setPreviewPosition(playback_percent)
}
},
startDrag(e, marker) {
......@@ -130,9 +126,8 @@ export default {
seekToCursor(e) {
if (!this.dragging_marker) {
let percent_x = this.getCursorPosition(e)
this.position = percent_x
if (this.$refs.video) {
this.$refs.video.currentTime = this.position * this.$refs.video.duration
this.$refs.video.currentTime = percent_x * this.$refs.video.duration
}
return false
}
......@@ -140,27 +135,35 @@ export default {
seekToMarker(marker) {
if (this.$refs.video) {
if (marker == 'start') {
this.$refs.video.currentTime = this.markers.start * this.$refs.video.duration
this.$refs.video.currentTime = this.preview.start * this.$refs.video.duration
} else if (marker == 'end') {
this.$refs.video.currentTime = this.markers.end * this.$refs.video.duration
this.$refs.video.currentTime = this.preview.end * this.$refs.video.duration
}
}
},
drag(e) {
if (this.$refs.clip.clientWidth < 80) {
this.clipSizeClass = "clip-too-small"
} else {
this.clipSizeClass = ""
}
if (this.dragging_marker == 'start') {
this.markers.start = Math.min(this.getCursorPosition(e), this.markers.end)
if (this.$refs.video && isFinite(this.markers.start * this.$refs.video.duration)) {
this.$refs.video.currentTime = this.markers.start * this.$refs.video.duration
this.setPreview({start: Math.min(this.getCursorPosition(e), this.preview.end),
end: this.preview.end})
if (this.$refs.video && isFinite(this.preview.start * this.$refs.video.duration)) {
this.$refs.video.currentTime = this.preview.start * this.$refs.video.duration
}
} else if (this.dragging_marker == 'end') {
this.markers.end = Math.max(this.getCursorPosition(e, 12), this.markers.start)
if (this.$refs.video && isFinite(this.markers.start * this.$refs.video.duration)) {
this.$refs.video.currentTime = this.markers.end * this.$refs.video.duration
this.setPreview({start: this.preview.start,
end: Math.max(this.getCursorPosition(e, 12), this.preview.start)})
if (this.$refs.video && isFinite(this.preview.start * this.$refs.video.duration)) {
this.$refs.video.currentTime = this.preview.end * this.$refs.video.duration
}
} else if (this.dragging_marker == 'playhead') {
let percent_x = this.getCursorPosition(e)
this.$refs.playhead.style.left = `${percent_x * 100}%`
if (this.$refs.video && isFinite(this.markers.start * this.$refs.video.duration)) {
if (this.$refs.video && isFinite(this.preview.start * this.$refs.video.duration)) {
this.$refs.video.currentTime = percent_x * this.$refs.video.duration
}
}
......@@ -196,38 +199,38 @@ export default {
let percent_x = (relative_x / timeline_bounds.width)
return percent_x
},
...mapActions(['createClip'])
...mapActions(['createClip']),
...mapMutations(['setPreview', 'setPreviewPosition'])
},
computed: {
...mapState(['previewFile']),
...mapState(['preview']),
hasPreviewFile() {
if (this.previewFile) {
if (this.preview.file) {
return true
} else {
return false
}
},
isImage() {
return (this.previewFile && this.previewFile.json.vcodec == "QImage")
return (this.preview.file && this.preview.file.json.vcodec == "QImage")
},
previewFormat() {
if (this.previewFile && this.previewFile.json.acodec == "mp3") {
if (this.preview.file && this.preview.file.json.acodec == "mp3") {
return 'audio/mpeg'
} else {
return `video/${this.previewFile.media.split('.').pop()}`
return `video/${this.preview.file.media.split('.').pop()}`
}
}
},
watch: {
previewFile() {
'preview.file': function() {
if (this.$refs.video) {
this.is_paused = true
this.$refs.video.pause()
this.$refs.video.load()
}
this.position = 0.0
this.markers.start = 0.0
this.markers.end = 1.0
this.setPreview({start: 0.0, end: 1.0})
this.setPreviewPosition(0.0)
},
},
updated() {
......@@ -241,9 +244,6 @@ video {
background-color: #000000;
width: 100%;
}
.export-btn {
float: right;
}
.timeline {
background-color: #000000;
height: 48px;
......@@ -271,6 +271,10 @@ video {
text-align: center;
padding-top: 15px;
}
.clip-too-small {
color: #0d6efd!important;
padding-top: 47px!important;
}
.marker_left {
border-left: rgba(255, 255, 255, 0.5) solid 1px;
}
......
......@@ -12,8 +12,14 @@ export default createStore({
uploads: [],
project: null,
user: null,
previewFile: null,
latestClip: null,
preview: {
file: null,
clip: null,
start: 0.0,
end: 1.0,
position: 0.0
},
scrollToClip: null,
},
mutations: {
......@@ -69,9 +75,15 @@ export default createStore({
state.clips = clips
if (!clips) {
// clear latest clip if no clips
state.latestClip = null
state.scrollToClip = null
}
},
setClip(state, clipObj) {
state.clips = [
...state.clips = state.clips.filter(clip => clip.id != clipObj.id),
clipObj
]
},
deleteClip(state, clip_id) {
state.clips = state.clips.filter(clip => clip.id != clip_id)
},
......@@ -82,12 +94,27 @@ export default createStore({
payload.obj
]
if (payload.latest) {
state.latestClip = payload.obj
state.scrollToClip = payload.obj
}
}
},
setPreview(state, payload) {
state.preview.start = payload.start
state.preview.end = payload.end
},
setPreviewPosition(state, position) {
state.preview.position = position
},
setPreviewFile(state, file) {
state.previewFile = file
state.preview.file = file
state.preview.start = 0.0
state.preview.end = 1.0
},
setPreviewClip(state, clipObj) {
state.preview.clip = clipObj
state.preview.start = clipObj.start
state.preview.end = clipObj.end
state.preview.position = clipObj.start
},
addUpload(state, upload) {
state.uploads.push(upload)
......@@ -203,6 +230,21 @@ export default createStore({
commit('addError', err.response.data)
}
},
async editClip({dispatch, commit}, payload) {
try {
const response = await instance.post(`${payload.project_url}clips/`, payload.data)
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) {
commit('addError', err.response.data)
}
},
async deleteClip({commit}, clip_id) {
try {
await instance.delete(`clips/${clip_id}/`)
......
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