Add manual image groups to the library
This commit is contained in:
+40
-6
@@ -128,6 +128,8 @@
|
|||||||
{{ folder.name }}{{ folder.protected && !folder.unlocked ? ' · locked' : '' }}
|
{{ folder.name }}{{ folder.protected && !folder.unlocked ? ' · locked' : '' }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button v-if="canGroupSelection" type="button" class="rounded-lg border border-amber-300/40 px-2 py-1 text-amber-100 disabled:opacity-50" :disabled="groupBusy" @click="groupSelectedImages()">Group images</button>
|
||||||
|
<button v-if="canUngroupSelection" type="button" class="text-amber-100 disabled:opacity-50" :disabled="groupBusy" @click="groupSelectedImages(true)">Ungroup</button>
|
||||||
<button type="button" class="text-red-300 hover:text-red-200" @click="askRemoveSelected">Delete</button>
|
<button type="button" class="text-red-300 hover:text-red-200" @click="askRemoveSelected">Delete</button>
|
||||||
<button type="button" class="text-zinc-400 hover:text-white" @click="clearSelection">Clear</button>
|
<button type="button" class="text-zinc-400 hover:text-white" @click="clearSelection">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,6 +185,7 @@
|
|||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
>
|
>
|
||||||
<span v-else class="block h-full w-full bg-zinc-800" />
|
<span v-else class="block h-full w-full bg-zinc-800" />
|
||||||
|
<FamilyNav v-if="entry.kind === 'still'" :count="partCount(entry)" :index="cardImageIndex(entry)" @step="stepCardImage(entry, $event)" />
|
||||||
<span class="absolute left-2 top-2 rounded-md bg-black/60 px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-zinc-200">{{ entry.kind === 'clip' ? 'Video' : entry.kind === 'track' ? 'Music' : 'Image' }}</span>
|
<span class="absolute left-2 top-2 rounded-md bg-black/60 px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-zinc-200">{{ entry.kind === 'clip' ? 'Video' : entry.kind === 'track' ? 'Music' : 'Image' }}</span>
|
||||||
<span
|
<span
|
||||||
v-if="partCount(entry) > 1"
|
v-if="partCount(entry) > 1"
|
||||||
@@ -340,7 +343,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="focusedParts.length > 1 && !detailsHidden" class="mt-3 grid grid-cols-5 gap-1.5">
|
<details v-else-if="focusedParts.length > 1 && !detailsHidden" class="mt-3">
|
||||||
|
<summary class="cursor-pointer text-sm text-amber-100">Expand images ({{ focusedParts.length }})</summary>
|
||||||
|
<div class="mt-2 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"
|
||||||
@@ -349,7 +354,7 @@
|
|||||||
:class="part.id === focusedPartId ? 'ring-2 ring-amber-300' : ''"
|
:class="part.id === focusedPartId ? 'ring-2 ring-amber-300' : ''"
|
||||||
@click="focusPart(part.id)"
|
@click="focusPart(part.id)"
|
||||||
>
|
>
|
||||||
<span v-if="'hideThumbnail' in part && part.hideThumbnail" class="sensitive-glow block aspect-square w-full" />
|
<span v-if="('hideThumbnail' in part && part.hideThumbnail) || ('hideInput' in part && part.hideInput)" class="sensitive-glow block aspect-square w-full" />
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
:src="partThumb(part)"
|
:src="partThumb(part)"
|
||||||
@@ -359,6 +364,7 @@
|
|||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
<p v-else-if="focused.kind === 'clip' && focusedDisplayNodes.length > 1 && detailsHidden" class="mt-3 text-xs text-zinc-500">
|
<p v-else-if="focused.kind === 'clip' && focusedDisplayNodes.length > 1 && detailsHidden" class="mt-3 text-xs text-zinc-500">
|
||||||
{{ focusedDisplayNodes.filter(node => node.type === 'clip').length }} parts hidden with preview
|
{{ focusedDisplayNodes.filter(node => node.type === 'clip').length }} parts hidden with preview
|
||||||
</p>
|
</p>
|
||||||
@@ -507,6 +513,7 @@ import {
|
|||||||
formatClipDuration,
|
formatClipDuration,
|
||||||
formatWhen,
|
formatWhen,
|
||||||
treeOrderedParts,
|
treeOrderedParts,
|
||||||
|
wrappedIndex,
|
||||||
type FamilyDisplayNode,
|
type FamilyDisplayNode,
|
||||||
type LibraryClip,
|
type LibraryClip,
|
||||||
type LibraryEntry,
|
type LibraryEntry,
|
||||||
@@ -549,6 +556,33 @@ const sort = ref<LibrarySort>('date-desc')
|
|||||||
const searchDraft = ref('')
|
const searchDraft = ref('')
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const selectedKeys = ref<string[]>([])
|
const selectedKeys = ref<string[]>([])
|
||||||
|
const groupBusy = ref(false)
|
||||||
|
const cardImages = ref<Record<string, number>>({})
|
||||||
|
const canGroupSelection = computed(() => selectedEntries().length >= 2 && selectedEntries().every(entry => entry.kind === 'still'))
|
||||||
|
const canUngroupSelection = computed(() => selectedEntries().length > 0 && selectedEntries().every(entry => entry.kind === 'still' && entry.stillGroup?.head.manualGroupId))
|
||||||
|
function cardImageIndex(entry: LibraryEntry) {
|
||||||
|
return (cardImages.value[entry.key] || 0) % Math.max(1, partCount(entry))
|
||||||
|
}
|
||||||
|
function stepCardImage(entry: LibraryEntry, delta: number) {
|
||||||
|
cardImages.value[entry.key] = wrappedIndex(partCount(entry), cardImageIndex(entry), delta)
|
||||||
|
if (focusedKey.value === entry.key) focusedPartId.value = entry.stillGroup!.parts[cardImageIndex(entry)].id
|
||||||
|
}
|
||||||
|
async function groupSelectedImages(ungroup = false) {
|
||||||
|
if (groupBusy.value) return
|
||||||
|
groupBusy.value = true
|
||||||
|
try {
|
||||||
|
const ids = selectedEntries().flatMap(entry => entry.stillGroup?.parts.map(part => part.id) || [])
|
||||||
|
applyLibrary(await $fetch('/api/library/still-groups', { method: 'POST', body: { ids, ungroup } }))
|
||||||
|
clearSelection()
|
||||||
|
clearFocus()
|
||||||
|
toast(ungroup ? 'Images ungrouped. Original iterations restored.' : 'Images grouped. Use the arrows to browse, or open the group to expand it.')
|
||||||
|
} catch (error: any) {
|
||||||
|
toast(error?.data?.statusMessage || 'Could not update image group')
|
||||||
|
} finally {
|
||||||
|
groupBusy.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const lastSelectedKey = ref('')
|
const lastSelectedKey = ref('')
|
||||||
const selectMode = ref(false)
|
const selectMode = ref(false)
|
||||||
const lightboxOpen = ref(false)
|
const lightboxOpen = ref(false)
|
||||||
@@ -796,11 +830,11 @@ function cardClass(entry: LibraryEntry) {
|
|||||||
function itemHidden(entry: LibraryEntry) {
|
function itemHidden(entry: LibraryEntry) {
|
||||||
if (entry.kind === 'clip') return Boolean(entry.group?.head.hideThumbnail)
|
if (entry.kind === 'clip') return Boolean(entry.group?.head.hideThumbnail)
|
||||||
if (entry.kind === 'track') return false
|
if (entry.kind === 'track') return false
|
||||||
return Boolean(entry.stillGroup?.head.hideInput)
|
return Boolean(entry.stillGroup?.parts[cardImageIndex(entry)]?.hideInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
function thumbHidden(entry: LibraryEntry) {
|
function thumbHidden(entry: LibraryEntry) {
|
||||||
const id = entry.kind === 'clip' ? entry.group?.head.id : entry.stillGroup?.head.id
|
const id = entry.kind === 'clip' ? entry.group?.head.id : entry.stillGroup?.parts[cardImageIndex(entry)]?.id
|
||||||
if (id && forceHiddenIds.value.includes(id)) return true
|
if (id && forceHiddenIds.value.includes(id)) return true
|
||||||
return itemHidden(entry) && !revealAll.value
|
return itemHidden(entry) && !revealAll.value
|
||||||
}
|
}
|
||||||
@@ -852,7 +886,7 @@ async function toggleFocusedHidden() {
|
|||||||
function thumbSrc(entry: LibraryEntry) {
|
function thumbSrc(entry: LibraryEntry) {
|
||||||
if (entry.kind === 'clip' && entry.group) return `/api/library/clips/${entry.group.head.id}/thumb`
|
if (entry.kind === 'clip' && entry.group) return `/api/library/clips/${entry.group.head.id}/thumb`
|
||||||
if (entry.kind === 'track') return ''
|
if (entry.kind === 'track') return ''
|
||||||
if (entry.stillGroup) return `/api/library/stills/${entry.stillGroup.head.id}?thumb=1`
|
if (entry.stillGroup) return `/api/library/stills/${entry.stillGroup.parts[cardImageIndex(entry)].id}?thumb=1`
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -960,7 +994,7 @@ function onCardClick(entry: LibraryEntry, event: MouseEvent) {
|
|||||||
? (entry.group?.head.id || '')
|
? (entry.group?.head.id || '')
|
||||||
: entry.kind === 'track'
|
: entry.kind === 'track'
|
||||||
? (entry.track?.id || '')
|
? (entry.track?.id || '')
|
||||||
: (entry.stillGroup?.head.id || '')
|
: (entry.stillGroup?.parts[cardImageIndex(entry)]?.id || '')
|
||||||
if (!sameCard) detailsHidden.value = itemHidden(entry) && !revealAll.value
|
if (!sameCard) detailsHidden.value = itemHidden(entry) && !revealAll.value
|
||||||
cancelRename()
|
cancelRename()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { owner } = assertLibraryOwner(event)
|
||||||
|
const body = await readBody(event)
|
||||||
|
if (!Array.isArray(body?.ids) || !body.ids.length || body.ids.length > 1000 || body.ids.some((id: unknown) => typeof id !== 'string')) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Select images to group' })
|
||||||
|
}
|
||||||
|
const ids = [...new Set<string>(body.ids)]
|
||||||
|
for (const id of ids) assertFolderAccess(event, getStill(owner, id).folderId)
|
||||||
|
await groupLibraryStills(owner, ids, body.ungroup === true)
|
||||||
|
return publicLibrary(event)
|
||||||
|
})
|
||||||
+38
-2
@@ -105,6 +105,9 @@ export interface LibraryStill {
|
|||||||
promptPre?: string
|
promptPre?: string
|
||||||
promptPost?: string
|
promptPost?: string
|
||||||
familyId?: string
|
familyId?: string
|
||||||
|
manualGroupId?: string
|
||||||
|
manualGroupName?: string
|
||||||
|
manualGroupIndex?: number
|
||||||
parentStillId?: string
|
parentStillId?: string
|
||||||
chainIndex?: number
|
chainIndex?: number
|
||||||
settings?: StillGenerationSettings
|
settings?: StillGenerationSettings
|
||||||
@@ -258,6 +261,9 @@ function normalizeStill(still: LibraryStill): LibraryStill {
|
|||||||
promptPre: still.promptPre,
|
promptPre: still.promptPre,
|
||||||
promptPost: still.promptPost,
|
promptPost: still.promptPost,
|
||||||
familyId: still.familyId,
|
familyId: still.familyId,
|
||||||
|
manualGroupId: still.manualGroupId,
|
||||||
|
manualGroupName: still.manualGroupName,
|
||||||
|
manualGroupIndex: still.manualGroupIndex,
|
||||||
parentStillId: still.parentStillId,
|
parentStillId: still.parentStillId,
|
||||||
chainIndex: still.chainIndex,
|
chainIndex: still.chainIndex,
|
||||||
settings: normalizeStillSettings(still.settings)
|
settings: normalizeStillSettings(still.settings)
|
||||||
@@ -413,10 +419,11 @@ export function clipsInSameFamily(catalog: Catalog, clip: LibraryClip) {
|
|||||||
|
|
||||||
export function stillsInSameFamily(catalog: Catalog, still: LibraryStill) {
|
export function stillsInSameFamily(catalog: Catalog, still: LibraryStill) {
|
||||||
const folderStills = catalog.stills.filter(item => item.folderId === still.folderId && stillRole(item) === 'output')
|
const folderStills = catalog.stills.filter(item => item.folderId === still.folderId && stillRole(item) === 'output')
|
||||||
|
if (still.manualGroupId) return folderStills.filter(item => item.manualGroupId === still.manualGroupId)
|
||||||
const ids = new Set<string>([still.id])
|
const ids = new Set<string>([still.id])
|
||||||
if (still.familyId) {
|
if (still.familyId) {
|
||||||
for (const item of folderStills) {
|
for (const item of folderStills) {
|
||||||
if (item.familyId === still.familyId) ids.add(item.id)
|
if (!item.manualGroupId && item.familyId === still.familyId) ids.add(item.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return folderStills.filter(item => ids.has(item.id))
|
return folderStills.filter(item => ids.has(item.id))
|
||||||
@@ -1580,7 +1587,7 @@ export function moveStillFamily(owner: string, id: string, folderId: string) {
|
|||||||
const familyId = family.find(item => item.familyId)?.familyId || still.familyId || crypto.randomUUID()
|
const familyId = family.find(item => item.familyId)?.familyId || still.familyId || crypto.randomUUID()
|
||||||
for (const item of family) {
|
for (const item of family) {
|
||||||
item.folderId = folder.id
|
item.folderId = folder.id
|
||||||
item.familyId = familyId
|
if (!still.manualGroupId) item.familyId = familyId
|
||||||
}
|
}
|
||||||
return family
|
return family
|
||||||
})
|
})
|
||||||
@@ -1617,6 +1624,10 @@ export function renameStillFamily(owner: string, id: string, name: string) {
|
|||||||
const family = [...stillsInSameFamily(catalog, still)].sort((a, b) => (a.chainIndex || 0) - (b.chainIndex || 0) || a.createdAt - b.createdAt)
|
const family = [...stillsInSameFamily(catalog, still)].sort((a, b) => (a.chainIndex || 0) - (b.chainIndex || 0) || a.createdAt - b.createdAt)
|
||||||
const base = familyBaseName(trimmed) || trimmed
|
const base = familyBaseName(trimmed) || trimmed
|
||||||
const familyId = family.find(item => item.familyId)?.familyId || still.familyId || crypto.randomUUID()
|
const familyId = family.find(item => item.familyId)?.familyId || still.familyId || crypto.randomUUID()
|
||||||
|
if (still.manualGroupId) {
|
||||||
|
for (const item of family) item.manualGroupName = trimmed
|
||||||
|
return family
|
||||||
|
}
|
||||||
family.forEach((item, index) => {
|
family.forEach((item, index) => {
|
||||||
item.familyId = familyId
|
item.familyId = familyId
|
||||||
item.name = stillChainName(base, index)
|
item.name = stillChainName(base, index)
|
||||||
@@ -2153,3 +2164,28 @@ export async function importMissingComfyAudio(owner: string, folderId?: string)
|
|||||||
}
|
}
|
||||||
return imported
|
return imported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grouping only changes library organization; generation ancestry remains intact.
|
||||||
|
export function groupLibraryStills(owner: string, ids: string[], ungroup = false) {
|
||||||
|
return mutate(owner, (catalog) => {
|
||||||
|
const selected = ids.map(id => catalog.stills.find(item => item.id === id))
|
||||||
|
if (selected.some(item => !item || stillRole(item) !== 'output')) throw createError({ statusCode: 400, statusMessage: 'Choose generated images' })
|
||||||
|
const images = selected as LibraryStill[]
|
||||||
|
if (new Set(images.map(item => item.folderId)).size !== 1) throw createError({ statusCode: 400, statusMessage: 'Move the images into the same folder first' })
|
||||||
|
const members = [...new Map(images.flatMap(item => stillsInSameFamily(catalog, item)).map(item => [item.id, item])).values()]
|
||||||
|
if (!ungroup && members.length < 2) throw createError({ statusCode: 400, statusMessage: 'Select at least two images' })
|
||||||
|
const groupId = crypto.randomUUID()
|
||||||
|
const name = images.find(item => item.manualGroupName)?.manualGroupName || 'Image group'
|
||||||
|
members.forEach((item, index) => {
|
||||||
|
if (ungroup) {
|
||||||
|
delete item.manualGroupId
|
||||||
|
delete item.manualGroupName
|
||||||
|
delete item.manualGroupIndex
|
||||||
|
} else {
|
||||||
|
item.manualGroupId = groupId
|
||||||
|
item.manualGroupName = name
|
||||||
|
item.manualGroupIndex = index
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import ts from 'typescript'
|
||||||
|
|
||||||
|
const source = readFileSync(new URL('../utils/libraryBrowse.ts', import.meta.url), 'utf8')
|
||||||
|
const js = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 } }).outputText
|
||||||
|
const { buildLibraryEntries, chronologicalStills } = await import(`data:text/javascript;base64,${Buffer.from(js).toString('base64')}`)
|
||||||
|
const image = (id, extra = {}) => ({ id, folderId: 'inbox', filename: `${id}.png`, role: 'output', createdAt: 1, width: 512, height: 512, ...extra })
|
||||||
|
const entries = stills => buildLibraryEntries({ stills, clips: [], folderId: 'all', filter: 'images', search: '', sort: 'date-desc' })
|
||||||
|
|
||||||
|
test('manual grouping joins unrelated images without changing iteration ancestry', () => {
|
||||||
|
const a = image('a', { familyId: 'iteration-a', chainIndex: 8, manualGroupId: 'manual', manualGroupName: 'Favorites', manualGroupIndex: 0 })
|
||||||
|
const b = image('b', { familyId: 'iteration-b', chainIndex: 0, manualGroupId: 'manual', manualGroupName: 'Favorites', manualGroupIndex: 1 })
|
||||||
|
const grouped = entries([b, a])
|
||||||
|
assert.equal(grouped.length, 1)
|
||||||
|
assert.equal(grouped[0].name, 'Favorites')
|
||||||
|
assert.deepEqual(chronologicalStills(grouped[0].stillGroup.parts).map(x => x.id), ['a', 'b'])
|
||||||
|
assert.equal(a.familyId, 'iteration-a')
|
||||||
|
assert.equal(b.familyId, 'iteration-b')
|
||||||
|
assert.equal(entries([a, b].map(({ manualGroupId, manualGroupName, manualGroupIndex, ...rest }) => rest)).length, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('groups remain folder scoped and do not absorb future iterations', () => {
|
||||||
|
const a = image('a', { familyId: 'iteration', manualGroupId: 'manual' })
|
||||||
|
const b = image('b', { familyId: 'iteration' })
|
||||||
|
const c = image('c', { folderId: 'private', manualGroupId: 'manual' })
|
||||||
|
assert.equal(entries([a, b, c]).length, 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
const librarySource = readFileSync(new URL('../server/utils/library.ts', import.meta.url), 'utf8')
|
||||||
|
const ast = ts.createSourceFile('library.ts', librarySource, ts.ScriptTarget.Latest, true)
|
||||||
|
const names = ['stillsInSameFamily', 'groupLibraryStills', 'moveStillFamily', 'renameStillFamily']
|
||||||
|
const functions = ast.statements.filter(node => ts.isFunctionDeclaration(node) && names.includes(node.name?.text)).map(node => node.getText(ast).replace(/^export /, '')).join('\n')
|
||||||
|
const serverJs = ts.transpileModule(functions, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
|
||||||
|
function server(catalog) {
|
||||||
|
return new Function('mutate', 'stillRole', 'createError', 'familyBaseName', 'stillChainName', `${serverJs}; return { ${names.join(',')} };`)(
|
||||||
|
async (_, fn) => fn(catalog), item => item.role, error => Object.assign(new Error(error.statusMessage), error), name => name, (name, index) => `${name} ${index}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
test('group, rename, move and ungroup preserve original generation links and names', async () => {
|
||||||
|
const a = image('a', { familyId: 'first', name: 'Original A' })
|
||||||
|
const b = image('b', { familyId: 'second', name: 'Original B' })
|
||||||
|
const catalog = { stills: [a, b], folders: [{ id: 'inbox' }, { id: 'other' }] }
|
||||||
|
const api = server(catalog)
|
||||||
|
await api.groupLibraryStills('owner', ['a', 'b'])
|
||||||
|
assert.equal(a.manualGroupId, b.manualGroupId)
|
||||||
|
await api.renameStillFamily('owner', 'a', 'Favorites')
|
||||||
|
await api.moveStillFamily('owner', 'a', 'other')
|
||||||
|
assert.equal(b.folderId, 'other')
|
||||||
|
assert.equal(b.manualGroupName, 'Favorites')
|
||||||
|
assert.equal(a.name, 'Original A')
|
||||||
|
assert.equal(b.familyId, 'second')
|
||||||
|
await api.groupLibraryStills('owner', ['a'], true)
|
||||||
|
assert.equal(a.manualGroupId, undefined)
|
||||||
|
assert.equal(b.manualGroupId, undefined)
|
||||||
|
assert.equal(a.familyId, 'first')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('invalid grouping is rejected before modifying any images', async () => {
|
||||||
|
const a = image('a')
|
||||||
|
const b = image('b', { folderId: 'private' })
|
||||||
|
const api = server({ stills: [a, b] })
|
||||||
|
await assert.rejects(api.groupLibraryStills('owner', ['a', 'b']), /same folder/)
|
||||||
|
await assert.rejects(api.groupLibraryStills('owner', ['a', 'missing']), /generated images/)
|
||||||
|
assert.equal(a.manualGroupId, undefined)
|
||||||
|
})
|
||||||
@@ -30,6 +30,9 @@ export type LibraryStill = {
|
|||||||
promptPre?: string
|
promptPre?: string
|
||||||
promptPost?: string
|
promptPost?: string
|
||||||
familyId?: string
|
familyId?: string
|
||||||
|
manualGroupId?: string
|
||||||
|
manualGroupName?: string
|
||||||
|
manualGroupIndex?: number
|
||||||
parentStillId?: string
|
parentStillId?: string
|
||||||
chainIndex?: number
|
chainIndex?: number
|
||||||
settings?: StillGenerationSettings
|
settings?: StillGenerationSettings
|
||||||
@@ -289,11 +292,13 @@ export function clipFamilyKey(clip: LibraryClip) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function stillFamilyKey(still: LibraryStill) {
|
export function stillFamilyKey(still: LibraryStill) {
|
||||||
|
if (still.manualGroupId) return `still-group:${still.folderId}:${still.manualGroupId}`
|
||||||
if (still.familyId) return `still-family:${still.folderId}:${still.familyId}`
|
if (still.familyId) return `still-family:${still.folderId}:${still.familyId}`
|
||||||
return `still-solo:${still.id}`
|
return `still-solo:${still.id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sortStillParts(a: LibraryStill, b: LibraryStill) {
|
export function sortStillParts(a: LibraryStill, b: LibraryStill) {
|
||||||
|
if (a.manualGroupId && a.manualGroupId === b.manualGroupId) return (a.manualGroupIndex || 0) - (b.manualGroupIndex || 0)
|
||||||
const chainDelta = (b.chainIndex || 0) - (a.chainIndex || 0)
|
const chainDelta = (b.chainIndex || 0) - (a.chainIndex || 0)
|
||||||
if (chainDelta) return chainDelta
|
if (chainDelta) return chainDelta
|
||||||
return b.createdAt - a.createdAt
|
return b.createdAt - a.createdAt
|
||||||
@@ -301,6 +306,7 @@ export function sortStillParts(a: LibraryStill, b: LibraryStill) {
|
|||||||
|
|
||||||
export function chronologicalStills(parts: LibraryStill[]) {
|
export function chronologicalStills(parts: LibraryStill[]) {
|
||||||
return [...parts].sort((a, b) => {
|
return [...parts].sort((a, b) => {
|
||||||
|
if (a.manualGroupId && a.manualGroupId === b.manualGroupId) return (a.manualGroupIndex || 0) - (b.manualGroupIndex || 0)
|
||||||
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
|
||||||
@@ -344,6 +350,7 @@ export function collectionTitle(group: ClipGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function stillCollectionTitle(group: StillGroup) {
|
export function stillCollectionTitle(group: StillGroup) {
|
||||||
|
if (group.head.manualGroupId) return group.head.manualGroupName || 'Image group'
|
||||||
const named = group.parts.map(part => familyBaseName(part.name || '')).find(Boolean)
|
const named = group.parts.map(part => familyBaseName(part.name || '')).find(Boolean)
|
||||||
return named || stillTitle(group.head)
|
return named || stillTitle(group.head)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user