macro-clips slice 4a: script frame dialog + reusable action editor
- ActionEditor.vue — reusable list editor for Action[]: add/remove/reorder, per-type parameter forms (jump to time/frame/marker, set button state/disabled, script source). Marker and button dropdowns pull from the store's current scene. - ScriptFrameDialog.vue — modal wrapping ActionEditor plus time and label inputs; commits on close, supports deletion. - Timeline wires the dialog: clicking a script frame opens it for edit, clicking the empty script lane creates a new frame at that time and opens the editor. - i18n keys for all action types, parameters, and dialog labels. Button-authoring UI (definitions + instance placement + properties panel) is its own slice (4b) — this commit ships script authoring only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
92d58937e3
commit
b9d1ca16f7
4 files changed
+635
No files matched your search
@@ -0,0 +1,178 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { PhTrash } from '@phosphor-icons/vue'
|
||||
import { useSceneStore } from '@/stores/scene'
|
||||
import { t } from '@/i18n'
|
||||
import { Modal } from '@/components/ui'
|
||||
import ActionEditor from './ActionEditor.vue'
|
||||
import type { Action } from '@/types/scene'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
frameId: string | null
|
||||
}>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
|
||||
const store = useSceneStore()
|
||||
|
||||
const frame = computed(() => {
|
||||
if (!props.frameId) return null
|
||||
return store.scene.scriptFrames?.find((f) => f.id === props.frameId) ?? null
|
||||
})
|
||||
|
||||
// Local working copy so edits don't commit until the user closes.
|
||||
const localTime = ref(0)
|
||||
const localActions = ref<Action[]>([])
|
||||
const localLabel = ref('')
|
||||
|
||||
watch(
|
||||
[() => props.modelValue, () => props.frameId],
|
||||
([open, fid]) => {
|
||||
if (!open || !fid) return
|
||||
const f = store.scene.scriptFrames?.find((x) => x.id === fid)
|
||||
if (!f) return
|
||||
localTime.value = f.time
|
||||
localActions.value = f.actions.map((a) => ({ ...a }))
|
||||
localLabel.value = f.label ?? ''
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function commit() {
|
||||
if (!props.frameId) return
|
||||
const f = frame.value
|
||||
if (!f) return
|
||||
if (Math.abs(f.time - localTime.value) > 1e-4) {
|
||||
store.setScriptFrameTime(f.id, localTime.value)
|
||||
}
|
||||
store.setScriptFrameActions(f.id, localActions.value)
|
||||
const nextLabel = localLabel.value.trim()
|
||||
if (nextLabel && nextLabel !== f.label) f.label = nextLabel
|
||||
else if (!nextLabel && f.label) delete f.label
|
||||
}
|
||||
|
||||
function close() {
|
||||
commit()
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
function removeFrame() {
|
||||
if (!props.frameId) return
|
||||
store.removeScriptFrame(props.frameId)
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:model-value="modelValue"
|
||||
size="md"
|
||||
:title="t('scriptFrameDialog.title')"
|
||||
@update:model-value="(v) => (v ? null : close())"
|
||||
>
|
||||
<div v-if="frame" class="body">
|
||||
<div class="row">
|
||||
<label>
|
||||
{{ t('scriptFrameDialog.time') }}
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
:value="localTime"
|
||||
@input="localTime = Number(($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</label>
|
||||
<label class="grow">
|
||||
{{ t('scriptFrameDialog.label') }}
|
||||
<input
|
||||
type="text"
|
||||
:value="localLabel"
|
||||
:placeholder="t('scriptFrameDialog.labelPlaceholder')"
|
||||
@input="localLabel = ($event.target as HTMLInputElement).value"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<ActionEditor
|
||||
:actions="localActions"
|
||||
@update:actions="localActions = $event"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button class="btn danger" @click="removeFrame">
|
||||
<PhTrash :size="12" />
|
||||
{{ t('scriptFrameDialog.remove') }}
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="btn primary" @click="close">{{ t('scriptFrameDialog.done') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.row label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.row label.grow {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.row input {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 4px 7px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 5px 10px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--panel-hover);
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn.danger:hover {
|
||||
background: var(--danger-soft);
|
||||
color: var(--danger);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user