Commit 57c793c5 by Jonathan Thomas

Removing single caption effect for entire project, and replacing with per-clip…

Removing single caption effect for entire project, and replacing with per-clip caption effects - which are easier to edit in openshot after the project is created. Also, fixing some issues with gaps left behind after rearranging/deleting clips.
parent ca7798bd
...@@ -186,6 +186,12 @@ ...@@ -186,6 +186,12 @@
<script> <script>
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import { mapState } from "vuex"; import { mapState } from "vuex";
import {
DEFAULT_CAPTION_POSITION,
getClipCaptionState,
normalizeCaptionPosition,
topValueToCaptionPosition
} from "../utils/captions";
export default { export default {
name: "ClipEffectsModal", name: "ClipEffectsModal",
...@@ -196,8 +202,11 @@ export default { ...@@ -196,8 +202,11 @@ export default {
clipContext: { clipContext: {
clipIndex: null, clipIndex: null,
clipName: "", clipName: "",
fileName: "" fileName: "",
clipId: null,
clipUrl: null
}, },
activeClip: null,
form: this.createEmptyForm(), form: this.createEmptyForm(),
snapshot: null, snapshot: null,
sampleEffectOptions: [ sampleEffectOptions: [
...@@ -231,8 +240,10 @@ export default { ...@@ -231,8 +240,10 @@ export default {
return label return label
}, },
normalizedEffectOptions() { normalizedEffectOptions() {
if (this.effectCatalog && this.effectCatalog.length) { if (Array.isArray(this.effectCatalog) && this.effectCatalog.length) {
return this.effectCatalog.map((effect, index) => this.normalizeEffectOption(effect, index)) return this.effectCatalog
.filter(effect => !this.isCaptionEffect(effect))
.map((effect, index) => this.normalizeEffectOption(effect, index))
} }
return this.sampleEffectOptions return this.sampleEffectOptions
} }
...@@ -241,7 +252,7 @@ export default { ...@@ -241,7 +252,7 @@ export default {
createEmptyForm() { createEmptyForm() {
return { return {
text: "", text: "",
position: "bottom", position: DEFAULT_CAPTION_POSITION,
audioLevel: 100, audioLevel: 100,
motion: { motion: {
slideIn: false, slideIn: false,
...@@ -277,25 +288,51 @@ export default { ...@@ -277,25 +288,51 @@ export default {
const normalized = this.normalizeEffectOption(effect, fallbackIndex) const normalized = this.normalizeEffectOption(effect, fallbackIndex)
return normalized return normalized
}, },
isCaptionEffect(effect) {
if (!effect) {
return false
}
if (typeof effect === 'string') {
return effect.toLowerCase() === 'caption'
}
const rawLabel = effect.title || effect.name || effect.display_name || effect.slug || effect.type || effect.class_name || ""
return typeof rawLabel === 'string' && rawLabel.toLowerCase().includes('caption')
},
open(payload = {}) { open(payload = {}) {
this.clipContext = { this.clipContext = {
clipIndex: payload.clipIndex ?? null, clipIndex: payload.clipIndex ?? null,
clipName: payload.clip?.name || payload.clipName || "", clipName: payload.clip?.name || payload.clipName || "",
fileName: payload.fileName || payload.clip?.name || "" fileName: payload.fileName || payload.clip?.name || "",
clipId: payload.clip?.id ?? payload.clipId ?? null,
clipUrl: payload.clip?.url ?? payload.clipUrl ?? null
} }
this.activeClip = payload.clip || null
const defaults = this.createEmptyForm() const defaults = this.createEmptyForm()
const clipCaptionState = getClipCaptionState(payload.clip)
const overridePosition = typeof payload.position === "string"
? payload.position
: (typeof payload.captionPosition === "string" ? payload.captionPosition : null)
const positionFromTop = typeof payload.captionTop === "number"
? topValueToCaptionPosition(payload.captionTop)
: clipCaptionState.position
const resolvedPosition = overridePosition
? normalizeCaptionPosition(overridePosition)
: positionFromTop
const appliedEffects = Array.isArray(payload.appliedEffects)
? payload.appliedEffects
.filter(effect => !this.isCaptionEffect(effect))
.map((effect, index) => this.normalizeAppliedEffect(effect, index))
: defaults.appliedEffects
this.form = { this.form = {
...defaults, ...defaults,
text: payload.text ?? payload.clip?.json?.text ?? defaults.text, text: payload.text ?? clipCaptionState.text ?? defaults.text,
position: payload.position || defaults.position, position: resolvedPosition || defaults.position,
audioLevel: typeof payload.audioLevel === "number" ? payload.audioLevel : defaults.audioLevel, audioLevel: typeof payload.audioLevel === "number" ? payload.audioLevel : defaults.audioLevel,
motion: { motion: {
...defaults.motion, ...defaults.motion,
...(payload.motion || {}) ...(payload.motion || {})
}, },
appliedEffects: Array.isArray(payload.appliedEffects) appliedEffects,
? payload.appliedEffects.map((effect, index) => this.normalizeAppliedEffect(effect, index))
: defaults.appliedEffects,
selectedEffectId: "" selectedEffectId: ""
} }
this.snapshot = JSON.parse(JSON.stringify(this.form)) this.snapshot = JSON.parse(JSON.stringify(this.form))
...@@ -310,12 +347,18 @@ export default { ...@@ -310,12 +347,18 @@ export default {
} }
}, },
handleReset() { handleReset() {
if (this.snapshot) {
this.form = JSON.parse(JSON.stringify(this.snapshot))
} else {
this.form = this.createEmptyForm() this.form = this.createEmptyForm()
const payload = {
clip: this.activeClip,
clipId: this.clipContext.clipId,
clipUrl: this.clipContext.clipUrl,
clipIndex: this.clipContext.clipIndex,
clipName: this.clipContext.clipName,
fileName: this.clipContext.fileName,
form: JSON.parse(JSON.stringify(this.form)),
hasCustomizations: false
} }
this.$emit('reset', { clip: this.clipContext }) this.$emit('reset', payload)
}, },
handleCancel() { handleCancel() {
if (this.snapshot) { if (this.snapshot) {
...@@ -326,8 +369,14 @@ export default { ...@@ -326,8 +369,14 @@ export default {
}, },
handleSave() { handleSave() {
const payload = { const payload = {
clip: this.clipContext, clip: this.activeClip,
form: JSON.parse(JSON.stringify(this.form)) clipId: this.clipContext.clipId,
clipUrl: this.clipContext.clipUrl,
clipIndex: this.clipContext.clipIndex,
clipName: this.clipContext.clipName,
fileName: this.clipContext.fileName,
form: JSON.parse(JSON.stringify(this.form)),
hasCustomizations: this.formHasCustomizations(this.form)
} }
this.$emit('save', payload) this.$emit('save', payload)
this.close() this.close()
...@@ -361,6 +410,30 @@ export default { ...@@ -361,6 +410,30 @@ export default {
this.$refs.textInput.focus() this.$refs.textInput.focus()
this.$refs.textInput.select() this.$refs.textInput.select()
} }
},
formHasCustomizations(form) {
const defaults = this.createEmptyForm()
if ((form?.text ?? "") !== defaults.text) {
return true
}
if (normalizeCaptionPosition(form?.position) !== defaults.position) {
return true
}
if (typeof form?.audioLevel === "number" && form.audioLevel !== defaults.audioLevel) {
return true
}
const motion = form?.motion || {}
const motionDefaults = defaults.motion
const motionKeys = Object.keys(motionDefaults)
for (const key of motionKeys) {
if (!!motion[key] !== !!motionDefaults[key]) {
return true
}
}
if (Array.isArray(form?.appliedEffects) && form.appliedEffects.length > 0) {
return true
}
return false
} }
}, },
mounted() { mounted() {
......
...@@ -67,11 +67,6 @@ ...@@ -67,11 +67,6 @@
</span> </span>
</div> </div>
<!-- Clip text input -->
<div v-if="clip.json.hasOwnProperty('text')" class="mb-3 clip-text">
<input @blur="updateTextInput($event, clip)" @mousedown.stop type="text" class="form-control" maxlength="35" placeholder="Enter text here" :value="clip.json.text">
</div>
<!-- Clip length label --> <!-- Clip length label -->
<div ref="clip" class="clip-label"> <div ref="clip" class="clip-label">
{{ (clip.end - clip.start).toFixed(1) }} Seconds {{ (clip.end - clip.start).toFixed(1) }} Seconds
...@@ -128,14 +123,15 @@ ...@@ -128,14 +123,15 @@
<ClipEffectsModal <ClipEffectsModal
ref="clipEffectsModal" ref="clipEffectsModal"
@save="handleClipEffectsSave" @save="handleClipEffectsSave"
@reset="handleClipEffectsReset"
/> />
</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 ClipEffectsModal from "./ClipEffectsModal" import ClipEffectsModal from "./ClipEffectsModal"
import { createClipWithCaptionForm, hasCaptionChanges } from "../utils/captions"
export default { export default {
name: "Clips.vue", name: "Clips.vue",
...@@ -153,8 +149,7 @@ export default { ...@@ -153,8 +149,7 @@ export default {
dragOverPosition: null, dragOverPosition: null,
autoScrollDirection: 0, autoScrollDirection: 0,
autoScrollFrame: null, autoScrollFrame: null,
dragListenersActive: false, dragListenersActive: false
pendingClipEffects: null
} }
}, },
methods: { methods: {
...@@ -167,7 +162,6 @@ export default { ...@@ -167,7 +162,6 @@ export default {
clipIndex: index + 1, clipIndex: index + 1,
clipName: clipObj?.name, clipName: clipObj?.name,
fileName, fileName,
text: clipObj?.json?.text || "",
appliedEffects appliedEffects
}) })
} }
...@@ -176,18 +170,40 @@ export default { ...@@ -176,18 +170,40 @@ export default {
if (!clipObj || !clipObj.json) { if (!clipObj || !clipObj.json) {
return false return false
} }
const hasText = !!clipObj.json.text
const hasFxFlag = !!clipObj.json.fx_enabled const hasFxFlag = !!clipObj.json.fx_enabled
const hasAppliedEffects = Array.isArray(clipObj.json.effects) && clipObj.json.effects.length > 0 const hasAppliedEffects = Array.isArray(clipObj.json.effects) && clipObj.json.effects.length > 0
return hasText || hasFxFlag || hasAppliedEffects return hasFxFlag || hasAppliedEffects
}, },
handleClipEffectsSave(payload) { async handleClipEffectsSave(payload) {
this.pendingClipEffects = payload const clipObj = payload.clip || this.clips.find(clip => clip.id === payload.clipId)
if (!clipObj || !payload.form) {
return
}
await this.persistClipEffects(clipObj, payload.form, !!payload.hasCustomizations)
},
async handleClipEffectsReset(payload) {
const clipObj = payload.clip || this.clips.find(clip => clip.id === payload.clipId)
if (!clipObj || !payload.form) {
return
}
await this.persistClipEffects(clipObj, payload.form, !!payload.hasCustomizations)
}, },
updateTextInput(e, clipObj) { async persistClipEffects(clipObj, form, fxEnabled) {
clipObj.json.text = e.target.value if (!clipObj || !form) {
let payload = { data: clipObj } return
this.editClip(payload) }
const currentFxEnabled = !!clipObj.json?.fx_enabled
const needsUpdate = hasCaptionChanges(clipObj, form) || currentFxEnabled !== fxEnabled
if (!needsUpdate) {
return
}
const updatedClip = createClipWithCaptionForm(clipObj, form, { fxEnabled })
if (!updatedClip) {
return
}
const editPayload = { data: updatedClip, latest: false, thumbnail: false }
await this.editClip(editPayload)
Object.assign(clipObj, updatedClip)
}, },
onDragStart(event, clipObj, index) { onDragStart(event, clipObj, index) {
if (this.thumbnailedClips.length <= 1) { if (this.thumbnailedClips.length <= 1) {
...@@ -408,54 +424,6 @@ export default { ...@@ -408,54 +424,6 @@ export default {
}, },
async startExport() { async startExport() {
this.exporting = true this.exporting = true
// Search for existing caption effect (we can re-use it)
let caption_results = this.effects.filter(e => e.layer == 999 && e.position == 0.0 && e.type == 'Caption')
let effect_payload = {}
let is_effect_new = false
// Get or create caption effect object
if (caption_results.length == 1) {
effect_payload = caption_results[0]
effect_payload.end = 0
effect_payload.json = caption
} else {
is_effect_new = true
effect_payload = {
position: 0,
start: 0,
end: 0,
layer: 999,
project: this.project.url,
title: 'Caption',
type: 'Caption',
json: caption
}
}
// Generate caption text (w/ timestamps)
caption.caption_text = ""
for (let clip of this.thumbnailedClips) {
if (clip.json.text) {
let clip_right_edge = clip.position + (clip.end - clip.start)
if (clip_right_edge > effect_payload.end) {
effect_payload.end = clip_right_edge
}
let start_timestamp = new Date(clip.position * 1000).toISOString().substr(11, 12)
let end_timestamp = new Date((clip.position + (clip.end - clip.start)) * 1000).toISOString().substr(11, 12)
caption.caption_text += `${start_timestamp} --> ${end_timestamp}\n${clip.json.text}\n\n`
}
}
// Save caption effect (if any text is needed)
if (effect_payload.end > 0) {
if (is_effect_new) {
await this.createEffect(effect_payload)
} else {
await this.updateEffect(effect_payload)
}
}
// Create export
let payload = { let payload = {
"export_type": "video", "export_type": "video",
"video_format": "mp4", "video_format": "mp4",
...@@ -504,12 +472,12 @@ export default { ...@@ -504,12 +472,12 @@ export default {
let myModal = new Modal(this.$refs.exportModal) let myModal = new Modal(this.$refs.exportModal)
myModal.show() myModal.show()
}, },
...mapActions(['loadClips', 'createClip', 'deleteClip', 'editClip', 'moveClip', 'createExport', 'createEffect', 'updateEffect']), ...mapActions(['loadClips', 'createClip', 'deleteClip', 'editClip', 'moveClip', 'createExport']),
...mapMutations((['setPreviewClip', 'setClips', 'setScrollToClip'])) ...mapMutations((['setPreviewClip', 'setClips', 'setScrollToClip']))
}, },
computed: { computed: {
...mapGetters(['totalClipDuration', 'thumbnailedClips']), ...mapGetters(['totalClipDuration', 'thumbnailedClips']),
...mapState(['clips', 'preview', 'scrollToClip', 'current_export', 'effects']), ...mapState(['clips', 'preview', 'scrollToClip', 'current_export']),
isDragging() { isDragging() {
return !!this.draggingClip return !!this.draggingClip
} }
...@@ -611,6 +579,7 @@ export default { ...@@ -611,6 +579,7 @@ export default {
padding: 4px; padding: 4px;
border-radius: 999px; border-radius: 999px;
background-color: rgba(0, 0, 0, 0.45); background-color: rgba(0, 0, 0, 0.45);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
transition: background-color 0.15s ease, color 0.15s ease; transition: background-color 0.15s ease, color 0.15s ease;
} }
.effects-trigger:hover { .effects-trigger:hover {
...@@ -684,21 +653,4 @@ export default { ...@@ -684,21 +653,4 @@ export default {
background-color: #000000; background-color: #000000;
width: 100%; width: 100%;
} }
.clip-text {
position: absolute;
width: 75%;
top: 0%;
padding-left: 5%;
padding-top: 10px;
}
input.form-control {
background-color: rgba(255, 255, 255, 0.20);
color: #fff;
border-color: #0d6efd;
filter: drop-shadow(0px 0px 1px #000000);
}
.form-control::placeholder {
filter: none;
color: #e3e3e3;
}
</style> </style>
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
v-if="canOpenClipEffects" v-if="canOpenClipEffects"
type="button" type="button"
class="preview-effects-button effects-trigger" class="preview-effects-button effects-trigger"
:class="{ active: hasPreviewEffectsChanges }"
title="Effects and Text Captions" title="Effects and Text Captions"
@click.stop="openPreviewEffects" @click.stop="openPreviewEffects"
> >
...@@ -104,6 +105,7 @@ ...@@ -104,6 +105,7 @@
<ClipEffectsModal <ClipEffectsModal
ref="clipEffectsModal" ref="clipEffectsModal"
@save="handleClipEffectsSave" @save="handleClipEffectsSave"
@reset="handleClipEffectsReset"
/> />
</template> </template>
...@@ -111,6 +113,7 @@ ...@@ -111,6 +113,7 @@
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"; import ClipEffectsModal from "./ClipEffectsModal";
import { createClipWithCaptionForm, hasCaptionChanges, syncCaptionEffectWithClip } from "../utils/captions";
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'];
...@@ -186,7 +189,14 @@ export default { ...@@ -186,7 +189,14 @@ export default {
"json": {} "json": {}
} }
let payload = {project_id: this.project.id, project_url: this.project.url, data} let payload = {project_id: this.project.id, project_url: this.project.url, data}
await this.createClip(payload) const newClip = await this.createClip(payload)
if (newClip && this.pendingClipEffects && this.pendingClipEffects.form) {
await this.persistClipEffects(newClip, this.pendingClipEffects.form, !!this.pendingClipEffects.hasCustomizations)
}
this.pendingClipEffects = null
if (newClip) {
await this.normalizeClipPositions()
}
} }
else else
{ {
...@@ -194,12 +204,10 @@ export default { ...@@ -194,12 +204,10 @@ export default {
let clipObj = this.preview.clip let clipObj = this.preview.clip
clipObj.start = this.preview.start * fixImageDuration(this.preview.file.json.duration) clipObj.start = this.preview.start * fixImageDuration(this.preview.file.json.duration)
clipObj.end = this.preview.end * fixImageDuration(this.preview.file.json.duration) clipObj.end = this.preview.end * fixImageDuration(this.preview.file.json.duration)
syncCaptionEffectWithClip(clipObj)
let payload = { data: clipObj, latest: false, thumbnail: true } let payload = { data: clipObj, latest: false, thumbnail: true }
await this.editClip(payload) await this.editClip(payload)
await this.normalizeClipPositions()
// Re-order clips (and fix 'position' to be valid again)
let move_payload = {direction: null, clip: clipObj, current: -1, dest: -1 }
await this.moveClip(move_payload)
} }
this.setPreviewFile(null) this.setPreviewFile(null)
}, },
...@@ -536,13 +544,21 @@ export default { ...@@ -536,13 +544,21 @@ export default {
if (!this.$refs.clipEffectsModal || !this.preview.file) { if (!this.$refs.clipEffectsModal || !this.preview.file) {
return return
} }
const clipIndex = Array.isArray(this.clips) && this.preview.clip
? this.clips.findIndex(clip => clip.id === this.preview.clip.id)
: -1
const appliedEffects = Array.isArray(this.preview.clip?.json?.effects) ? this.preview.clip.json.effects : [] const appliedEffects = Array.isArray(this.preview.clip?.json?.effects) ? this.preview.clip.json.effects : []
const pendingForm = !this.preview.clip && this.pendingClipEffects ? this.pendingClipEffects.form : null
this.$refs.clipEffectsModal.open({ this.$refs.clipEffectsModal.open({
clip: this.preview.clip || null, clip: this.preview.clip || null,
clipIndex: clipIndex >= 0 ? clipIndex + 1 : null,
clipName: this.preview.clip?.name || this.getPreviewFileName(), clipName: this.preview.clip?.name || this.getPreviewFileName(),
fileName: this.getPreviewFileName(), fileName: this.getPreviewFileName(),
text: this.preview.clip?.json?.text || "", text: pendingForm?.text,
appliedEffects position: pendingForm?.position,
audioLevel: pendingForm?.audioLevel,
motion: pendingForm?.motion,
appliedEffects: this.preview.clip ? appliedEffects : (pendingForm?.appliedEffects || [])
}) })
}, },
getPreviewFileName() { getPreviewFileName() {
...@@ -551,11 +567,59 @@ export default { ...@@ -551,11 +567,59 @@ export default {
} }
return this.preview.file.name || this.preview.file.title || this.preview.file.filename || this.preview.file.media?.split('/').pop() || '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) { async handleClipEffectsSave(payload) {
this.pendingClipEffects = payload if (!payload.form) {
return
}
const clipObj = payload.clip || this.clips.find(clip => clip.id === payload.clipId)
if (!clipObj) {
this.pendingClipEffects = {
form: JSON.parse(JSON.stringify(payload.form)),
hasCustomizations: !!payload.hasCustomizations
}
return
}
this.pendingClipEffects = null
const updatedClip = await this.persistClipEffects(clipObj, payload.form, !!payload.hasCustomizations)
if (updatedClip && this.preview.clip && this.preview.clip.id === updatedClip.id) {
this.setPreviewClip(updatedClip)
}
}, },
...mapActions(['createClip', 'editClip', 'moveClip']), async handleClipEffectsReset(payload) {
...mapMutations(['setPreview', 'setPreviewPosition', 'setPreviewFile']) if (!payload.form) {
return
}
const clipObj = payload.clip || this.clips.find(clip => clip.id === payload.clipId)
if (!clipObj) {
this.pendingClipEffects = null
return
}
const updatedClip = await this.persistClipEffects(clipObj, payload.form, !!payload.hasCustomizations)
if (updatedClip && this.preview.clip && this.preview.clip.id === updatedClip.id) {
this.setPreviewClip(updatedClip)
}
},
async persistClipEffects(clipObj, form, fxEnabled) {
if (!clipObj || !form) {
return null
}
const currentFxEnabled = !!clipObj.json?.fx_enabled
const needsUpdate = hasCaptionChanges(clipObj, form) || currentFxEnabled !== fxEnabled
if (!needsUpdate) {
clipObj.json.fx_enabled = fxEnabled
return clipObj
}
const updatedClip = createClipWithCaptionForm(clipObj, form, { fxEnabled })
if (!updatedClip) {
return null
}
const editPayload = { data: updatedClip, latest: false, thumbnail: false }
await this.editClip(editPayload)
Object.assign(clipObj, updatedClip)
return clipObj
},
...mapActions(['createClip', 'editClip', 'normalizeClipPositions']),
...mapMutations(['setPreview', 'setPreviewPosition', 'setPreviewFile', 'setPreviewClip'])
}, },
computed: { computed: {
...mapState(['preview', 'files', 'clips']), ...mapState(['preview', 'files', 'clips']),
...@@ -582,6 +646,12 @@ export default { ...@@ -582,6 +646,12 @@ export default {
canOpenClipEffects() { canOpenClipEffects() {
return this.hasPreviewFile return this.hasPreviewFile
}, },
hasPreviewEffectsChanges() {
if (this.preview.clip) {
return !!this.preview.clip.json?.fx_enabled
}
return !!(this.pendingClipEffects && this.pendingClipEffects.hasCustomizations)
},
hasAnyFiles() { hasAnyFiles() {
return Array.isArray(this.files) && this.files.length > 0 return Array.isArray(this.files) && this.files.length > 0
}, },
...@@ -718,12 +788,17 @@ export default { ...@@ -718,12 +788,17 @@ export default {
padding: 6px; padding: 6px;
border-radius: 999px; border-radius: 999px;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
color: #ffffff; color: #ffffff;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: background-color 0.15s ease, color 0.15s ease; transition: background-color 0.15s ease, color 0.15s ease;
} }
.preview-effects-button.active {
color: #0d6efd;
background-color: rgba(13, 110, 253, 0.2);
}
.preview-effects-button:hover { .preview-effects-button:hover {
color: #0d6efd; color: #0d6efd;
background-color: rgba(13, 110, 253, 0.2); background-color: rgba(13, 110, 253, 0.2);
......
...@@ -330,9 +330,11 @@ export default createStore({ ...@@ -330,9 +330,11 @@ export default createStore({
let fps = clipObj.json.reader.fps.num / clipObj.json.reader.fps.den let fps = clipObj.json.reader.fps.num / clipObj.json.reader.fps.den
let thumbnail_payload = { obj: clipObj, frame: clipObj.start * fps, latest: true } let thumbnail_payload = { obj: clipObj, frame: clipObj.start * fps, latest: true }
dispatch('attachThumbnail', thumbnail_payload) await dispatch('attachThumbnail', thumbnail_payload)
return clipObj
} catch(err) { } catch(err) {
commit('addError', err.response.data) commit('addError', err.response.data)
return null
} }
}, },
async editClip({dispatch, commit}, payload) { async editClip({dispatch, commit}, payload) {
...@@ -375,13 +377,15 @@ export default createStore({ ...@@ -375,13 +377,15 @@ export default createStore({
pos += (clip.end - clip.start) pos += (clip.end - clip.start)
} }
for (let edit_payload of updates) { for (let edit_payload of updates) {
dispatch('editClip', edit_payload) await dispatch('editClip', edit_payload)
} }
await dispatch('normalizeClipPositions')
}, },
async deleteClip({commit}, clip_id) { async deleteClip({commit, dispatch}, clip_id) {
try { try {
await instance.delete(`clips/${clip_id}/`) await instance.delete(`clips/${clip_id}/`)
commit('deleteClip', clip_id) commit('deleteClip', clip_id)
await dispatch('normalizeClipPositions')
} catch(err) { } catch(err) {
commit('addError', err.response.data) commit('addError', err.response.data)
} }
...@@ -552,6 +556,25 @@ export default createStore({ ...@@ -552,6 +556,25 @@ export default createStore({
commit('addError', err.response.data) commit('addError', err.response.data)
} }
}, },
async normalizeClipPositions({getters, dispatch}) {
const clips = [...getters.thumbnailedClips]
if (!Array.isArray(clips) || clips.length === 0) {
return
}
let position = 0
const updates = []
for (let clip of clips) {
const duration = Math.max(0, (clip.end - clip.start))
if (Math.abs((clip.position ?? 0) - position) > 0.0001) {
clip.position = position
updates.push({ data: clip, latest: false, thumbnail: false })
}
position += duration
}
for (let payload of updates) {
await dispatch('editClip', payload)
}
},
}, },
modules: { modules: {
} }
......
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