0.18.1: editor-hide cascades to groups + rename for layers & clip tracks
Track groups get the same editor-hide affordance as layer and clip tracks: new eye button on the group header, setGroupEditorHidden action (clears selection pointing inside the group), and a cascade through the Konva stackItems + render.ts so every child track in a hidden group vanishes in the editor. Track renaming via right-click context menu now covers layers and clip tracks too — setLayerName / setClipTrackName actions, new i18n strings, and end-to-end UI tests driving the menu → promptDialog → rename flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6784b76607
commit
5af0cdb3d4
9 files changed
+382
-2
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ruwio",
|
||||
"private": true,
|
||||
"version": "0.18.0",
|
||||
"version": "0.18.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -821,11 +821,20 @@ const stackItems = computed<StackItem[]>(() => {
|
||||
instancesByTrack.set(ci.clipTrackId, arr)
|
||||
}
|
||||
|
||||
// Tracks whose parent TrackGroup is editor-hidden — same cascade
|
||||
// as the Timeline's lockedGroupChildIds logic, just for the eye
|
||||
// toggle. A child with its own editorHidden flag unset still
|
||||
// disappears if its group carries one.
|
||||
const hiddenGroupChildIds = new Set<string>()
|
||||
for (const g of store.scene.groups ?? []) {
|
||||
if (g.editorHidden) for (const cid of g.childIds) hiddenGroupChildIds.add(cid)
|
||||
}
|
||||
const expanded = expandedTrackOrder(store.scene)
|
||||
const order = expanded.length ? expanded : store.scene.layers.map((l) => l.id)
|
||||
// iterate bottom → top: reverse the array
|
||||
for (let i = order.length - 1; i >= 0; i--) {
|
||||
const id = order[i]
|
||||
if (hiddenGroupChildIds.has(id)) continue
|
||||
const layer = layerById.get(id)
|
||||
if (layer) {
|
||||
// Editor-only hide: skip on the Konva stage while the scene
|
||||
|
||||
@@ -89,6 +89,7 @@ interface TimelineGroupTrack {
|
||||
collapsed: boolean
|
||||
color: string | null
|
||||
locked: boolean
|
||||
editorHidden: boolean
|
||||
orderIndex: number
|
||||
childCount: number
|
||||
}
|
||||
@@ -808,6 +809,7 @@ const tracks = computed<TimelineTrack[]>(() => {
|
||||
collapsed: !!grp.collapsed,
|
||||
color: grp.color ?? null,
|
||||
locked: !!grp.locked,
|
||||
editorHidden: !!grp.editorHidden,
|
||||
orderIndex,
|
||||
childCount: grp.childIds.length,
|
||||
})
|
||||
@@ -2741,6 +2743,36 @@ function duplicateTrackFromMenu() {
|
||||
|
||||
async function renameFromMenu() {
|
||||
if (!trackMenu.value) return
|
||||
if (trackMenu.value.kind === 'layer') {
|
||||
const layer = store.activeLayers.find((l) => l.id === trackMenu.value!.trackId)
|
||||
if (!layer) return
|
||||
const id = trackMenu.value.trackId
|
||||
const current = layer.name
|
||||
trackMenu.value = null
|
||||
const next = await promptDialog({
|
||||
title: t('timeline.renameLayerDialog.title'),
|
||||
defaultValue: current,
|
||||
confirmLabel: t('timeline.renameLayerDialog.confirm'),
|
||||
})
|
||||
if (next === null) return
|
||||
store.setLayerName(id, next)
|
||||
return
|
||||
}
|
||||
if (trackMenu.value.kind === 'clip') {
|
||||
const track = store.scene.clipTracks?.find((t) => t.id === trackMenu.value!.trackId)
|
||||
if (!track) return
|
||||
const id = trackMenu.value.trackId
|
||||
const current = track.name ?? t('timeline.clipTrackName')
|
||||
trackMenu.value = null
|
||||
const next = await promptDialog({
|
||||
title: t('timeline.renameClipTrackDialog.title'),
|
||||
defaultValue: current,
|
||||
confirmLabel: t('timeline.renameClipTrackDialog.confirm'),
|
||||
})
|
||||
if (next === null) return
|
||||
store.setClipTrackName(id, next)
|
||||
return
|
||||
}
|
||||
if (trackMenu.value.kind === 'group') {
|
||||
const group = groupById.value.get(trackMenu.value.trackId)
|
||||
if (!group) return
|
||||
@@ -3778,6 +3810,16 @@ onBeforeUnmount(() => {
|
||||
<PhFolder :size="12" class="name-badge" />
|
||||
<span class="track-label">{{ track.name }}</span>
|
||||
<span class="group-count">{{ t('timeline.groupChildCount', { n: track.childCount }) }}</span>
|
||||
<button
|
||||
class="eye-btn"
|
||||
:class="{ hidden: !!track.editorHidden }"
|
||||
v-tooltip="track.editorHidden ? t('timeline.showInEditor') : t('timeline.hideInEditor')"
|
||||
@click.stop="store.setGroupEditorHidden(track.groupId, !track.editorHidden)"
|
||||
@mousedown.stop
|
||||
@dragstart.prevent.stop
|
||||
>
|
||||
<component :is="track.editorHidden ? PhEyeSlash : PhEye" :size="12" />
|
||||
</button>
|
||||
<button
|
||||
class="lock-btn"
|
||||
:class="{ locked: track.locked }"
|
||||
@@ -3973,6 +4015,13 @@ onBeforeUnmount(() => {
|
||||
{{ t('timeline.renameScriptTrack') }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="trackMenu.kind === 'clip'">
|
||||
<div class="kf-menu-divider"></div>
|
||||
<button class="kf-menu-item" @click="renameFromMenu">
|
||||
<PhPencilSimpleLine :size="12" class="kf-menu-icon" />
|
||||
{{ t('timeline.renameClipTrack') }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="trackMenu.kind === 'audio'">
|
||||
<div class="kf-menu-divider"></div>
|
||||
<button class="kf-menu-item" @click="renameFromMenu">
|
||||
@@ -3993,6 +4042,10 @@ onBeforeUnmount(() => {
|
||||
<template v-else>
|
||||
<template v-if="trackMenu.kind === 'layer'">
|
||||
<div class="kf-menu-divider"></div>
|
||||
<button class="kf-menu-item" @click="renameFromMenu">
|
||||
<PhPencilSimpleLine :size="12" class="kf-menu-icon" />
|
||||
{{ t('timeline.renameLayer') }}
|
||||
</button>
|
||||
<button
|
||||
class="kf-menu-item"
|
||||
:disabled="trackMenu.locked"
|
||||
|
||||
@@ -6159,6 +6159,56 @@ export function registerAllTests(): void {
|
||||
})
|
||||
})
|
||||
|
||||
suite('scene store — track group editor hide', () => {
|
||||
test('setGroupEditorHidden toggles the flag on + off', () => {
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'R', width: 10, height: 10, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)!
|
||||
const g = s.addTrackGroup(undefined, l.id)
|
||||
expect(g.editorHidden).toBe(undefined)
|
||||
s.setGroupEditorHidden(g.id, true)
|
||||
expect(g.editorHidden).toBe(true)
|
||||
s.setGroupEditorHidden(g.id, false)
|
||||
expect('editorHidden' in g).toBe(false)
|
||||
})
|
||||
|
||||
test('hiding a group clears selection of a child layer', () => {
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'R', width: 10, height: 10, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)!
|
||||
s.selectLayer(l.id)
|
||||
const groupId = s.groupSelectedLayers('G1')!
|
||||
expect(groupId).toBeTruthy()
|
||||
// groupSelectedLayers clears selection after consuming it, so
|
||||
// re-select the layer now that it sits inside the group.
|
||||
s.selectLayer(l.id)
|
||||
expect(s.selectedLayerId).toBe(l.id)
|
||||
s.setGroupEditorHidden(groupId, true)
|
||||
expect(s.selectedLayerId).toBe(null)
|
||||
expect(s.selectedLayerIds).toEqual([])
|
||||
})
|
||||
|
||||
test('hiding a group clears clip-instance selection when the instance track is inside', () => {
|
||||
const s = makeFreshStore()
|
||||
const clip = s.addClip('CInst')
|
||||
clip.duration = 2
|
||||
const track = s.scene.clipTracks![0]
|
||||
const ci = s.addClipInstance(clip.id, 0, track.id)!
|
||||
// Wrap the clip track into a fresh group by poking childIds
|
||||
// directly — there's no public addClipTrackToGroup action, the
|
||||
// real app nests tracks through drag-drop.
|
||||
const g = s.addTrackGroup('G2', track.id)
|
||||
g.childIds.push(track.id)
|
||||
const order = s.scene.trackOrder!
|
||||
const ti = order.indexOf(track.id)
|
||||
if (ti >= 0) order.splice(ti, 1)
|
||||
s.selectClipInstance(ci.id)
|
||||
expect(s.selectedClipInstanceId).toBe(ci.id)
|
||||
s.setGroupEditorHidden(g.id, true)
|
||||
expect(s.selectedClipInstanceId).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
suite('ui — Timeline clip-track eye button', () => {
|
||||
test('clicking the eye toggles editorHidden on the clip track', async () => {
|
||||
const s = makeFreshStore()
|
||||
@@ -6183,6 +6233,178 @@ export function registerAllTests(): void {
|
||||
})
|
||||
})
|
||||
|
||||
suite('scene store — track rename (layer + clip track)', () => {
|
||||
test('setLayerName applies a trimmed non-empty name', () => {
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'R', width: 10, height: 10, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)!
|
||||
s.setLayerName(l.id, ' Hero ')
|
||||
expect(l.name).toBe('Hero')
|
||||
})
|
||||
|
||||
test('setLayerName ignores empty / whitespace-only input', () => {
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'R', width: 10, height: 10, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)!
|
||||
const original = l.name
|
||||
s.setLayerName(l.id, '')
|
||||
expect(l.name).toBe(original)
|
||||
s.setLayerName(l.id, ' ')
|
||||
expect(l.name).toBe(original)
|
||||
})
|
||||
|
||||
test('setClipTrackName sets + clears the optional name field', () => {
|
||||
const s = makeFreshStore()
|
||||
s.addClip('CInst')
|
||||
const track = s.scene.clipTracks![0]
|
||||
s.setClipTrackName(track.id, 'Intro clips')
|
||||
expect(track.name).toBe('Intro clips')
|
||||
// Empty clears back to undefined — default label is shown via i18n.
|
||||
s.setClipTrackName(track.id, '')
|
||||
expect(track.name).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
suite('ui — Timeline rename menu (layer + clip track)', () => {
|
||||
// Finds the menu item whose label contains the given i18n string.
|
||||
// Menu items share `.kf-menu-item`, so labels are the only stable
|
||||
// handle that distinguishes rename from lock / delete / etc.
|
||||
function findMenuItem(menu: HTMLElement, label: string): HTMLButtonElement | null {
|
||||
const items = Array.from(menu.querySelectorAll('.kf-menu-item')) as HTMLButtonElement[]
|
||||
return items.find((b) => b.textContent?.includes(label)) ?? null
|
||||
}
|
||||
|
||||
test('right-click on a layer row → Rename layer → prompt → setLayerName', async () => {
|
||||
const { dialogState, resolveDialog } = await import(
|
||||
'@/components/ui/dialogs'
|
||||
)
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'Rect', width: 10, height: 10, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)!
|
||||
const handle = mountComponent(Timeline, {})
|
||||
try {
|
||||
await nextTick()
|
||||
const row = handle.host.querySelector('.track .track-name') as HTMLElement
|
||||
expect(row).toBeTruthy()
|
||||
row.dispatchEvent(
|
||||
new MouseEvent('contextmenu', {
|
||||
bubbles: true, cancelable: true, clientX: 10, clientY: 10,
|
||||
}),
|
||||
)
|
||||
await nextTick()
|
||||
const menu = handle.host.querySelector('.kf-menu') as HTMLElement
|
||||
expect(menu).toBeTruthy()
|
||||
const renameBtn = findMenuItem(menu, i18nT('timeline.renameLayer'))
|
||||
expect(renameBtn).toBeTruthy()
|
||||
// renameFromMenu is async but runs synchronously up to the
|
||||
// `await promptDialog(...)`; promptDialog's new Promise cb
|
||||
// runs in the same tick and writes dialogState.value before
|
||||
// returning. So the prompt state is observable immediately
|
||||
// after the click.
|
||||
renameBtn!.click()
|
||||
expect(dialogState.value?.kind).toBe('prompt')
|
||||
resolveDialog('Hero-from-menu')
|
||||
// The awaited promptDialog now resolves on a microtask — tick
|
||||
// so the continuation (store.setLayerName) runs.
|
||||
await nextTick()
|
||||
expect(l.name).toBe('Hero-from-menu')
|
||||
} finally {
|
||||
handle.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
test('Rename layer prompt cancelled → layer name stays the same', async () => {
|
||||
// Dismissing the prompt must not clobber the existing name.
|
||||
const { dialogState, cancelDialog } = await import(
|
||||
'@/components/ui/dialogs'
|
||||
)
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'Rect', width: 10, height: 10, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)!
|
||||
const original = l.name
|
||||
const handle = mountComponent(Timeline, {})
|
||||
try {
|
||||
await nextTick()
|
||||
const row = handle.host.querySelector('.track .track-name') as HTMLElement
|
||||
row.dispatchEvent(
|
||||
new MouseEvent('contextmenu', {
|
||||
bubbles: true, cancelable: true, clientX: 10, clientY: 10,
|
||||
}),
|
||||
)
|
||||
await nextTick()
|
||||
const menu = handle.host.querySelector('.kf-menu') as HTMLElement
|
||||
findMenuItem(menu, i18nT('timeline.renameLayer'))!.click()
|
||||
expect(dialogState.value?.kind).toBe('prompt')
|
||||
cancelDialog()
|
||||
await nextTick()
|
||||
expect(l.name).toBe(original)
|
||||
} finally {
|
||||
handle.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
test('right-click on a clip-track row → Rename clip track → prompt → setClipTrackName', async () => {
|
||||
const { dialogState, resolveDialog } = await import(
|
||||
'@/components/ui/dialogs'
|
||||
)
|
||||
const s = makeFreshStore()
|
||||
s.addClip('CInst')
|
||||
const track = s.scene.clipTracks![0]
|
||||
const handle = mountComponent(Timeline, {})
|
||||
try {
|
||||
await nextTick()
|
||||
const row = handle.host.querySelector(
|
||||
'.track.clip-track .track-name',
|
||||
) as HTMLElement
|
||||
expect(row).toBeTruthy()
|
||||
row.dispatchEvent(
|
||||
new MouseEvent('contextmenu', {
|
||||
bubbles: true, cancelable: true, clientX: 10, clientY: 10,
|
||||
}),
|
||||
)
|
||||
await nextTick()
|
||||
const menu = handle.host.querySelector('.kf-menu') as HTMLElement
|
||||
expect(menu).toBeTruthy()
|
||||
const renameBtn = findMenuItem(menu, i18nT('timeline.renameClipTrack'))
|
||||
expect(renameBtn).toBeTruthy()
|
||||
renameBtn!.click()
|
||||
expect(dialogState.value?.kind).toBe('prompt')
|
||||
resolveDialog('Intro clips')
|
||||
await nextTick()
|
||||
expect(track.name).toBe('Intro clips')
|
||||
} finally {
|
||||
handle.unmount()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
suite('ui — Timeline group eye button', () => {
|
||||
test('clicking the eye toggles editorHidden on the track group', async () => {
|
||||
const s = makeFreshStore()
|
||||
const a = s.addAsset({ kind: 'rect', name: 'R', width: 10, height: 10, fill: '#fff' })
|
||||
const l = s.addLayer(a.id)!
|
||||
s.selectLayer(l.id)
|
||||
const groupId = s.groupSelectedLayers('G1')!
|
||||
const group = s.scene.groups!.find((g) => g.id === groupId)!
|
||||
const handle = mountComponent(Timeline, {})
|
||||
await nextTick()
|
||||
const groupTrack = handle.host.querySelector('.track.group-track') as HTMLElement
|
||||
expect(groupTrack).toBeTruthy()
|
||||
const eye = groupTrack.querySelector('.track-name .eye-btn') as HTMLButtonElement
|
||||
expect(eye).toBeTruthy()
|
||||
expect(eye.classList.contains('hidden')).toBe(false)
|
||||
eye.click()
|
||||
await nextTick()
|
||||
expect(group.editorHidden).toBe(true)
|
||||
const eye2 = groupTrack.querySelector('.track-name .eye-btn') as HTMLButtonElement
|
||||
expect(eye2.classList.contains('hidden')).toBe(true)
|
||||
eye2.click()
|
||||
await nextTick()
|
||||
expect('editorHidden' in group).toBe(false)
|
||||
handle.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
suite('ui — Timeline audio-clip context menu', () => {
|
||||
test('right-click on an audio-clip-block opens a menu with Duplicate + Delete', async () => {
|
||||
const s = makeFreshStore()
|
||||
@@ -7345,6 +7567,32 @@ export function registerAllTests(): void {
|
||||
}
|
||||
})
|
||||
|
||||
test('editorHidden group cascades — children vanish from the Konva stack', async () => {
|
||||
// Regression guard for the cascade: even if the child track's
|
||||
// own flag is unset, hiding the parent group must drop its
|
||||
// layers and clip instances from the editor.
|
||||
const s = makeFreshStore()
|
||||
s.setDuration(10)
|
||||
const rect = s.addAsset({
|
||||
kind: 'rect', name: 'R', width: 40, height: 40, fill: '#f00',
|
||||
})
|
||||
const layer = s.addLayer(rect.id)!
|
||||
s.selectLayer(layer.id)
|
||||
const groupId = s.groupSelectedLayers('G')!
|
||||
expect(groupId).toBeTruthy()
|
||||
s.setGroupEditorHidden(groupId, true)
|
||||
const handle = await mountCanvasWithKonva()
|
||||
try {
|
||||
await nextTick()
|
||||
await tick(40)
|
||||
const stage = await getKonvaStage()
|
||||
// The child layer's Konva node carries the layer id directly.
|
||||
expect(stage.findOne(`#${layer.id}`)).toBeFalsy()
|
||||
} finally {
|
||||
handle.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
test('editorHidden clip track drops its instances from the Konva stack', async () => {
|
||||
// Matches the layer editor-hide semantics: the track stays in
|
||||
// the scene file (so exports still see it) but no ci-group or
|
||||
@@ -10353,6 +10601,18 @@ export function registerAllTests(): void {
|
||||
expect(confirm === 'timeline.renameScriptTrackDialog.confirm').toBe(false)
|
||||
})
|
||||
|
||||
test('renameLayer + renameClipTrack are translated', () => {
|
||||
expect(i18nT('timeline.renameLayer') === 'timeline.renameLayer').toBe(false)
|
||||
expect(i18nT('timeline.renameClipTrack') === 'timeline.renameClipTrack').toBe(false)
|
||||
})
|
||||
|
||||
test('renameLayerDialog / renameClipTrackDialog titles + confirms are translated', () => {
|
||||
expect(i18nT('timeline.renameLayerDialog.title') === 'timeline.renameLayerDialog.title').toBe(false)
|
||||
expect(i18nT('timeline.renameLayerDialog.confirm') === 'timeline.renameLayerDialog.confirm').toBe(false)
|
||||
expect(i18nT('timeline.renameClipTrackDialog.title') === 'timeline.renameClipTrackDialog.title').toBe(false)
|
||||
expect(i18nT('timeline.renameClipTrackDialog.confirm') === 'timeline.renameClipTrackDialog.confirm').toBe(false)
|
||||
})
|
||||
|
||||
test('sidebar noClips / noButtons empty-states both suggest "yet"', () => {
|
||||
// Regression: the clip library used to read "No clips" while the
|
||||
// button library read "No buttons yet". Minor inconsistency that
|
||||
|
||||
@@ -237,6 +237,14 @@ export const en = {
|
||||
title: 'Rename audio track',
|
||||
confirm: 'Apply',
|
||||
},
|
||||
renameLayerDialog: {
|
||||
title: 'Rename layer',
|
||||
confirm: 'Apply',
|
||||
},
|
||||
renameClipTrackDialog: {
|
||||
title: 'Rename clip track',
|
||||
confirm: 'Apply',
|
||||
},
|
||||
removeAudioClipDialog: {
|
||||
title: 'Remove audio clip?',
|
||||
message: 'Remove this audio clip from the timeline.',
|
||||
@@ -248,6 +256,8 @@ export const en = {
|
||||
audioMuted: 'Muted — click to unmute',
|
||||
muteAudioTrack: 'Mute track',
|
||||
renameAudioTrack: 'Rename audio track',
|
||||
renameLayer: 'Rename layer',
|
||||
renameClipTrack: 'Rename clip track',
|
||||
ungroup: 'Ungroup',
|
||||
groupSelected: 'Group {n} selected layers',
|
||||
convertToClip: 'Convert {n} selected to clip',
|
||||
|
||||
@@ -487,6 +487,10 @@ export function drawSceneFrame(
|
||||
}
|
||||
const group = groupById.get(id)
|
||||
if (group) {
|
||||
// Editor-only hide cascades to every child track. Match the
|
||||
// Konva stage's stackItems so mask-preview and thumbnails
|
||||
// agree with the canvas.
|
||||
if (group.editorHidden) continue
|
||||
const childLayers: Layer[] = []
|
||||
for (let k = group.childIds.length - 1; k >= 0; k--) {
|
||||
const cl = layerById.get(group.childIds[k])
|
||||
|
||||
@@ -1829,6 +1829,35 @@ export const useSceneStore = defineStore('scene', {
|
||||
}
|
||||
}
|
||||
},
|
||||
setGroupEditorHidden(groupId: string, hidden: boolean) {
|
||||
const group = this.scene.groups?.find((g) => g.id === groupId)
|
||||
if (!group) return
|
||||
if (hidden) group.editorHidden = true
|
||||
else delete group.editorHidden
|
||||
// Editor-hide cascades to every child track; drop any selection
|
||||
// that now points inside the group so the transformer, keyboard
|
||||
// shortcuts and Properties panel stop offering edits that won't
|
||||
// render.
|
||||
if (!hidden) return
|
||||
const childIds = new Set(group.childIds)
|
||||
if (this.selectedLayerId && childIds.has(this.selectedLayerId)) {
|
||||
this.selectedLayerId = null
|
||||
this.selectedLayerIds = []
|
||||
} else if (this.selectedLayerIds.length) {
|
||||
this.selectedLayerIds = this.selectedLayerIds.filter(
|
||||
(id) => !childIds.has(id),
|
||||
)
|
||||
if (!this.selectedLayerIds.length) this.selectedLayerId = null
|
||||
}
|
||||
if (this.selectedClipInstanceId) {
|
||||
const sel = this.scene.clipInstances?.find(
|
||||
(ci) => ci.id === this.selectedClipInstanceId,
|
||||
)
|
||||
if (sel && childIds.has(sel.clipTrackId)) {
|
||||
this.selectedClipInstanceId = null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// -------- Markers --------------------------------------------------
|
||||
|
||||
@@ -1892,6 +1921,16 @@ export const useSceneStore = defineStore('scene', {
|
||||
const track = this.scene.scriptTracks?.find((t) => t.id === id)
|
||||
if (track) track.name = name.trim() || undefined
|
||||
},
|
||||
setLayerName(id: string, name: string) {
|
||||
const layer = this.activeLayers.find((l) => l.id === id)
|
||||
if (!layer) return
|
||||
const n = name.trim()
|
||||
if (n) layer.name = n
|
||||
},
|
||||
setClipTrackName(id: string, name: string) {
|
||||
const track = this.scene.clipTracks?.find((t) => t.id === id)
|
||||
if (track) track.name = name.trim() || undefined
|
||||
},
|
||||
addScriptFrame(
|
||||
trackId: string,
|
||||
time: number,
|
||||
|
||||
@@ -234,6 +234,11 @@ export interface TrackGroup {
|
||||
color?: string | null
|
||||
colorOverridden?: boolean
|
||||
locked?: boolean
|
||||
/** Editor-only hide that cascades to every child track. Same
|
||||
* semantics as `Layer.editorHidden` / `ClipTrack.editorHidden` —
|
||||
* in-editor render paths skip the children, exports still see
|
||||
* them unless the user opts into strip-hidden. */
|
||||
editorHidden?: boolean
|
||||
}
|
||||
|
||||
export interface ClipInstance {
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
export const APP_VERSION = '0.18.0'
|
||||
export const APP_VERSION = '0.18.1'
|
||||
export const APP_NAME = 'RUWIO'
|
||||
Reference in new issue
Block a user