Commit e7847e4c by Jonathan Thomas

Added multiple file uploads and progress bars. Added custom video preview…

Added multiple file uploads and progress bars. Added custom video preview controls, with trimming support and seeking support. Still a WIP.
parent fcab0502
<template>
<div class="row mb-3 p-2 scrolling-container">
<h3>Clips</h3>
<h3>Clips <button type="button" class="btn btn-secondary clip-btn">Add Clip</button></h3>
<div class="col-12">
<div class="row gy-2 gx-2 mb-2" v-for="clip in clips" :key="clip.id">
......@@ -61,4 +61,7 @@ export default {
.clip-thumbnail {
cursor: pointer;
}
.clip-btn {
float: right;
}
</style>
\ No newline at end of file
......@@ -19,7 +19,7 @@
</div>
</div>
</div>
<input ref="fileUpload" type="file" @change="fileChanged" hidden>
<input ref="fileUpload" type="file" @change="fileChanged" multiple hidden>
</div>
</template>
......@@ -37,16 +37,25 @@ export default {
this.$refs.fileUpload.click()
},
async fileChanged(event) {
let results = []
let data = new FormData();
data.append('media', event.target.files[0]);
data.append('project', this.project.url);
data.append('json', "{}");
let payload = { project_id: this.project.id,
project_url: this.project.url,
data}
await this.createFile(payload)
for (let file of event.target.files) {
data.append('media', file);
data.append('project', this.project.url);
data.append('json', "{}");
let payload = {
project_id: this.project.id,
project_url: this.project.url,
data
}
// Invoke async upload (keep track of Promise)
results.push(this.createFile(payload))
}
for (let result in results) {
await result
}
// Clear file input
this.$refs.fileUpload.value = null
},
toggleSelection(fileObject) {
this.setPreviewFile(fileObject)
......@@ -66,6 +75,9 @@ export default {
},
mounted() {
this.loadFiles(this.project.id)
},
unmounted() {
this.setPreviewFile(null)
}
}
</script>
......
<template>
<h3 class="p-2">Preview <button type="button" class="btn btn-danger export-btn">Export</button></h3>
<video v-if="!isImage && hasPreviewFile" controls="" preload="" poster="" class="video-responsive">
<source :type="previewFormat" :src="previewFile.media">
</video>
<img v-if="isImage && hasPreviewFile" :src="previewFile.media" class="img-fluid img-thumbnail" />
<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>
</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">
</video>
<img v-if="isImage && hasPreviewFile" :src="previewFile.media" class="img-fluid img-thumbnail" />
</div>
</div>
<div v-if="hasPreviewFile" class="d-flex flex-column d-sm-flex flex-sm-row">
<div class="col-sm-1 d-flex flex-column p-1">
<button type="button" class="btn btn-secondary timeline-btn" @click="togglePlayback">
<svg v-show="is_paused" xmlns="http://www.w3.org/2000/svg" width="25" height="25" fill="currentColor" class="bi bi-play-fill" viewBox="0 0 16 16">
<path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"/>
</svg>
<svg v-show="!is_paused" xmlns="http://www.w3.org/2000/svg" width="25" height="25" fill="currentColor" class="bi bi-pause-fill" viewBox="0 0 16 16">
<path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"/>
</svg>
</button>
</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 ref="playhead" class="playhead" @mousedown="startDrag($event, 'playhead')" @touchstart="startDrag($event, 'playhead')" :style="{left: position * 100.0 + '%'}"></div>
<div ref="start" class="marker marker_left" @click="seekToMarker('start')" @mousedown="startDrag($event, 'start')" @touchstart="startDrag($event, 'start')" :style="{left: markers.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 ref="end" class="marker marker_right" @click="seekToMarker('end')" @mousedown="startDrag($event, 'end')" @touchstart="startDrag($event, 'end')" :style="{left: (markers.end - getMarkerWidth()) * 100.0 + '%'}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="48" fill="currentColor" class="bi bi-grip-vertical" viewBox="1 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>
</div>
<div class="col-sm-1 d-flex flex-column p-1">
<button type="button" class="btn btn-secondary timeline-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" fill="currentColor" class="bi bi-plus" viewBox="2 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>
</button>
</div>
</div>
<div v-if="!previewFile" 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>
</div>
</template>
<script>
......@@ -12,8 +63,103 @@ import {mapState} from "vuex";
export default {
name: "Preview.vue",
props: ['project'],
data() {
return {
dragging_marker: null,
is_paused: true,
position: 0.0,
markers: {
start: 0.0,
end: 1.0
},
marker_width: 12
}
},
methods: {
togglePause() {
this.is_paused = this.$refs.video.paused
},
togglePlayback() {
if (this.$refs.video.paused) {
this.$refs.video.play()
} else {
this.$refs.video.pause()
}
},
videoTimeUpdate() {
let playback_percent = (this.$refs.video.currentTime / this.$refs.video.duration)
this.position = playback_percent
},
startDrag(e, marker) {
if (e) {
e.preventDefault()
e.stopPropagation()
}
this.dragging_marker = marker
},
endDrag() {
this.dragging_marker = null
},
seekToCursor(e) {
if (!this.dragging_marker) {
let percent_x = this.getCursorPosition(e)
this.position = percent_x
this.$refs.video.currentTime = this.position * this.$refs.video.duration
return false
}
},
seekToMarker(marker) {
if (marker == 'start') {
this.$refs.video.currentTime = this.markers.start * this.$refs.video.duration
} else if (marker == 'end') {
this.$refs.video.currentTime = this.markers.end * this.$refs.video.duration
}
},
drag(e) {
if (this.dragging_marker == 'start') {
this.markers.start = Math.min(this.getCursorPosition(e), this.markers.end)
this.$refs.video.currentTime = this.markers.start * this.$refs.video.duration
} else if (this.dragging_marker == 'end') {
this.markers.end = Math.max(this.getCursorPosition(e, 12), this.markers.start)
this.$refs.video.currentTime = this.markers.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}%`
this.$refs.video.currentTime = percent_x * this.$refs.video.duration
}
},
getMarkerWidth() {
if (this.$refs.timeline) {
return this.marker_width / this.$refs.timeline.clientWidth
} else {
return 0.0
}
},
getCursorPosition(e, x_offset=0) {
let timeline_bounds = this.$refs.timeline.getBoundingClientRect()
let x = x_offset
// Get X coordinate
if (e.type == 'touchstart' || e.type == 'touchmove' || e.type == 'touchend' || e.type == 'touchcancel') {
let touch = e.touches[0] || e.changedTouches[0];
x += touch.clientX;
} else if (e.type == 'click' || e.type == 'mousedown' || e.type == 'mouseup' || e.type == 'mousemove' || e.type == 'mouseover' || e.type == 'mouseout' || e.type == 'mouseenter' || e.type == 'mouseleave') {
x += e.clientX;
}
// Clamp to parent bounds
if (x < timeline_bounds.left) {
x = timeline_bounds.left
} else if (x + 12 - x_offset > timeline_bounds.right) {
x = timeline_bounds.right - 12 + x_offset
}
// Convert position to percentage of timeline width
let relative_x = x - timeline_bounds.left
let percent_x = (relative_x / timeline_bounds.width)
return percent_x
}
},
computed: {
...mapState(['previewFile']),
......@@ -41,9 +187,44 @@ export default {
video {
background-color: #000000;
width: 100%;
height: 95%;
}
.export-btn {
float: right;
}
.timeline {
background-color: #000000;
height: 48px;
position: relative;
border-radius: 4px;
flex-grow: 1;
}
.marker {
color: #ffffff;
background-color: #0d6efd;
height:48px;
width: 12px;
border-radius: 4px;
position: absolute;
cursor: grab;
}
.marker_left {
border-left: #ffffff solid 1px;
}
.marker_right {
border-right: #ffffff solid 1px;
}
.playhead {
background-color: #ffffff;
width: 3px;
height: 48px;
position: absolute;
cursor: grab;
z-index: 100;
opacity: 80%;
}
.timeline-btn {
height: 48px;
width: 100%;
padding: 0.15em;
}
</style>
\ No newline at end of file
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