diff --git a/src/components/ActionEditor.vue b/src/components/ActionEditor.vue new file mode 100644 index 0000000..1c14a1e --- /dev/null +++ b/src/components/ActionEditor.vue @@ -0,0 +1,392 @@ + + + + + {{ t('actions.empty') }} + + + + {{ i + 1 }} + + + {{ actionTypeLabel(type) }} + + + + + + + + + + + + + + + {{ t('actions.param.time') }} + + + + + + {{ t('actions.param.frame') }} + + + + + + {{ t('actions.param.marker') }} + + {{ t('actions.noMarkers') }} + {{ name }} + + + + + + {{ t('actions.param.button') }} + + {{ t('actions.noButtons') }} + {{ b.label }} + + + + {{ t('actions.param.state') }} + + {{ s }} + + + + + + {{ t('actions.param.button') }} + + {{ t('actions.noButtons') }} + {{ b.label }} + + + + + {{ t('actions.param.disabled') }} + + + + + {{ t('actions.param.source') }} + + + + + + + + {{ t('actions.add') }} + + + + + diff --git a/src/components/ScriptFrameDialog.vue b/src/components/ScriptFrameDialog.vue new file mode 100644 index 0000000..354e2ba --- /dev/null +++ b/src/components/ScriptFrameDialog.vue @@ -0,0 +1,178 @@ + + + + (v ? null : close())" + > + + + + {{ t('scriptFrameDialog.time') }} + + + + {{ t('scriptFrameDialog.label') }} + + + + + + + + + {{ t('scriptFrameDialog.remove') }} + + + {{ t('scriptFrameDialog.done') }} + + + + + diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 6fcb262..43f5f7c 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -30,6 +30,7 @@ import { useSceneStore } from '@/stores/scene' import { sampleLayer } from '@/lib/interpolate' import { snapToCandidates } from '@/lib/timelineSnap' import { confirmDialog, promptDialog } from '@/components/ui' +import ScriptFrameDialog from './ScriptFrameDialog.vue' import { t } from '@/i18n' import { kfSelectionKey, useKfSelection, type KfSelectionKey } from '@/composables/useKfSelection' import { showLocked } from '@/composables/useShowLocked' @@ -1478,6 +1479,27 @@ function clipBlockStyle(ci: ClipInstance) { } const BUTTON_STATES = ['normal', 'hover', 'pressed', 'disabled'] as const + +// Script frame dialog state +const scriptDialogOpen = ref(false) +const scriptDialogFrameId = ref(null) + +function openScriptFrame(frameId: string) { + scriptDialogFrameId.value = frameId + scriptDialogOpen.value = true +} + +function onScriptLaneClick(e: MouseEvent, trackId: string) { + // Only handle clicks on the bare lane (not on an existing frame). + if ((e.target as HTMLElement).closest('.script-frame')) return + const el = e.currentTarget as HTMLElement + const rect = el.getBoundingClientRect() + if (rect.width <= 0) return + const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)) + const time = snapTime(ratio * duration.value) + const frame = store.addScriptFrame(trackId, time) + if (frame) openScriptFrame(frame.id) +} const BUTTON_CELL_SEC = 0.5 // visual width of each of the 4 state cells function buttonInstanceStyle(bi: ButtonInstance) { // Four cells, each with a fixed per-cell visual width. Kept as a fixed @@ -2311,6 +2333,7 @@ onBeforeUnmount(() => { { class="script-frame" :style="{ left: pxFromTime(frame.time) + 'px' }" :title="frame.label ?? t('timeline.scriptFrameTooltip', { n: frame.actions.length })" + @click.stop="openScriptFrame(frame.id)" > {{ frame.actions.length }} @@ -2550,6 +2574,10 @@ onBeforeUnmount(() => { }} + diff --git a/src/i18n/messages.ts b/src/i18n/messages.ts index dad6f9e..5709613 100644 --- a/src/i18n/messages.ts +++ b/src/i18n/messages.ts @@ -359,6 +359,43 @@ export const en = { panCanvas: 'Pan canvas', }, }, + actions: { + empty: 'No actions yet. Add one below.', + add: 'Add action', + remove: 'Remove', + moveUp: 'Move up', + moveDown: 'Move down', + noMarkers: 'No markers defined', + noButtons: 'No labeled button instances', + type: { + play: 'Play', + pause: 'Pause', + rewindToLoop: 'Rewind to loop start', + jumpTo: 'Jump to time', + jumpToFrame: 'Jump to frame', + jumpToMarker: 'Jump to marker', + setButtonState: 'Set button state', + setButtonDisabled: 'Set button disabled', + script: 'Run script', + }, + param: { + time: 'Seconds', + frame: 'Frame', + marker: 'Marker', + button: 'Button', + state: 'State', + disabled: 'Disabled', + source: 'Script source', + }, + }, + scriptFrameDialog: { + title: 'Script frame', + time: 'Time (s)', + label: 'Label (optional)', + labelPlaceholder: 'e.g. intro-end', + remove: 'Delete frame', + done: 'Done', + }, } as const export type Messages = typeof en