Show family branches in plain English with Start as the card head.
Collapsed cards use parts/forks/longest instead of pN codes; expanded Studio and Library trees get fork headers and from/branch labels. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+33
-34
@@ -1725,7 +1725,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="truncate text-xs text-zinc-500">{{ clipMetaLine(entry.group.head) }}</p>
|
<p class="truncate text-xs text-zinc-500">{{ clipMetaLine(entry.group.head) }}</p>
|
||||||
<p v-if="familyBranchHint(entry.group.parts)" class="mt-1 truncate text-[11px] text-amber-200/80">{{ familyBranchHint(entry.group.parts) }}</p>
|
<p v-if="familyBranchSummary(entry.group.parts)" class="mt-1 truncate text-[11px] text-amber-200/80">{{ familyBranchSummary(entry.group.parts) }}</p>
|
||||||
<p v-if="entry.group.head.globalLocks" class="mt-1 text-[11px] text-zinc-600">Global locks saved with this collection.</p>
|
<p v-if="entry.group.head.globalLocks" class="mt-1 text-[11px] text-zinc-600">Global locks saved with this collection.</p>
|
||||||
<PermanenceRefStrip
|
<PermanenceRefStrip
|
||||||
v-if="entry.group.parts.length === 1 && !entry.group.head.hideThumbnail"
|
v-if="entry.group.parts.length === 1 && !entry.group.head.hideThumbnail"
|
||||||
@@ -1782,19 +1782,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul v-if="expandedFamilies[entry.group.key] && entry.group.parts.length > 1" class="mt-3 space-y-2 border-t border-white/5 pt-3">
|
<ul v-if="expandedFamilies[entry.group.key] && entry.group.parts.length > 1" class="mt-3 space-y-2 border-t border-white/5 pt-3">
|
||||||
<li
|
<template v-for="node in familyDisplayNodes(entry.group.parts)" :key="node.key">
|
||||||
v-for="node in treeOrderedParts(entry.group.parts)"
|
<li
|
||||||
:key="node.clip.id"
|
v-if="node.type === 'fork'"
|
||||||
class="flex cursor-pointer flex-wrap items-start gap-3 rounded-xl border p-2"
|
class="rounded-lg px-2 py-1 text-[11px] font-medium uppercase tracking-wide text-amber-200/70"
|
||||||
:class="node.clip.id === reviewingClipId
|
:style="node.depth ? { marginLeft: `${Math.min(node.depth, 6) * 0.85}rem` } : undefined"
|
||||||
? 'border-amber-300/80 bg-amber-400/15 ring-1 ring-amber-300/60'
|
>
|
||||||
: extendOpen && node.clip.id === extendClipId
|
{{ node.label }}
|
||||||
? 'border-amber-300/40 bg-amber-400/5'
|
</li>
|
||||||
: 'border-white/5 bg-black/20'"
|
<li
|
||||||
:style="node.depth ? { marginLeft: `${Math.min(node.depth, 6) * 0.85}rem` } : undefined"
|
v-else
|
||||||
:aria-current="node.clip.id === reviewingClipId ? 'true' : undefined"
|
class="flex cursor-pointer flex-wrap items-start gap-3 rounded-xl border p-2"
|
||||||
@click="onLibraryClipPartClick(node.clip, $event)"
|
:class="node.clip.id === reviewingClipId
|
||||||
>
|
? 'border-amber-300/80 bg-amber-400/15 ring-1 ring-amber-300/60'
|
||||||
|
: extendOpen && node.clip.id === extendClipId
|
||||||
|
? 'border-amber-300/40 bg-amber-400/5'
|
||||||
|
: 'border-white/5 bg-black/20'"
|
||||||
|
:style="node.depth ? { marginLeft: `${Math.min(node.depth, 6) * 0.85}rem` } : undefined"
|
||||||
|
:aria-current="node.clip.id === reviewingClipId ? 'true' : undefined"
|
||||||
|
@click="onLibraryClipPartClick(node.clip, $event)"
|
||||||
|
>
|
||||||
<ClipFrameThumbs
|
<ClipFrameThumbs
|
||||||
:hidden="node.clip.hideThumbnail"
|
:hidden="node.clip.hideThumbnail"
|
||||||
:revealed="!!stitchReveal[node.clip.id]"
|
:revealed="!!stitchReveal[node.clip.id]"
|
||||||
@@ -1817,7 +1824,8 @@
|
|||||||
>
|
>
|
||||||
<p v-else class="min-w-0 flex-1 truncate text-sm font-medium">
|
<p v-else class="min-w-0 flex-1 truncate text-sm font-medium">
|
||||||
{{ node.clip.name || node.clip.prompt }}
|
{{ node.clip.name || node.clip.prompt }}
|
||||||
<span v-if="clipBranchFromLabel(node.clip, entry.group.parts)" class="ml-1.5 text-[11px] font-normal text-amber-200/80">{{ clipBranchFromLabel(node.clip, entry.group.parts) }}</span>
|
<span v-if="node.fromLabel" class="ml-1.5 text-[11px] font-normal text-amber-200/80">{{ node.fromLabel }}</span>
|
||||||
|
<span v-if="node.branchLabel" class="ml-1.5 text-[11px] font-normal text-zinc-400">{{ node.branchLabel }}</span>
|
||||||
<span v-if="extendOpen && node.clip.id === extendClipId" class="ml-1.5 text-[10px] font-semibold uppercase tracking-wide text-amber-200">extending</span>
|
<span v-if="extendOpen && node.clip.id === extendClipId" class="ml-1.5 text-[10px] font-semibold uppercase tracking-wide text-amber-200">extending</span>
|
||||||
</p>
|
</p>
|
||||||
<PromptPreviewTip
|
<PromptPreviewTip
|
||||||
@@ -1878,7 +1886,8 @@
|
|||||||
empty="No generator settings were saved with this clip."
|
empty="No generator settings were saved with this clip."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="entry.kind === 'still' && entry.stillGroup">
|
<template v-else-if="entry.kind === 'still' && entry.stillGroup">
|
||||||
@@ -2249,13 +2258,12 @@ import { parseImageAspect, generateSizeForAspect } from '~/utils/imageAspect'
|
|||||||
import { buildOutputBaseName, nextUniqueName } from '~/utils/outputNames'
|
import { buildOutputBaseName, nextUniqueName } from '~/utils/outputNames'
|
||||||
import { emptyKeepPrompts, insertKeepSnippet, normalizeKeepPrompts, type KeepPrompt, type KeepPromptLists } from '~/utils/keepPrompts'
|
import { emptyKeepPrompts, insertKeepSnippet, normalizeKeepPrompts, type KeepPrompt, type KeepPromptLists } from '~/utils/keepPrompts'
|
||||||
import {
|
import {
|
||||||
clipBranchFromLabel,
|
|
||||||
clipPartLabel,
|
clipPartLabel,
|
||||||
familyBaseName,
|
familyBaseName,
|
||||||
familyBranchCount,
|
familyBranchSummary,
|
||||||
familyBranchHint,
|
familyDisplayNodes,
|
||||||
nextClipPartName,
|
familyRootClip,
|
||||||
treeOrderedParts
|
nextClipPartName
|
||||||
} from '~/utils/libraryBrowse'
|
} from '~/utils/libraryBrowse'
|
||||||
import {
|
import {
|
||||||
IMAGE_V2_CFG_DEFAULT,
|
IMAGE_V2_CFG_DEFAULT,
|
||||||
@@ -3355,8 +3363,8 @@ const libraryGroups = computed(() => {
|
|||||||
}
|
}
|
||||||
return [...groups.entries()]
|
return [...groups.entries()]
|
||||||
.map(([key, parts]) => {
|
.map(([key, parts]) => {
|
||||||
const sorted = [...parts].sort(sortClipParts)
|
const head = familyRootClip(parts)
|
||||||
return { key, head: sorted[0], parts: sorted }
|
return { key, head, parts: [...parts] }
|
||||||
})
|
})
|
||||||
.sort((a, b) => b.head.createdAt - a.head.createdAt)
|
.sort((a, b) => b.head.createdAt - a.head.createdAt)
|
||||||
})
|
})
|
||||||
@@ -3501,14 +3509,6 @@ function chronologicalStills(parts: LibraryStill[]) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortClipParts(a: LibraryClip, b: LibraryClip) {
|
|
||||||
const durationDelta = (Number(b.duration) || 0) - (Number(a.duration) || 0)
|
|
||||||
if (durationDelta) return durationDelta
|
|
||||||
const chainDelta = (b.chainIndex || 0) - (a.chainIndex || 0)
|
|
||||||
if (chainDelta) return chainDelta
|
|
||||||
return a.createdAt - b.createdAt
|
|
||||||
}
|
|
||||||
|
|
||||||
function chronologicalParts(parts: LibraryClip[]) {
|
function chronologicalParts(parts: LibraryClip[]) {
|
||||||
return [...parts].sort((a, b) => {
|
return [...parts].sort((a, b) => {
|
||||||
const chain = (a.chainIndex || 0) - (b.chainIndex || 0)
|
const chain = (a.chainIndex || 0) - (b.chainIndex || 0)
|
||||||
@@ -3531,9 +3531,8 @@ function previousClipIdFor(item: LibraryClip, parts: LibraryClip[]) {
|
|||||||
|
|
||||||
function familyExpandLabel(parts: LibraryClip[], expanded?: boolean) {
|
function familyExpandLabel(parts: LibraryClip[], expanded?: boolean) {
|
||||||
if (expanded) return 'Collapse'
|
if (expanded) return 'Collapse'
|
||||||
const branches = familyBranchCount(parts)
|
const summary = familyBranchSummary(parts)
|
||||||
if (branches) return `Expand ${parts.length} parts · ${branches} branch${branches === 1 ? '' : 'es'}`
|
return summary ? `Expand · ${summary}` : `Expand ${parts.length} parts`
|
||||||
return `Expand ${parts.length} parts`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureFamilyExpanded(item: LibraryClip) {
|
function ensureFamilyExpanded(item: LibraryClip) {
|
||||||
|
|||||||
+41
-5
@@ -303,7 +303,34 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="focusedParts.length > 1" class="mt-3 grid grid-cols-5 gap-1.5">
|
<div v-if="focused.kind === 'clip' && focusedDisplayNodes.length > 1" class="mt-3 space-y-1.5">
|
||||||
|
<template v-for="node in focusedDisplayNodes" :key="node.key">
|
||||||
|
<p
|
||||||
|
v-if="node.type === 'fork'"
|
||||||
|
class="px-1 text-[11px] font-medium uppercase tracking-wide text-amber-200/70"
|
||||||
|
:style="node.depth ? { marginLeft: `${Math.min(node.depth, 6) * 0.65}rem` } : undefined"
|
||||||
|
>
|
||||||
|
{{ node.label }}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
type="button"
|
||||||
|
class="flex w-full items-center gap-2 overflow-hidden rounded-lg border border-white/10 p-1 text-left"
|
||||||
|
:class="node.clip.id === focusedPartId ? 'ring-2 ring-amber-300' : ''"
|
||||||
|
:style="node.depth ? { marginLeft: `${Math.min(node.depth, 6) * 0.65}rem` } : undefined"
|
||||||
|
@click="focusPart(node.clip.id)"
|
||||||
|
>
|
||||||
|
<img :src="partThumb(node.clip)" alt="" class="h-12 w-12 shrink-0 rounded object-cover" loading="lazy">
|
||||||
|
<span class="min-w-0 flex-1">
|
||||||
|
<span class="block truncate text-xs font-medium text-zinc-100">{{ node.clip.name }}</span>
|
||||||
|
<span v-if="node.fromLabel || node.branchLabel" class="block truncate text-[11px] text-amber-200/80">
|
||||||
|
{{ [node.fromLabel, node.branchLabel].filter(Boolean).join(' · ') }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="focusedParts.length > 1" class="mt-3 grid grid-cols-5 gap-1.5">
|
||||||
<button
|
<button
|
||||||
v-for="part in focusedParts"
|
v-for="part in focusedParts"
|
||||||
:key="part.id"
|
:key="part.id"
|
||||||
@@ -453,10 +480,13 @@ import {
|
|||||||
buildLibraryEntries,
|
buildLibraryEntries,
|
||||||
chronologicalStills,
|
chronologicalStills,
|
||||||
clipBranchFromLabel,
|
clipBranchFromLabel,
|
||||||
familyBranchHint,
|
clipBranchLetterLabel,
|
||||||
|
familyBranchSummary,
|
||||||
|
familyDisplayNodes,
|
||||||
formatClipDuration,
|
formatClipDuration,
|
||||||
formatWhen,
|
formatWhen,
|
||||||
treeOrderedParts,
|
treeOrderedParts,
|
||||||
|
type FamilyDisplayNode,
|
||||||
type LibraryClip,
|
type LibraryClip,
|
||||||
type LibraryEntry,
|
type LibraryEntry,
|
||||||
type LibraryFilter,
|
type LibraryFilter,
|
||||||
@@ -614,6 +644,10 @@ const focusedParts = computed(() => {
|
|||||||
if (focused.value?.kind === 'clip') return focused.value.group?.parts || []
|
if (focused.value?.kind === 'clip') return focused.value.group?.parts || []
|
||||||
return focused.value?.stillGroup?.parts || []
|
return focused.value?.stillGroup?.parts || []
|
||||||
})
|
})
|
||||||
|
const focusedDisplayNodes = computed((): FamilyDisplayNode[] => {
|
||||||
|
if (focused.value?.kind !== 'clip' || !focused.value.group) return []
|
||||||
|
return familyDisplayNodes(focused.value.group.parts)
|
||||||
|
})
|
||||||
const focusedNavParts = computed(() => {
|
const focusedNavParts = computed(() => {
|
||||||
const parts = focusedParts.value
|
const parts = focusedParts.value
|
||||||
if (parts.length < 2) return []
|
if (parts.length < 2) return []
|
||||||
@@ -624,7 +658,9 @@ const focusedBranchHint = computed(() => {
|
|||||||
if (focused.value?.kind !== 'clip' || !focused.value.group) return ''
|
if (focused.value?.kind !== 'clip' || !focused.value.group) return ''
|
||||||
const parts = focused.value.group.parts
|
const parts = focused.value.group.parts
|
||||||
const current = parts.find(part => part.id === focusedPartId.value)
|
const current = parts.find(part => part.id === focusedPartId.value)
|
||||||
return (current && clipBranchFromLabel(current, parts)) || familyBranchHint(parts)
|
if (!current) return familyBranchSummary(parts)
|
||||||
|
const bits = [clipBranchFromLabel(current, parts), clipBranchLetterLabel(current)].filter(Boolean)
|
||||||
|
return bits.join(' · ') || familyBranchSummary(parts)
|
||||||
})
|
})
|
||||||
const focusedNavIndex = computed(() => {
|
const focusedNavIndex = computed(() => {
|
||||||
const id = focusedPartId.value
|
const id = focusedPartId.value
|
||||||
@@ -806,8 +842,8 @@ function partCount(entry: LibraryEntry) {
|
|||||||
|
|
||||||
function cardMeta(entry: LibraryEntry) {
|
function cardMeta(entry: LibraryEntry) {
|
||||||
if (entry.kind === 'clip' && entry.group) {
|
if (entry.kind === 'clip' && entry.group) {
|
||||||
const branches = familyBranchHint(entry.group.parts)
|
const summary = familyBranchSummary(entry.group.parts)
|
||||||
return [formatClipDuration(entry.group.head.duration), formatWhen(entry.createdAt), branches].filter(Boolean).join(' · ')
|
return [formatClipDuration(entry.group.head.duration), formatWhen(entry.createdAt), summary].filter(Boolean).join(' · ')
|
||||||
}
|
}
|
||||||
if (entry.kind === 'track' && entry.track) {
|
if (entry.kind === 'track' && entry.track) {
|
||||||
return [formatClipDuration(entry.track.duration), formatWhen(entry.createdAt)].filter(Boolean).join(' · ')
|
return [formatClipDuration(entry.track.duration), formatWhen(entry.createdAt)].filter(Boolean).join(' · ')
|
||||||
|
|||||||
+84
-20
@@ -136,14 +136,17 @@ export function nextClipPartName(name: string, existingNames: string[] = []) {
|
|||||||
|
|
||||||
export type ClipTreeNode = { clip: LibraryClip; depth: number }
|
export type ClipTreeNode = { clip: LibraryClip; depth: number }
|
||||||
|
|
||||||
|
export type FamilyDisplayNode =
|
||||||
|
| { type: 'fork'; key: string; depth: number; label: string; parentId: string }
|
||||||
|
| { type: 'clip'; key: string; depth: number; clip: LibraryClip; fromLabel: string; branchLabel: string }
|
||||||
|
|
||||||
function sortTreeSiblings(a: LibraryClip, b: LibraryClip) {
|
function sortTreeSiblings(a: LibraryClip, b: LibraryClip) {
|
||||||
const chain = (a.chainIndex || 0) - (b.chainIndex || 0)
|
const chain = (a.chainIndex || 0) - (b.chainIndex || 0)
|
||||||
if (chain) return chain
|
if (chain) return chain
|
||||||
return a.createdAt - b.createdAt
|
return a.createdAt - b.createdAt
|
||||||
}
|
}
|
||||||
|
|
||||||
export function treeOrderedParts(parts: LibraryClip[]): ClipTreeNode[] {
|
function buildClipChildren(parts: LibraryClip[]) {
|
||||||
if (!parts.length) return []
|
|
||||||
const byId = new Map(parts.map(part => [part.id, part]))
|
const byId = new Map(parts.map(part => [part.id, part]))
|
||||||
const children = new Map<string, LibraryClip[]>()
|
const children = new Map<string, LibraryClip[]>()
|
||||||
const roots: LibraryClip[] = []
|
const roots: LibraryClip[] = []
|
||||||
@@ -159,6 +162,12 @@ export function treeOrderedParts(parts: LibraryClip[]): ClipTreeNode[] {
|
|||||||
}
|
}
|
||||||
roots.sort(sortTreeSiblings)
|
roots.sort(sortTreeSiblings)
|
||||||
for (const list of children.values()) list.sort(sortTreeSiblings)
|
for (const list of children.values()) list.sort(sortTreeSiblings)
|
||||||
|
return { byId, children, roots }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function treeOrderedParts(parts: LibraryClip[]): ClipTreeNode[] {
|
||||||
|
if (!parts.length) return []
|
||||||
|
const { children, roots } = buildClipChildren(parts)
|
||||||
const out: ClipTreeNode[] = []
|
const out: ClipTreeNode[] = []
|
||||||
const walk = (clip: LibraryClip, depth: number) => {
|
const walk = (clip: LibraryClip, depth: number) => {
|
||||||
out.push({ clip, depth })
|
out.push({ clip, depth })
|
||||||
@@ -168,6 +177,17 @@ export function treeOrderedParts(parts: LibraryClip[]): ClipTreeNode[] {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function familyRootClip(parts: LibraryClip[]): LibraryClip {
|
||||||
|
if (!parts.length) throw new Error('familyRootClip requires at least one clip')
|
||||||
|
const { roots } = buildClipChildren(parts)
|
||||||
|
if (roots.length) return roots[0]
|
||||||
|
return chronologicalParts(parts)[0] || parts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function familyLongestDuration(parts: LibraryClip[]) {
|
||||||
|
return parts.reduce((max, part) => Math.max(max, Number(part.duration) || 0), 0)
|
||||||
|
}
|
||||||
|
|
||||||
export function familyBranchCount(parts: LibraryClip[]) {
|
export function familyBranchCount(parts: LibraryClip[]) {
|
||||||
const counts = new Map<string, number>()
|
const counts = new Map<string, number>()
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
@@ -177,31 +197,75 @@ export function familyBranchCount(parts: LibraryClip[]) {
|
|||||||
return [...counts.values()].filter(count => count > 1).length
|
return [...counts.values()].filter(count => count > 1).length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Plain English family line for collapsed cards. No p2→p3 codes. */
|
||||||
|
export function familyBranchSummary(parts: LibraryClip[]) {
|
||||||
|
if (parts.length <= 1) return ''
|
||||||
|
const forks = familyBranchCount(parts)
|
||||||
|
const longest = familyLongestDuration(parts)
|
||||||
|
const bits = [`${parts.length} parts`]
|
||||||
|
if (forks) bits.push(`${forks} fork${forks === 1 ? '' : 's'}`)
|
||||||
|
if (longest > 0) bits.push(`longest ${formatClipDuration(longest)}`)
|
||||||
|
return bits.join(' · ')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use familyBranchSummary — kept so old imports do not break. */
|
||||||
export function familyBranchHint(parts: LibraryClip[]) {
|
export function familyBranchHint(parts: LibraryClip[]) {
|
||||||
const groups = new Map<string, LibraryClip[]>()
|
return familyBranchSummary(parts)
|
||||||
for (const part of parts) {
|
}
|
||||||
if (!part.parentClipId) continue
|
|
||||||
const list = groups.get(part.parentClipId) || []
|
export function clipParentDisplayLabel(parent: LibraryClip | undefined, parts: LibraryClip[]) {
|
||||||
list.push(part)
|
if (!parent) return ''
|
||||||
groups.set(part.parentClipId, list)
|
const byId = new Map(parts.map(part => [part.id, part]))
|
||||||
}
|
const isRoot = !parent.parentClipId || !byId.has(parent.parentClipId)
|
||||||
const branched = [...groups.entries()].filter(([, kids]) => kids.length > 1)
|
if (isRoot) return 'Start'
|
||||||
if (!branched.length) return ''
|
return clipPartLabel(parent.name) || parent.name || 'previous'
|
||||||
return branched.map(([parentId, kids]) => {
|
}
|
||||||
const parent = parts.find(part => part.id === parentId)
|
|
||||||
const from = clipPartLabel(parent?.name) || 'a part'
|
export function clipBranchLetterLabel(item: LibraryClip) {
|
||||||
const names = kids.map(kid => clipPartLabel(kid.name) || kid.name || 'part').join(', ')
|
const match = String(item.name || '').trim().match(/\s+p\d+([a-z])$/i)
|
||||||
return `${from} → ${names}`
|
if (!match?.[1]) return ''
|
||||||
}).join(' · ')
|
return `branch ${match[1].toUpperCase()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clipBranchFromLabel(item: LibraryClip, parts: LibraryClip[]) {
|
export function clipBranchFromLabel(item: LibraryClip, parts: LibraryClip[]) {
|
||||||
if (!item.parentClipId) return ''
|
if (!item.parentClipId) return ''
|
||||||
const parent = parts.find(part => part.id === item.parentClipId)
|
const parent = parts.find(part => part.id === item.parentClipId)
|
||||||
const from = clipPartLabel(parent?.name) || parent?.name
|
const from = clipParentDisplayLabel(parent, parts)
|
||||||
return from ? `from ${from}` : ''
|
return from ? `from ${from}` : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function familyDisplayNodes(parts: LibraryClip[]): FamilyDisplayNode[] {
|
||||||
|
if (!parts.length) return []
|
||||||
|
const { byId, children, roots } = buildClipChildren(parts)
|
||||||
|
const out: FamilyDisplayNode[] = []
|
||||||
|
const walk = (clip: LibraryClip, depth: number) => {
|
||||||
|
const parent = clip.parentClipId ? byId.get(clip.parentClipId) : undefined
|
||||||
|
const from = clip.parentClipId ? clipParentDisplayLabel(parent, parts) : ''
|
||||||
|
out.push({
|
||||||
|
type: 'clip',
|
||||||
|
key: `clip:${clip.id}`,
|
||||||
|
depth,
|
||||||
|
clip,
|
||||||
|
fromLabel: from ? `from ${from}` : '',
|
||||||
|
branchLabel: clipBranchLetterLabel(clip)
|
||||||
|
})
|
||||||
|
const kids = children.get(clip.id) || []
|
||||||
|
if (kids.length > 1) {
|
||||||
|
const parentLabel = clipParentDisplayLabel(clip, parts) || clipPartLabel(clip.name) || 'Start'
|
||||||
|
out.push({
|
||||||
|
type: 'fork',
|
||||||
|
key: `fork:${clip.id}`,
|
||||||
|
depth: depth + 1,
|
||||||
|
label: `Forks from ${parentLabel} (${kids.length})`,
|
||||||
|
parentId: clip.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (const child of kids) walk(child, depth + 1)
|
||||||
|
}
|
||||||
|
for (const root of roots) walk(root, 0)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
export function clipFamilyKey(clip: LibraryClip) {
|
export function clipFamilyKey(clip: LibraryClip) {
|
||||||
if (clip.familyId) return `family:${clip.folderId}:${clip.familyId}`
|
if (clip.familyId) return `family:${clip.folderId}:${clip.familyId}`
|
||||||
const base = familyBaseName(clip.name || '').toLowerCase()
|
const base = familyBaseName(clip.name || '').toLowerCase()
|
||||||
@@ -298,8 +362,8 @@ function groupClips(clips: LibraryClip[]): ClipGroup[] {
|
|||||||
groups.set(key, list)
|
groups.set(key, list)
|
||||||
}
|
}
|
||||||
return [...groups.entries()].map(([key, parts]) => {
|
return [...groups.entries()].map(([key, parts]) => {
|
||||||
const sorted = [...parts].sort(sortClipParts)
|
const head = familyRootClip(parts)
|
||||||
return { key, head: sorted[0], parts: sorted }
|
return { key, head, parts: [...parts] }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user