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 @@
<!-- Clip sequence # badge -->
<span class="clip-badge badge">{{ index + 1 }}</span>
<!-- Clip text icon -->
<!-- Clip effects icon -->
<div class="context-menu" @mousedown.stop>
<span @click.prevent="toggleText(clip)"
class="menu-icon toggle-icon"
:class="{ active: isTextActive(clip) }"
title="Add Text"
draggable="false">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-textarea-t" viewBox="0 0 16 16">
<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">
<span
@click.prevent="openClipEffectsModal(clip, index)"
class="menu-icon toggle-icon effects-trigger"
:class="{ active: hasClipEnhancements(clip) }"
title="Effects and Text Captions"
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">
<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"/>
......@@ -132,16 +124,25 @@
</div>
</div>
</div>
<ClipEffectsModal
ref="clipEffectsModal"
@save="handleClipEffectsSave"
/>
</template>
<script>
import {mapActions, mapState, mapMutations, mapGetters} from "vuex"
import { Modal } from "bootstrap"
import caption from "../data/caption.json"
import ClipEffectsModal from "./ClipEffectsModal"
export default {
name: "Clips.vue",
props: ['project'],
components: {
ClipEffectsModal
},
data() {
return {
show_spinner: false,
......@@ -152,45 +153,42 @@ export default {
dragOverPosition: null,
autoScrollDirection: 0,
autoScrollFrame: null,
dragListenersActive: false
dragListenersActive: false,
pendingClipEffects: null
}
},
methods: {
toggleText(clipObj) {
if (Object.prototype.hasOwnProperty.call(clipObj.json, 'text')) {
delete clipObj.json.text
} else {
clipObj.json.text = ""
if (clipObj.json.fx_enabled) {
delete clipObj.json.fx_enabled
}
openClipEffectsModal(clipObj, index) {
const fileName = clipObj?.name || clipObj?.file_name || ""
const appliedEffects = Array.isArray(clipObj?.json?.effects) ? clipObj.json.effects : []
if (this.$refs.clipEffectsModal) {
this.$refs.clipEffectsModal.open({
clip: clipObj,
clipIndex: index + 1,
clipName: clipObj?.name,
fileName,
text: clipObj?.json?.text || "",
appliedEffects
})
}
let payload = { data: clipObj }
this.editClip(payload)
},
toggleFx(clipObj) {
if (clipObj.json.fx_enabled) {
delete clipObj.json.fx_enabled
} else {
clipObj.json.fx_enabled = true
if (Object.prototype.hasOwnProperty.call(clipObj.json, 'text')) {
delete clipObj.json.text
}
}
let payload = { data: clipObj }
this.editClip(payload)
hasClipEnhancements(clipObj) {
if (!clipObj || !clipObj.json) {
return false
}
const hasText = !!clipObj.json.text
const hasFxFlag = !!clipObj.json.fx_enabled
const hasAppliedEffects = Array.isArray(clipObj.json.effects) && clipObj.json.effects.length > 0
return hasText || hasFxFlag || hasAppliedEffects
},
handleClipEffectsSave(payload) {
this.pendingClipEffects = payload
},
updateTextInput(e, clipObj) {
clipObj.json.text = e.target.value
let payload = { data: clipObj }
this.editClip(payload)
},
isTextActive(clipObj) {
return Object.prototype.hasOwnProperty.call(clipObj.json, 'text')
},
isFxActive(clipObj) {
return !!clipObj.json.fx_enabled
},
onDragStart(event, clipObj, index) {
if (this.thumbnailedClips.length <= 1) {
event.preventDefault()
......@@ -609,6 +607,16 @@ export default {
color: #0d6efd;
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 {
color: #0d6efd;
}
......
......@@ -9,6 +9,18 @@
<div class="row preview-stage-row" v-if="hasPreviewFile || show_spinner">
<div class="col-12" ref="previewStageWrapper">
<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 class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
......@@ -89,11 +101,16 @@
</div>
</div>
<ClipEffectsModal
ref="clipEffectsModal"
@save="handleClipEffectsSave"
/>
</template>
<script>
import {mapState, mapActions, mapGetters, mapMutations} from "vuex";
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 AUDIO_FILE_EXTENSIONS = ['mp3', 'aac', 'm4a', 'wav', 'oga', 'ogg', 'flac', 'opus', 'wma', 'aif', 'aiff'];
......@@ -123,6 +140,9 @@ const MEDIA_MIME_TYPES = {
export default {
name: "Preview.vue",
props: ['project'],
components: {
ClipEffectsModal
},
data() {
return {
dragging_marker: null,
......@@ -148,7 +168,8 @@ export default {
stageResizeStartHeight: 0,
isResizingStage: false,
stageMinHeight: 220,
stageMaxHeight: 720
stageMaxHeight: 720,
pendingClipEffects: null
}
},
methods: {
......@@ -511,6 +532,28 @@ export default {
this.stageHeightManual = null
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']),
...mapMutations(['setPreview', 'setPreviewPosition', 'setPreviewFile'])
},
......@@ -536,6 +579,9 @@ export default {
return false
}
},
canOpenClipEffects() {
return this.hasPreviewFile
},
hasAnyFiles() {
return Array.isArray(this.files) && this.files.length > 0
},
......@@ -664,6 +710,30 @@ export default {
</script>
<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 {
margin-bottom: 1rem;
}
......
......@@ -14,6 +14,7 @@ export default createStore({
effects: [],
project: null,
current_export: { progress: 0.0 },
effectCatalog: [],
user: null,
preview: {
file: null,
......@@ -74,6 +75,9 @@ export default createStore({
effectObj
]
},
setEffectCatalog(state, effects) {
state.effectCatalog = effects || []
},
updateProjectThumbnail(state, payload) {
state.projects = [
...state.projects = state.projects.filter(p => p.id != payload.obj.id),
......@@ -517,6 +521,20 @@ export default createStore({
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) {
try {
const response = await instance.post('/effects/', payload)
......
......@@ -37,7 +37,7 @@ export default {
this.checkExportProgress(this.activeExports[0])
}
},
...mapActions(['getProject', 'loadExports', 'checkExportProgress', 'loadEffects', 'attachThumbnail']),
...mapActions(['getProject', 'loadExports', 'checkExportProgress', 'loadEffects', 'loadEffectCatalog', 'attachThumbnail']),
...mapMutations(['addError', 'setProject', 'setExports'])
},
computed: {
......@@ -55,6 +55,7 @@ export default {
let project_id = this.$route.params.id
this.loadExports(project_id)
this.loadEffects(project_id)
this.loadEffectCatalog()
let results = await this.getProject(project_id)
this.setProject(results.data)
this.hasData = true
......@@ -80,4 +81,4 @@ export default {
<style scoped>
</style>
\ No newline at end of file
</style>
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