0.13.0: unify all track kinds — same height, same drag, same grouping
- Scripts bumped from 26px to 30px so the five track kinds read as one visual list; clip / script / audio tracks lose their kind-specific gradient backgrounds now that they're no longer fixed-position chrome. - Audio tracks pick up the standard drag attributes + handlers; they now participate in the same trackOrder flow as layer / clip / script tracks and can be dropped into groups. - normalizeScenePage accepts scripts + audio as group children (used to silently strip them); the per-kind ordered appending is replaced with a single tail loop so no kind is hardcoded to the bottom. - moveTrackIntoGroup color cascade now covers all four groupable kinds (scripts + audio inherit group colour like layers + clip tracks). - Group expansion in Timeline emits scripts + audio as first-class sub-track children, with the shared in-group / sub-track classes and group-aware drag handlers. - Tests: move-into-group + color cascade for scripts + audio, legacy childIds roundtrip preserves both kinds, audio track is draggable, script track is 30px. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
354dd8a4f0
commit
86ec7aa835
5 files changed
+227
-63
No files matched your search
+108
-43
@@ -99,6 +99,9 @@ interface TimelineScriptTrack {
|
||||
orderIndex: number
|
||||
locked: boolean
|
||||
color: string | null
|
||||
inGroup: boolean
|
||||
parentGroupId?: string
|
||||
groupChildIndex?: number
|
||||
}
|
||||
|
||||
interface TimelineAudioTrack {
|
||||
@@ -109,6 +112,9 @@ interface TimelineAudioTrack {
|
||||
locked: boolean
|
||||
muted: boolean
|
||||
color: string | null
|
||||
inGroup: boolean
|
||||
parentGroupId?: string
|
||||
groupChildIndex?: number
|
||||
}
|
||||
|
||||
type TimelineTrack =
|
||||
@@ -774,6 +780,7 @@ const tracks = computed<TimelineTrack[]>(() => {
|
||||
orderIndex,
|
||||
locked: !!st.locked,
|
||||
color: st.color ?? null,
|
||||
inGroup: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -787,6 +794,7 @@ const tracks = computed<TimelineTrack[]>(() => {
|
||||
locked: !!at.locked,
|
||||
muted: !!at.muted,
|
||||
color: at.color ?? null,
|
||||
inGroup: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -820,6 +828,37 @@ const tracks = computed<TimelineTrack[]>(() => {
|
||||
parentGroupId: grp.id,
|
||||
groupChildIndex: childIndex,
|
||||
})
|
||||
return
|
||||
}
|
||||
const cst = scriptTrackById.value.get(cid)
|
||||
if (cst) {
|
||||
out.push({
|
||||
type: 'script',
|
||||
scriptTrackId: cst.id,
|
||||
name: cst.name ?? t('timeline.scriptTrackName'),
|
||||
orderIndex: -1,
|
||||
locked: !!cst.locked,
|
||||
color: cst.color ?? null,
|
||||
inGroup: true,
|
||||
parentGroupId: grp.id,
|
||||
groupChildIndex: childIndex,
|
||||
})
|
||||
return
|
||||
}
|
||||
const cat = audioTrackById.value.get(cid)
|
||||
if (cat) {
|
||||
out.push({
|
||||
type: 'audio',
|
||||
audioTrackId: cat.id,
|
||||
name: cat.name ?? t('timeline.audioTrackName'),
|
||||
orderIndex: -1,
|
||||
locked: !!cat.locked,
|
||||
muted: !!cat.muted,
|
||||
color: cat.color ?? null,
|
||||
inGroup: true,
|
||||
parentGroupId: grp.id,
|
||||
groupChildIndex: childIndex,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -846,10 +885,14 @@ const tracks = computed<TimelineTrack[]>(() => {
|
||||
return true
|
||||
}
|
||||
if (tr.type === 'script') {
|
||||
return !tr.locked
|
||||
if (tr.locked) return false
|
||||
if (lockedGroupChildIds.has(tr.scriptTrackId)) return false
|
||||
return true
|
||||
}
|
||||
if (tr.type === 'audio') {
|
||||
return !tr.locked
|
||||
if (tr.locked) return false
|
||||
if (lockedGroupChildIds.has(tr.audioTrackId)) return false
|
||||
return true
|
||||
}
|
||||
// group: hide when locked
|
||||
return !lockedGroupIds.has(tr.groupId)
|
||||
@@ -870,19 +913,42 @@ function isTrackHead(track: TimelineTrack): boolean {
|
||||
}
|
||||
|
||||
function trackChildIndex(track: TimelineTrack): number | null {
|
||||
if (track.type === 'layer' || track.type === 'clip') {
|
||||
if (
|
||||
track.type === 'layer' ||
|
||||
track.type === 'clip' ||
|
||||
track.type === 'script' ||
|
||||
track.type === 'audio'
|
||||
) {
|
||||
return track.groupChildIndex ?? null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function trackParentGroupId(track: TimelineTrack): string | null {
|
||||
if (track.type === 'layer' || track.type === 'clip') {
|
||||
if (
|
||||
track.type === 'layer' ||
|
||||
track.type === 'clip' ||
|
||||
track.type === 'script' ||
|
||||
track.type === 'audio'
|
||||
) {
|
||||
return track.parentGroupId ?? null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function isGroupable(track: TimelineTrack): track is
|
||||
| TimelineLayerTrack
|
||||
| TimelineClipTrack
|
||||
| TimelineScriptTrack
|
||||
| TimelineAudioTrack {
|
||||
return (
|
||||
track.type === 'layer' ||
|
||||
track.type === 'clip' ||
|
||||
track.type === 'script' ||
|
||||
track.type === 'audio'
|
||||
)
|
||||
}
|
||||
|
||||
function isDropBefore(track: TimelineTrack): boolean {
|
||||
if (dropIndex.value === null) return false
|
||||
if (!isTrackHead(track)) return false
|
||||
@@ -891,7 +957,7 @@ function isDropBefore(track: TimelineTrack): boolean {
|
||||
if (trackParentGroupId(track) !== dropWithinGroupId.value) return false
|
||||
return dropIndex.value === trackChildIndex(track)
|
||||
}
|
||||
if ((track.type === 'layer' || track.type === 'clip') && track.inGroup) return false
|
||||
if (isGroupable(track) && track.inGroup) return false
|
||||
return dropIndex.value === track.orderIndex
|
||||
}
|
||||
|
||||
@@ -906,7 +972,7 @@ function isDropAfter(track: TimelineTrack, nextTrack: TimelineTrack | undefined)
|
||||
if (nextTrack && trackChildIndex(nextTrack) === ci) return false
|
||||
return true
|
||||
}
|
||||
if ((track.type === 'layer' || track.type === 'clip') && track.inGroup) return false
|
||||
if (isGroupable(track) && track.inGroup) return false
|
||||
if (dropIndex.value !== track.orderIndex + 1) return false
|
||||
if (nextTrack && nextTrack.orderIndex === track.orderIndex) return false
|
||||
return true
|
||||
@@ -3432,10 +3498,22 @@ onBeforeUnmount(() => {
|
||||
dragging: dragTrackId === track.scriptTrackId,
|
||||
colored: !!track.color,
|
||||
locked: track.locked,
|
||||
'in-group': track.inGroup,
|
||||
'sub-track': track.inGroup,
|
||||
}"
|
||||
:style="trackColorStyle(track.color)"
|
||||
@dragover="!track.locked ? onTrackDragOver($event, track.orderIndex) : null"
|
||||
@drop="!track.locked ? onTrackDrop($event) : null"
|
||||
@dragover="
|
||||
!track.locked
|
||||
? (track.inGroup && track.parentGroupId !== undefined && track.groupChildIndex !== undefined
|
||||
? onGroupChildDragOver($event, track.parentGroupId, track.groupChildIndex)
|
||||
: onTrackDragOver($event, track.orderIndex))
|
||||
: null
|
||||
"
|
||||
@drop="
|
||||
!track.locked
|
||||
? (track.inGroup ? onGroupChildDrop($event) : onTrackDrop($event))
|
||||
: null
|
||||
"
|
||||
@dragleave="onTrackDragLeave"
|
||||
>
|
||||
<span
|
||||
@@ -3497,16 +3575,37 @@ onBeforeUnmount(() => {
|
||||
v-else-if="track.type === 'audio'"
|
||||
class="track audio-track"
|
||||
:class="{
|
||||
'drop-before': isDropBefore(track),
|
||||
'drop-after': isDropAfter(track, tracks[idx + 1]),
|
||||
dragging: dragTrackId === track.audioTrackId,
|
||||
colored: !!track.color,
|
||||
locked: track.locked,
|
||||
muted: track.muted,
|
||||
'in-group': track.inGroup,
|
||||
'sub-track': track.inGroup,
|
||||
}"
|
||||
:style="trackColorStyle(track.color)"
|
||||
:data-audio-track-id="track.audioTrackId"
|
||||
@dragover="
|
||||
!track.locked
|
||||
? (track.inGroup && track.parentGroupId !== undefined && track.groupChildIndex !== undefined
|
||||
? onGroupChildDragOver($event, track.parentGroupId, track.groupChildIndex)
|
||||
: onTrackDragOver($event, track.orderIndex))
|
||||
: null
|
||||
"
|
||||
@drop="
|
||||
!track.locked
|
||||
? (track.inGroup ? onGroupChildDrop($event) : onTrackDrop($event))
|
||||
: null
|
||||
"
|
||||
@dragleave="onTrackDragLeave"
|
||||
>
|
||||
<span
|
||||
class="track-name audio-track-name"
|
||||
:draggable="!track.locked"
|
||||
@contextmenu="!track.locked ? onAudioTrackContextMenu($event, track.audioTrackId) : null"
|
||||
@dragstart="!track.locked ? onTrackDragStart($event, track.audioTrackId) : null"
|
||||
@dragend="onTrackDragEnd"
|
||||
>
|
||||
<PhDotsSixVertical :size="12" class="drag-handle" />
|
||||
<PhMusicNote :size="11" class="name-badge" />
|
||||
@@ -5040,18 +5139,6 @@ onBeforeUnmount(() => {
|
||||
|
||||
.track.clip-track {
|
||||
height: 34px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
rgba(110, 168, 255, 0.04) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.clip-track .track-name {
|
||||
background: var(--panel-alt);
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.clip-lane {
|
||||
@@ -5061,18 +5148,7 @@ onBeforeUnmount(() => {
|
||||
/* ---- Script track ---- */
|
||||
|
||||
.track.script-track {
|
||||
height: 26px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(245, 158, 11, 0.06) 0%,
|
||||
rgba(245, 158, 11, 0.02) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.script-track .track-name {
|
||||
background: var(--panel-alt);
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.script-lane {
|
||||
@@ -5115,17 +5191,6 @@ onBeforeUnmount(() => {
|
||||
|
||||
.track.audio-track {
|
||||
height: 30px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
color-mix(in srgb, var(--track-color, #0ea5e9) 8%, transparent) 0%,
|
||||
color-mix(in srgb, var(--track-color, #0ea5e9) 2%, transparent) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.audio-track .track-name {
|
||||
background: var(--panel-alt);
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.audio-track.muted .track-name .track-label {
|
||||
|
||||
Reference in new issue
Block a user