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 @@ ...@@ -30,7 +30,7 @@
<!--router content--> <!--router content-->
<div class="container"> <div class="container">
<!-- show any errors from vuex --> <!-- 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:"> <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"/> <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> </svg>
...@@ -68,7 +68,7 @@ export default { ...@@ -68,7 +68,7 @@ export default {
await this.login(auth) await this.login(auth)
if (!this.isAuthenticated) { if (!this.isAuthenticated) {
// login failed, show login page // login failed, show login page
this.$router.push('/login') await this.$router.push('/login')
} }
} }
} }
...@@ -79,4 +79,7 @@ export default { ...@@ -79,4 +79,7 @@ export default {
.navbar-light .navbar-nav .nav-link.active, .navbar-light .navbar-nav .show>.nav-link { .navbar-light .navbar-nav .nav-link.active, .navbar-light .navbar-nav .show>.nav-link {
color: #ffffff; color: #ffffff;
} }
.error-message {
text-transform: capitalize;
}
</style> </style>
<template> <template>
<div class="row mb-3 p-2 scrolling-container"> <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="col-12">
<div class="row gy-2 gx-2 mb-2" v-for="clip in clips" :key="clip.id"> <div class="row gy-2 gx-2 mb-2" v-for="clip in clips" :key="clip.id">
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
<div class="d-grid gap-1 p-2"> <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 Up</button>
<button type="button" class="btn btn-outline-primary">Move Down</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>
</div> </div>
...@@ -34,18 +34,25 @@ ...@@ -34,18 +34,25 @@
</template> </template>
<script> <script>
import {mapActions, mapState} from "vuex"
export default { export default {
name: "Clips.vue", name: "Clips.vue",
props: ['project'], props: ['project'],
data() { data() {
return { return {}
clips: [ },
{ 'id': 1, 'name': 'filename1.mp4', 'start': 0, 'end': 10 }, methods: {
{ 'id': 2, 'name': 'filename1.mp4', 'start': 3, 'end': 13 }, deleteClipBtn(clip_id) {
{ 'id': 3, 'name': 'filename1.mp4', 'start': 0, 'end': 5 }, this.deleteClip(clip_id)
{ 'id': 4, 'name': 'filename1.mp4', 'start': 10, 'end': 15 } },
] ...mapActions(['loadClips', 'createClip', 'deleteClip'])
} },
computed: {
...mapState(['clips'])
},
async mounted() {
await this.loadClips(this.project.id)
} }
} }
</script> </script>
......
...@@ -38,8 +38,8 @@ export default { ...@@ -38,8 +38,8 @@ export default {
}, },
async fileChanged(event) { async fileChanged(event) {
let results = [] let results = []
let data = new FormData();
for (let file of event.target.files) { for (let file of event.target.files) {
let data = new FormData();
data.append('media', file); data.append('media', file);
data.append('project', this.project.url); data.append('project', this.project.url);
data.append('json', "{}"); data.append('json', "{}");
...@@ -58,7 +58,11 @@ export default { ...@@ -58,7 +58,11 @@ export default {
this.$refs.fileUpload.value = null this.$refs.fileUpload.value = null
}, },
toggleSelection(fileObject) { toggleSelection(fileObject) {
this.setPreviewFile(fileObject) if (fileObject != this.previewFile) {
this.setPreviewFile(fileObject)
} else {
this.setPreviewFile(null)
}
}, },
getSelectedClass(fileObject) { getSelectedClass(fileObject) {
if (this.previewFile && fileObject.id == this.previewFile.id) { if (this.previewFile && fileObject.id == this.previewFile.id) {
......
...@@ -33,16 +33,17 @@ ...@@ -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"/> <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 ref="end" class="marker marker_right" @click="seekToMarker('end')" @mousedown="startDrag($event, 'end')" @touchstart="startDrag($event, 'end')" :style="{left: (markers.end - getMarkerWidth()) * 100.0 + '%'}"> <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="1 0 16 16"> <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"/> <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> </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">
<button type="button" class="btn btn-secondary timeline-btn"> <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="2 0 16 16"> <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"/> <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> </svg>
</button> </button>
...@@ -58,7 +59,7 @@ ...@@ -58,7 +59,7 @@
</template> </template>
<script> <script>
import {mapState} from "vuex"; import {mapState, mapActions} from "vuex";
export default { export default {
name: "Preview.vue", name: "Preview.vue",
...@@ -72,10 +73,28 @@ export default { ...@@ -72,10 +73,28 @@ export default {
start: 0.0, start: 0.0,
end: 1.0 end: 1.0
}, },
marker_width: 12 marker_width: 12,
marker_width_percent: 0.0
} }
}, },
methods: { 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() { togglePause() {
this.is_paused = this.$refs.video.paused this.is_paused = this.$refs.video.paused
}, },
...@@ -134,7 +153,6 @@ export default { ...@@ -134,7 +153,6 @@ export default {
} else { } else {
return 0.0 return 0.0
} }
}, },
getCursorPosition(e, x_offset=0) { getCursorPosition(e, x_offset=0) {
let timeline_bounds = this.$refs.timeline.getBoundingClientRect() let timeline_bounds = this.$refs.timeline.getBoundingClientRect()
...@@ -159,7 +177,8 @@ export default { ...@@ -159,7 +177,8 @@ export default {
let relative_x = x - timeline_bounds.left let relative_x = x - timeline_bounds.left
let percent_x = (relative_x / timeline_bounds.width) let percent_x = (relative_x / timeline_bounds.width)
return percent_x return percent_x
} },
...mapActions(['createClip'])
}, },
computed: { computed: {
...mapState(['previewFile']), ...mapState(['previewFile']),
...@@ -179,6 +198,9 @@ export default { ...@@ -179,6 +198,9 @@ export default {
} }
return '' return ''
} }
},
updated() {
this.marker_width_percent = this.getMarkerWidth()
} }
} }
</script> </script>
...@@ -205,13 +227,21 @@ video { ...@@ -205,13 +227,21 @@ video {
width: 12px; width: 12px;
border-radius: 4px; border-radius: 4px;
position: absolute; 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 { .marker_left {
border-left: #ffffff solid 1px; border-left: rgba(255, 255, 255, 0.5) solid 1px;
} }
.marker_right { .marker_right {
border-right: #ffffff solid 1px; border-right: rgba(255, 255, 255, 0.5) solid 1px;
} }
.playhead { .playhead {
background-color: #ffffff; background-color: #ffffff;
......
...@@ -14,16 +14,4 @@ instance.interceptors.request.use(function (config) { ...@@ -14,16 +14,4 @@ instance.interceptors.request.use(function (config) {
return 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 }; export { instance };
\ No newline at end of file
...@@ -7,6 +7,7 @@ export default createStore({ ...@@ -7,6 +7,7 @@ export default createStore({
state: { state: {
projects: [], projects: [],
files: [], files: [],
clips: [],
user: null, user: null,
errors: [], errors: [],
previewFile: null, previewFile: null,
...@@ -15,8 +16,17 @@ export default createStore({ ...@@ -15,8 +16,17 @@ export default createStore({
mutations: { mutations: {
addError(state, message) { addError(state, message) {
let error = {'id': uuidv4(), 'message': message} if (typeof(message) == "object") {
state.errors.push(error) // 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) { removeError(state, error) {
state.errors = state.errors.filter(err => err.id != error.id ) state.errors = state.errors.filter(err => err.id != error.id )
...@@ -33,6 +43,12 @@ export default createStore({ ...@@ -33,6 +43,12 @@ export default createStore({
setFiles(state, files) { setFiles(state, files) {
state.files = 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) { setPreviewFile(state, file) {
state.previewFile = file state.previewFile = file
}, },
...@@ -63,7 +79,7 @@ export default createStore({ ...@@ -63,7 +79,7 @@ export default createStore({
commit('setUser', user_object) commit('setUser', user_object)
} catch(err) { } catch(err) {
commit('setUser', null) commit('setUser', null)
commit('addError', JSON.stringify(err.response.data)) commit('addError', err.response.data)
} }
}, },
async loadProjects({commit}) { async loadProjects({commit}) {
...@@ -71,14 +87,14 @@ export default createStore({ ...@@ -71,14 +87,14 @@ export default createStore({
const response = await instance.get('/projects/') const response = await instance.get('/projects/')
commit('setProjects', response.data.results) commit('setProjects', response.data.results)
} catch(err) { } catch(err) {
commit('addError', JSON.stringify(err.response.data)) commit('addError', err.response.data)
} }
}, },
async getProject({commit}, project_id) { async getProject({commit}, project_id) {
try { try {
return instance.get(`projects/${project_id}/`) return instance.get(`projects/${project_id}/`)
} catch(err) { } catch(err) {
commit('addError', JSON.stringify(err.response.data)) commit('addError', err.response.data)
} }
}, },
async createProject({dispatch, commit}, payload) { async createProject({dispatch, commit}, payload) {
...@@ -86,7 +102,7 @@ export default createStore({ ...@@ -86,7 +102,7 @@ export default createStore({
await instance.post('/projects/', payload) await instance.post('/projects/', payload)
dispatch('loadProjects') dispatch('loadProjects')
} catch(err) { } catch(err) {
commit('addError', JSON.stringify(err.response.data)) commit('addError', err.response.data)
} }
}, },
async deleteProject({commit, dispatch}, project_id) { async deleteProject({commit, dispatch}, project_id) {
...@@ -94,7 +110,7 @@ export default createStore({ ...@@ -94,7 +110,7 @@ export default createStore({
await instance.delete(`projects/${project_id}/`) await instance.delete(`projects/${project_id}/`)
dispatch('loadProjects') dispatch('loadProjects')
} catch(err) { } catch(err) {
commit('addError', JSON.stringify(err.response.data)) commit('addError', err.response.data)
} }
}, },
async loadFiles({commit}, project_id) { async loadFiles({commit}, project_id) {
...@@ -102,7 +118,31 @@ export default createStore({ ...@@ -102,7 +118,31 @@ export default createStore({
const response = await instance.get(`projects/${project_id}/files/`) const response = await instance.get(`projects/${project_id}/files/`)
commit('setFiles', response.data.results) commit('setFiles', response.data.results)
} catch(err) { } 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) { async createFile({dispatch, commit}, payload) {
...@@ -124,7 +164,7 @@ export default createStore({ ...@@ -124,7 +164,7 @@ export default createStore({
dispatch('loadFiles', payload.project_id) dispatch('loadFiles', payload.project_id)
} catch(err) { } catch(err) {
commit('removeUpload', uploadID) commit('removeUpload', uploadID)
commit('addError', JSON.stringify(err.response.data)) commit('addError', err.response.data)
} }
} }
}, },
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
</template> </template>
<script> <script>
import { mapActions } from "vuex"; import { mapActions, mapMutations } from "vuex";
import Files from "../components/Files"; import Files from "../components/Files";
import Clips from "../components/Clips"; import Clips from "../components/Clips";
import Preview from "../components/Preview"; import Preview from "../components/Preview";
...@@ -25,7 +25,8 @@ export default { ...@@ -25,7 +25,8 @@ export default {
} }
}, },
methods: { methods: {
...mapActions(['getProject']) ...mapActions(['getProject']),
...mapMutations(['addError'])
}, },
computed: { computed: {
id() { id() {
...@@ -36,9 +37,16 @@ export default { ...@@ -36,9 +37,16 @@ export default {
Files, Clips, Preview Files, Clips, Preview
}, },
async mounted() { async mounted() {
let results = await this.getProject(this.$route.params.id) try {
this.project = results.data let results = await this.getProject(this.$route.params.id)
this.hasData = true this.project = results.data
this.hasData = true
} catch(err) {
// Handle 404
this.addError(err.response.data)
await this.$router.push(`/`)
}
} }
} }
</script> </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