Commit 913ab881 by Jonathan Thomas

Improved timeline trimming, integrated Clip API calls (create, load, delete),…

Improved timeline trimming, integrated Clip API calls (create, load, delete), fixed duplicate upload bug, made errors prettier / parse key/value, capitalize, and handle 404 on editor
parent e7847e4c
......@@ -30,7 +30,7 @@
<!--router content-->
<div class="container">
<!-- show any errors from vuex -->
<div v-for="error in errors" :key="error.id" class="alert alert-danger alert-dismissible fade show" role="alert">
<div v-for="error in errors" :key="error.id" class="error-message alert alert-danger alert-dismissible fade show" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-exclamation-triangle-fill flex-shrink-0 me-2" viewBox="0 0 16 16" role="img" aria-label="Warning:">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</svg>
......@@ -68,7 +68,7 @@ export default {
await this.login(auth)
if (!this.isAuthenticated) {
// login failed, show login page
this.$router.push('/login')
await this.$router.push('/login')
}
}
}
......@@ -79,4 +79,7 @@ export default {
.navbar-light .navbar-nav .nav-link.active, .navbar-light .navbar-nav .show>.nav-link {
color: #ffffff;
}
.error-message {
text-transform: capitalize;
}
</style>
<template>
<div class="row mb-3 p-2 scrolling-container">
<h3>Clips <button type="button" class="btn btn-secondary clip-btn">Add Clip</button></h3>
<h3>Clips</h3>
<div class="col-12">
<div class="row gy-2 gx-2 mb-2" v-for="clip in clips" :key="clip.id">
......@@ -20,7 +20,7 @@
<div class="d-grid gap-1 p-2">
<button type="button" class="btn btn-outline-primary">Move Up</button>
<button type="button" class="btn btn-outline-primary">Move Down</button>
<button type="button" class="btn btn-outline-primary">Delete</button>
<button type="button" class="btn btn-outline-primary" @click="deleteClipBtn(clip.id)">Delete</button>
</div>
</div>
......@@ -34,18 +34,25 @@
</template>
<script>
import {mapActions, mapState} from "vuex"
export default {
name: "Clips.vue",
props: ['project'],
data() {
return {
clips: [
{ 'id': 1, 'name': 'filename1.mp4', 'start': 0, 'end': 10 },
{ 'id': 2, 'name': 'filename1.mp4', 'start': 3, 'end': 13 },
{ 'id': 3, 'name': 'filename1.mp4', 'start': 0, 'end': 5 },
{ 'id': 4, 'name': 'filename1.mp4', 'start': 10, 'end': 15 }
]
}
return {}
},
methods: {
deleteClipBtn(clip_id) {
this.deleteClip(clip_id)
},
...mapActions(['loadClips', 'createClip', 'deleteClip'])
},
computed: {
...mapState(['clips'])
},
async mounted() {
await this.loadClips(this.project.id)
}
}
</script>
......
......@@ -38,8 +38,8 @@ export default {
},
async fileChanged(event) {
let results = []
let data = new FormData();
for (let file of event.target.files) {
let data = new FormData();
data.append('media', file);
data.append('project', this.project.url);
data.append('json', "{}");
......@@ -58,7 +58,11 @@ export default {
this.$refs.fileUpload.value = null
},
toggleSelection(fileObject) {
this.setPreviewFile(fileObject)
if (fileObject != this.previewFile) {
this.setPreviewFile(fileObject)
} else {
this.setPreviewFile(null)
}
},
getSelectedClass(fileObject) {
if (this.previewFile && fileObject.id == this.previewFile.id) {
......
......@@ -33,16 +33,17 @@
<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">
<div 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 + '%'}">
<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: (markers.end - markers.start) * 100.0 + '%'}"></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">
<button type="button" class="btn btn-secondary timeline-btn" @click="createClipBtn">
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" fill="currentColor" class="bi bi-plus" viewBox="0 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>
......@@ -58,7 +59,7 @@
</template>
<script>
import {mapState} from "vuex";
import {mapState, mapActions} from "vuex";
export default {
name: "Preview.vue",
......@@ -72,10 +73,28 @@ export default {
start: 0.0,
end: 1.0
},
marker_width: 12
marker_width: 12,
marker_width_percent: 0.0
}
},
methods: {
async createClipBtn() {
let data = {
"file": this.previewFile.url,
"position": 0.0,
"start": this.markers.start * this.previewFile.json.duration,
"end": this.markers.end * this.previewFile.json.duration,
"layer": 1,
"project": this.project.url,
"json": {}
}
let payload = {
project_id: this.project.id,
project_url: this.project.url,
data
}
await this.createClip(payload)
},
togglePause() {
this.is_paused = this.$refs.video.paused
},
......@@ -134,7 +153,6 @@ export default {
} else {
return 0.0
}
},
getCursorPosition(e, x_offset=0) {
let timeline_bounds = this.$refs.timeline.getBoundingClientRect()
......@@ -159,7 +177,8 @@ export default {
let relative_x = x - timeline_bounds.left
let percent_x = (relative_x / timeline_bounds.width)
return percent_x
}
},
...mapActions(['createClip'])
},
computed: {
...mapState(['previewFile']),
......@@ -179,6 +198,9 @@ export default {
}
return ''
}
},
updated() {
this.marker_width_percent = this.getMarkerWidth()
}
}
</script>
......@@ -205,13 +227,21 @@ video {
width: 12px;
border-radius: 4px;
position: absolute;
cursor: grab;
cursor: ew-resize;
z-index: 10;
}
.clip {
background-color: #0d6efd;
height:48px;
position: absolute;
opacity: 50%;
z-index: 0;
}
.marker_left {
border-left: #ffffff solid 1px;
border-left: rgba(255, 255, 255, 0.5) solid 1px;
}
.marker_right {
border-right: #ffffff solid 1px;
border-right: rgba(255, 255, 255, 0.5) solid 1px;
}
.playhead {
background-color: #ffffff;
......
......@@ -14,16 +14,4 @@ instance.interceptors.request.use(function (config) {
return config;
});
// Handle authorization errors
instance.interceptors.response.use(
response => {
return response;
},
error => {
if (error.response.status === 401) {
//store.commit('addError', 'Invalid username or password, please login again')
}
return Promise.reject(error)
});
export { instance };
\ No newline at end of file
......@@ -7,6 +7,7 @@ export default createStore({
state: {
projects: [],
files: [],
clips: [],
user: null,
errors: [],
previewFile: null,
......@@ -15,8 +16,17 @@ export default createStore({
mutations: {
addError(state, message) {
let error = {'id': uuidv4(), 'message': message}
state.errors.push(error)
if (typeof(message) == "object") {
// Loop through object keys (potentially multiple errors)
for (const key in message) {
let error = {'id': uuidv4(), 'message': `${key}: ${message[key]}`}
state.errors.push(error)
}
} else {
// Single error message
let error = {'id': uuidv4(), 'message': message}
state.errors.push(error)
}
},
removeError(state, error) {
state.errors = state.errors.filter(err => err.id != error.id )
......@@ -33,6 +43,12 @@ export default createStore({
setFiles(state, files) {
state.files = files
},
setClips(state, clips) {
state.clips = clips
},
deleteClip(state, clip_id) {
state.clips = state.clips.filter(clip => clip.id != clip_id)
},
setPreviewFile(state, file) {
state.previewFile = file
},
......@@ -63,7 +79,7 @@ export default createStore({
commit('setUser', user_object)
} catch(err) {
commit('setUser', null)
commit('addError', JSON.stringify(err.response.data))
commit('addError', err.response.data)
}
},
async loadProjects({commit}) {
......@@ -71,14 +87,14 @@ export default createStore({
const response = await instance.get('/projects/')
commit('setProjects', response.data.results)
} catch(err) {
commit('addError', JSON.stringify(err.response.data))
commit('addError', err.response.data)
}
},
async getProject({commit}, project_id) {
try {
return instance.get(`projects/${project_id}/`)
} catch(err) {
commit('addError', JSON.stringify(err.response.data))
commit('addError', err.response.data)
}
},
async createProject({dispatch, commit}, payload) {
......@@ -86,7 +102,7 @@ export default createStore({
await instance.post('/projects/', payload)
dispatch('loadProjects')
} catch(err) {
commit('addError', JSON.stringify(err.response.data))
commit('addError', err.response.data)
}
},
async deleteProject({commit, dispatch}, project_id) {
......@@ -94,7 +110,7 @@ export default createStore({
await instance.delete(`projects/${project_id}/`)
dispatch('loadProjects')
} catch(err) {
commit('addError', JSON.stringify(err.response.data))
commit('addError', err.response.data)
}
},
async loadFiles({commit}, project_id) {
......@@ -102,7 +118,31 @@ export default createStore({
const response = await instance.get(`projects/${project_id}/files/`)
commit('setFiles', response.data.results)
} catch(err) {
commit('addError', JSON.stringify(err.response.data))
commit('addError', err.response.data)
}
},
async loadClips({commit}, project_id) {
try {
const response = await instance.get(`projects/${project_id}/clips/`)
commit('setClips', response.data.results)
} catch(err) {
commit('addError', err.response.data)
}
},
async createClip({dispatch, commit}, payload) {
try {
await instance.post(`${payload.project_url}clips/`, payload.data)
dispatch('loadClips', payload.project_id)
} catch(err) {
commit('addError', err.response.data)
}
},
async deleteClip({commit}, clip_id) {
try {
await instance.delete(`clips/${clip_id}/`)
commit('deleteClip', clip_id)
} catch(err) {
commit('addError', err.response.data)
}
},
async createFile({dispatch, commit}, payload) {
......@@ -124,7 +164,7 @@ export default createStore({
dispatch('loadFiles', payload.project_id)
} catch(err) {
commit('removeUpload', uploadID)
commit('addError', JSON.stringify(err.response.data))
commit('addError', err.response.data)
}
}
},
......
......@@ -11,7 +11,7 @@
</template>
<script>
import { mapActions } from "vuex";
import { mapActions, mapMutations } from "vuex";
import Files from "../components/Files";
import Clips from "../components/Clips";
import Preview from "../components/Preview";
......@@ -25,7 +25,8 @@ export default {
}
},
methods: {
...mapActions(['getProject'])
...mapActions(['getProject']),
...mapMutations(['addError'])
},
computed: {
id() {
......@@ -36,9 +37,16 @@ export default {
Files, Clips, Preview
},
async mounted() {
let results = await this.getProject(this.$route.params.id)
this.project = results.data
this.hasData = true
try {
let results = await this.getProject(this.$route.params.id)
this.project = results.data
this.hasData = true
} catch(err) {
// Handle 404
this.addError(err.response.data)
await this.$router.push(`/`)
}
}
}
</script>
......
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