Commit ca7798bd by Jonathan Thomas

Adding new clip effects modal, and /info/effects/ endpoint to load all effects…

Adding new clip effects modal, and /info/effects/ endpoint to load all effects from the cloud api endpoint. This is still a work in progress.
parent 5025e0ea
...@@ -37,23 +37,15 @@ ...@@ -37,23 +37,15 @@
<!-- Clip sequence # badge --> <!-- Clip sequence # badge -->
<span class="clip-badge badge">{{ index + 1 }}</span> <span class="clip-badge badge">{{ index + 1 }}</span>
<!-- Clip text icon --> <!-- Clip effects icon -->
<div class="context-menu" @mousedown.stop> <div class="context-menu" @mousedown.stop>
<span @click.prevent="toggleText(clip)" <span
class="menu-icon toggle-icon" @click.prevent="openClipEffectsModal(clip, index)"
:class="{ active: isTextActive(clip) }" class="menu-icon toggle-icon effects-trigger"
title="Add Text" :class="{ active: hasClipEnhancements(clip) }"
draggable="false"> title="Effects and Text Captions"
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-textarea-t" viewBox="0 0 16 16"> draggable="false"
<path d="M1.5 2.5A1.5 1.5 0 0 1 3 1h10a1.5 1.5 0 0 1 1.5 1.5v3.563a2 2 0 0 1 0 3.874V13.5A1.5 1.5 0 0 1 13 15H3a1.5 1.5 0 0 1-1.5-1.5V9.937a2 2 0 0 1 0-3.874V2.5zm1 3.563a2 2 0 0 1 0 3.874V13.5a.5.5 0 0 0 .5.5h10a.5.5 0 0 0 .5-.5V9.937a2 2 0 0 1 0-3.874V2.5A.5.5 0 0 0 13 2H3a.5.5 0 0 0-.5.5v3.563zM2 7a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm12 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/> >
<path d="M11.434 4H4.566L4.5 5.994h.386c.21-1.252.612-1.446 2.173-1.495l.343-.011v6.343c0 .537-.116.665-1.049.748V12h3.294v-.421c-.938-.083-1.054-.21-1.054-.748V4.488l.348.01c1.56.05 1.963.244 2.173 1.496h.386L11.434 4z"/>
</svg>
</span>
<span @click.prevent="toggleFx(clip)"
class="menu-icon toggle-icon"
:class="{ active: isFxActive(clip) }"
title="Add FX"
draggable="false">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-stars" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-stars" viewBox="0 0 16 16">
<path d="M7.5 2.5a.5.5 0 0 1 .5.5c0 .654.291 1.304.854 1.866.562.563 1.212.854 1.866.854a.5.5 0 0 1 0 1c-.654 0-1.304.291-1.866.854-.563.562-.854 1.212-.854 1.866a.5.5 0 0 1-1 0c0-.654-.291-1.304-.854-1.866C5.084 6.511 4.434 6.22 3.78 6.22a.5.5 0 0 1 0-1c.654 0 1.304-.291 1.866-.854C6.208 3.804 6.5 3.154 6.5 2.5a.5.5 0 0 1 .5-.5z"/> <path d="M7.5 2.5a.5.5 0 0 1 .5.5c0 .654.291 1.304.854 1.866.562.563 1.212.854 1.866.854a.5.5 0 0 1 0 1c-.654 0-1.304.291-1.866.854-.563.562-.854 1.212-.854 1.866a.5.5 0 0 1-1 0c0-.654-.291-1.304-.854-1.866C5.084 6.511 4.434 6.22 3.78 6.22a.5.5 0 0 1 0-1c.654 0 1.304-.291 1.866-.854C6.208 3.804 6.5 3.154 6.5 2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M2.5 9.5a.5.5 0 0 1 .5.5c0 .379.168.756.494 1.082.326.326.703.494 1.082.494a.5.5 0 0 1 0 1 2.58 2.58 0 0 0-1.082.494A1.53 1.53 0 0 0 3 14.152a.5.5 0 0 1-1 0 1.53 1.53 0 0 0-.494-1.082A2.58 2.58 0 0 0 .152 12.5a.5.5 0 0 1 0-1c.379 0 .756-.168 1.082-.494.326-.326.494-.703.494-1.082a.5.5 0 0 1 .5-.5zM12.5.5a.5.5 0 0 1 .5.5c0 .506.225 1.005.659 1.439.434.434.933.659 1.439.659a.5.5 0 0 1 0 1 2.74 2.74 0 0 0-1.439.659A2.04 2.04 0 0 0 13 5.196a.5.5 0 0 1-1 0 2.04 2.04 0 0 0-.659-1.439A2.74 2.74 0 0 0 9.902 3.1a.5.5 0 0 1 0-1c.506 0 1.005-.225 1.439-.659C11.774 1.505 12 .506 12 0a.5.5 0 0 1 .5-.5z"/> <path d="M2.5 9.5a.5.5 0 0 1 .5.5c0 .379.168.756.494 1.082.326.326.703.494 1.082.494a.5.5 0 0 1 0 1 2.58 2.58 0 0 0-1.082.494A1.53 1.53 0 0 0 3 14.152a.5.5 0 0 1-1 0 1.53 1.53 0 0 0-.494-1.082A2.58 2.58 0 0 0 .152 12.5a.5.5 0 0 1 0-1c.379 0 .756-.168 1.082-.494.326-.326.494-.703.494-1.082a.5.5 0 0 1 .5-.5zM12.5.5a.5.5 0 0 1 .5.5c0 .506.225 1.005.659 1.439.434.434.933.659 1.439.659a.5.5 0 0 1 0 1 2.74 2.74 0 0 0-1.439.659A2.04 2.04 0 0 0 13 5.196a.5.5 0 0 1-1 0 2.04 2.04 0 0 0-.659-1.439A2.74 2.74 0 0 0 9.902 3.1a.5.5 0 0 1 0-1c.506 0 1.005-.225 1.439-.659C11.774 1.505 12 .506 12 0a.5.5 0 0 1 .5-.5z"/>
...@@ -132,16 +124,25 @@ ...@@ -132,16 +124,25 @@
</div> </div>
</div> </div>
</div> </div>
<ClipEffectsModal
ref="clipEffectsModal"
@save="handleClipEffectsSave"
/>
</template> </template>
<script> <script>
import {mapActions, mapState, mapMutations, mapGetters} from "vuex" import {mapActions, mapState, mapMutations, mapGetters} from "vuex"
import { Modal } from "bootstrap" import { Modal } from "bootstrap"
import caption from "../data/caption.json" import caption from "../data/caption.json"
import ClipEffectsModal from "./ClipEffectsModal"
export default { export default {
name: "Clips.vue", name: "Clips.vue",
props: ['project'], props: ['project'],
components: {
ClipEffectsModal
},
data() { data() {
return { return {
show_spinner: false, show_spinner: false,
...@@ -152,45 +153,42 @@ export default { ...@@ -152,45 +153,42 @@ export default {
dragOverPosition: null, dragOverPosition: null,
autoScrollDirection: 0, autoScrollDirection: 0,
autoScrollFrame: null, autoScrollFrame: null,
dragListenersActive: false dragListenersActive: false,
pendingClipEffects: null
} }
}, },
methods: { methods: {
toggleText(clipObj) { openClipEffectsModal(clipObj, index) {
if (Object.prototype.hasOwnProperty.call(clipObj.json, 'text')) { const fileName = clipObj?.name || clipObj?.file_name || ""
delete clipObj.json.text const appliedEffects = Array.isArray(clipObj?.json?.effects) ? clipObj.json.effects : []
} else { if (this.$refs.clipEffectsModal) {
clipObj.json.text = "" this.$refs.clipEffectsModal.open({
if (clipObj.json.fx_enabled) { clip: clipObj,
delete clipObj.json.fx_enabled clipIndex: index + 1,
} clipName: clipObj?.name,
fileName,
text: clipObj?.json?.text || "",
appliedEffects
})
} }
let payload = { data: clipObj }
this.editClip(payload)
}, },
toggleFx(clipObj) { hasClipEnhancements(clipObj) {
if (clipObj.json.fx_enabled) { if (!clipObj || !clipObj.json) {
delete clipObj.json.fx_enabled return false
} else { }
clipObj.json.fx_enabled = true const hasText = !!clipObj.json.text
if (Object.prototype.hasOwnProperty.call(clipObj.json, 'text')) { const hasFxFlag = !!clipObj.json.fx_enabled
delete clipObj.json.text const hasAppliedEffects = Array.isArray(clipObj.json.effects) && clipObj.json.effects.length > 0
} return hasText || hasFxFlag || hasAppliedEffects
} },
let payload = { data: clipObj } handleClipEffectsSave(payload) {
this.editClip(payload) this.pendingClipEffects = payload
}, },
updateTextInput(e, clipObj) { updateTextInput(e, clipObj) {
clipObj.json.text = e.target.value clipObj.json.text = e.target.value
let payload = { data: clipObj } let payload = { data: clipObj }
this.editClip(payload) this.editClip(payload)
}, },
isTextActive(clipObj) {
return Object.prototype.hasOwnProperty.call(clipObj.json, 'text')
},
isFxActive(clipObj) {
return !!clipObj.json.fx_enabled
},
onDragStart(event, clipObj, index) { onDragStart(event, clipObj, index) {
if (this.thumbnailedClips.length <= 1) { if (this.thumbnailedClips.length <= 1) {
event.preventDefault() event.preventDefault()
...@@ -609,6 +607,16 @@ export default { ...@@ -609,6 +607,16 @@ export default {
color: #0d6efd; color: #0d6efd;
background-color: rgba(13, 110, 253, 0.25); background-color: rgba(13, 110, 253, 0.25);
} }
.effects-trigger {
padding: 4px;
border-radius: 999px;
background-color: rgba(0, 0, 0, 0.45);
transition: background-color 0.15s ease, color 0.15s ease;
}
.effects-trigger:hover {
color: #0d6efd;
background-color: rgba(13, 110, 253, 0.2);
}
.menu-icon:hover { .menu-icon:hover {
color: #0d6efd; color: #0d6efd;
} }
......
...@@ -9,6 +9,18 @@ ...@@ -9,6 +9,18 @@
<div class="row preview-stage-row" v-if="hasPreviewFile || show_spinner"> <div class="row preview-stage-row" v-if="hasPreviewFile || show_spinner">
<div class="col-12" ref="previewStageWrapper"> <div class="col-12" ref="previewStageWrapper">
<div class="preview-stage" :class="{'is-resizing': isResizingStage}" :style="previewStageStyle"> <div class="preview-stage" :class="{'is-resizing': isResizingStage}" :style="previewStageStyle">
<button
v-if="canOpenClipEffects"
type="button"
class="preview-effects-button effects-trigger"
title="Effects and Text Captions"
@click.stop="openPreviewEffects"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-stars" viewBox="0 0 16 16">
<path d="M7.5 2.5a.5.5 0 0 1 .5.5c0 .654.291 1.304.854 1.866.562.563 1.212.854 1.866.854a.5.5 0 0 1 0 1c-.654 0-1.304.291-1.866.854-.563.562-.854 1.212-.854 1.866a.5.5 0 0 1-1 0c0-.654-.291-1.304-.854-1.866C5.084 6.511 4.434 6.22 3.78 6.22a.5.5 0 0 1 0-1c.654 0 1.304-.291 1.866-.854C6.208 3.804 6.5 3.154 6.5 2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M2.5 9.5a.5.5 0 0 1 .5.5c0 .379.168.756.494 1.082.326.326.703.494 1.082.494a.5.5 0 0 1 0 1 2.58 2.58 0 0 0-1.082.494A1.53 1.53 0 0 0 3 14.152a.5.5 0 0 1-1 0 1.53 1.53 0 0 0-.494-1.082A2.58 2.58 0 0 0 .152 12.5a.5.5 0 0 1 0-1c.379 0 .756-.168 1.082-.494.326-.326.494-.703.494-1.082a.5.5 0 0 1 .5-.5zM12.5.5a.5.5 0 0 1 .5.5c0 .506.225 1.005.659 1.439.434.434.933.659 1.439.659a.5.5 0 0 1 0 1 2.74 2.74 0 0 0-1.439.659A2.04 2.04 0 0 0 13 5.196a.5.5 0 0 1-1 0 2.04 2.04 0 0 0-.659-1.439A2.74 2.74 0 0 0 9.902 3.1a.5.5 0 0 1 0-1c.506 0 1.005-.225 1.439-.659C11.774 1.505 12 .506 12 0a.5.5 0 0 1 .5-.5z"/>
</svg>
</button>
<div v-if="show_spinner" class="preview-stage-spinner"> <div v-if="show_spinner" class="preview-stage-spinner">
<div class="spinner-border" role="status"> <div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
...@@ -89,11 +101,16 @@ ...@@ -89,11 +101,16 @@
</div> </div>
</div> </div>
<ClipEffectsModal
ref="clipEffectsModal"
@save="handleClipEffectsSave"
/>
</template> </template>
<script> <script>
import {mapState, mapActions, mapGetters, mapMutations} from "vuex"; import {mapState, mapActions, mapGetters, mapMutations} from "vuex";
import {fixImageDuration} from "../store/axios"; import {fixImageDuration} from "../store/axios";
import ClipEffectsModal from "./ClipEffectsModal";
const IMAGE_FILE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'avif', 'svg', 'tif', 'tiff', 'heic', 'heif']; const IMAGE_FILE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'avif', 'svg', 'tif', 'tiff', 'heic', 'heif'];
const AUDIO_FILE_EXTENSIONS = ['mp3', 'aac', 'm4a', 'wav', 'oga', 'ogg', 'flac', 'opus', 'wma', 'aif', 'aiff']; const AUDIO_FILE_EXTENSIONS = ['mp3', 'aac', 'm4a', 'wav', 'oga', 'ogg', 'flac', 'opus', 'wma', 'aif', 'aiff'];
...@@ -123,6 +140,9 @@ const MEDIA_MIME_TYPES = { ...@@ -123,6 +140,9 @@ const MEDIA_MIME_TYPES = {
export default { export default {
name: "Preview.vue", name: "Preview.vue",
props: ['project'], props: ['project'],
components: {
ClipEffectsModal
},
data() { data() {
return { return {
dragging_marker: null, dragging_marker: null,
...@@ -148,7 +168,8 @@ export default { ...@@ -148,7 +168,8 @@ export default {
stageResizeStartHeight: 0, stageResizeStartHeight: 0,
isResizingStage: false, isResizingStage: false,
stageMinHeight: 220, stageMinHeight: 220,
stageMaxHeight: 720 stageMaxHeight: 720,
pendingClipEffects: null
} }
}, },
methods: { methods: {
...@@ -511,6 +532,28 @@ export default { ...@@ -511,6 +532,28 @@ export default {
this.stageHeightManual = null this.stageHeightManual = null
this.scheduleStageMeasure() this.scheduleStageMeasure()
}, },
openPreviewEffects() {
if (!this.$refs.clipEffectsModal || !this.preview.file) {
return
}
const appliedEffects = Array.isArray(this.preview.clip?.json?.effects) ? this.preview.clip.json.effects : []
this.$refs.clipEffectsModal.open({
clip: this.preview.clip || null,
clipName: this.preview.clip?.name || this.getPreviewFileName(),
fileName: this.getPreviewFileName(),
text: this.preview.clip?.json?.text || "",
appliedEffects
})
},
getPreviewFileName() {
if (!this.preview.file) {
return 'Current Clip'
}
return this.preview.file.name || this.preview.file.title || this.preview.file.filename || this.preview.file.media?.split('/').pop() || 'Current Clip'
},
handleClipEffectsSave(payload) {
this.pendingClipEffects = payload
},
...mapActions(['createClip', 'editClip', 'moveClip']), ...mapActions(['createClip', 'editClip', 'moveClip']),
...mapMutations(['setPreview', 'setPreviewPosition', 'setPreviewFile']) ...mapMutations(['setPreview', 'setPreviewPosition', 'setPreviewFile'])
}, },
...@@ -536,6 +579,9 @@ export default { ...@@ -536,6 +579,9 @@ export default {
return false return false
} }
}, },
canOpenClipEffects() {
return this.hasPreviewFile
},
hasAnyFiles() { hasAnyFiles() {
return Array.isArray(this.files) && this.files.length > 0 return Array.isArray(this.files) && this.files.length > 0
}, },
...@@ -664,6 +710,30 @@ export default { ...@@ -664,6 +710,30 @@ export default {
</script> </script>
<style scoped> <style scoped>
.preview-effects-button {
position: absolute;
top: 10px;
right: 10px;
border: none;
padding: 6px;
border-radius: 999px;
background-color: rgba(0, 0, 0, 0.5);
color: #ffffff;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s ease, color 0.15s ease;
}
.preview-effects-button:hover {
color: #0d6efd;
background-color: rgba(13, 110, 253, 0.2);
}
.preview-effects-button:focus-visible {
outline: 2px solid #0d6efd;
}
.preview-effects-button svg {
pointer-events: none;
}
.preview-stage-row { .preview-stage-row {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
......
...@@ -14,6 +14,7 @@ export default createStore({ ...@@ -14,6 +14,7 @@ export default createStore({
effects: [], effects: [],
project: null, project: null,
current_export: { progress: 0.0 }, current_export: { progress: 0.0 },
effectCatalog: [],
user: null, user: null,
preview: { preview: {
file: null, file: null,
...@@ -74,6 +75,9 @@ export default createStore({ ...@@ -74,6 +75,9 @@ export default createStore({
effectObj effectObj
] ]
}, },
setEffectCatalog(state, effects) {
state.effectCatalog = effects || []
},
updateProjectThumbnail(state, payload) { updateProjectThumbnail(state, payload) {
state.projects = [ state.projects = [
...state.projects = state.projects.filter(p => p.id != payload.obj.id), ...state.projects = state.projects.filter(p => p.id != payload.obj.id),
...@@ -517,6 +521,20 @@ export default createStore({ ...@@ -517,6 +521,20 @@ export default createStore({
commit('addError', err.response.data) commit('addError', err.response.data)
} }
}, },
async loadEffectCatalog({state, commit}) {
if (state.effectCatalog && state.effectCatalog.length > 0) {
return state.effectCatalog
}
try {
const response = await instance.get('/info/effects/')
const data = response.data && response.data.results ? response.data.results : response.data
commit('setEffectCatalog', data || [])
return data
} catch(err) {
commit('addError', err.response?.data || err.message)
return []
}
},
async createEffect({commit}, payload) { async createEffect({commit}, payload) {
try { try {
const response = await instance.post('/effects/', payload) const response = await instance.post('/effects/', payload)
......
...@@ -37,7 +37,7 @@ export default { ...@@ -37,7 +37,7 @@ export default {
this.checkExportProgress(this.activeExports[0]) this.checkExportProgress(this.activeExports[0])
} }
}, },
...mapActions(['getProject', 'loadExports', 'checkExportProgress', 'loadEffects', 'attachThumbnail']), ...mapActions(['getProject', 'loadExports', 'checkExportProgress', 'loadEffects', 'loadEffectCatalog', 'attachThumbnail']),
...mapMutations(['addError', 'setProject', 'setExports']) ...mapMutations(['addError', 'setProject', 'setExports'])
}, },
computed: { computed: {
...@@ -55,6 +55,7 @@ export default { ...@@ -55,6 +55,7 @@ export default {
let project_id = this.$route.params.id let project_id = this.$route.params.id
this.loadExports(project_id) this.loadExports(project_id)
this.loadEffects(project_id) this.loadEffects(project_id)
this.loadEffectCatalog()
let results = await this.getProject(project_id) let results = await this.getProject(project_id)
this.setProject(results.data) this.setProject(results.data)
this.hasData = true this.hasData = true
...@@ -80,4 +81,4 @@ export default { ...@@ -80,4 +81,4 @@ export default {
<style scoped> <style scoped>
</style> </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