Make library hide/reveal instant instead of rewriting the whole catalog.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-02 15:05:04 -05:00
co-authored by Cursor
parent e7298d9693
commit 4546aa5f61
6 changed files with 108 additions and 12 deletions
+23 -1
View File
@@ -3626,7 +3626,29 @@ function applyInstancePrivacy() {
}
function applyLibrary(data: { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record<string, string>; loraTriggers?: Record<string, string>; keepPrompts?: KeepPromptLists } | unknown) {
const payload = (data || {}) as { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record<string, string>; loraTriggers?: Record<string, string>; keepPrompts?: KeepPromptLists }
const payload = (data || {}) as {
id?: string
hideInput?: boolean
hideThumbnail?: boolean
folders?: LibraryFolder[]
clips?: LibraryClip[]
stills?: LibraryStill[]
drafts?: RetryDraft[]
loraAliases?: Record<string, string>
loraTriggers?: Record<string, string>
keepPrompts?: KeepPromptLists
}
if (payload.id && !payload.folders && !payload.clips && !payload.stills) {
if (typeof payload.hideInput === 'boolean') {
const still = stills.value.find(item => item.id === payload.id)
if (still) still.hideInput = payload.hideInput
}
if (typeof payload.hideThumbnail === 'boolean') {
const clip = clips.value.find(item => item.id === payload.id)
if (clip) clip.hideThumbnail = payload.hideThumbnail
}
return
}
folders.value = payload.folders || []
clips.value = payload.clips || []
stills.value = payload.stills || []
+61 -3
View File
@@ -215,6 +215,7 @@
v-else-if="focused.kind === 'still' && previewStillSrc"
:src="previewStillSrc"
alt=""
decoding="async"
class="aspect-video w-full object-contain"
>
<img
@@ -236,7 +237,7 @@
class="absolute right-2 top-2 z-30 flex h-8 w-8 items-center justify-center rounded-full border border-white/20 bg-black/60 text-zinc-100 hover:border-amber-300/50 hover:text-amber-100"
:title="detailsHidden ? 'Show preview' : 'Hide preview'"
:aria-label="detailsHidden ? 'Show preview' : 'Hide preview'"
@click.stop="detailsHidden = !detailsHidden"
@click.stop="toggleFocusedHidden"
>
<svg v-if="detailsHidden" class="h-4 w-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M3 3l18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
@@ -410,6 +411,8 @@ const renameInput = ref<HTMLInputElement | null>(null)
const deleteBusy = ref(false)
const detailsHidden = ref(false)
const revealAll = ref(false)
const hidingFocused = ref(false)
const forceHiddenIds = ref<string[]>([])
const pendingDelete = ref<{ title: string; name: string; detail?: string; consequence: string; keys: string[] } | null>(null)
const keepingInputBusy = ref(false)
const toasts = ref<Array<{ id: number; message: string }>>([])
@@ -488,7 +491,7 @@ const previewVideo = computed(() => {
const previewStillSrc = computed(() => {
if (focused.value?.kind !== 'still' || !focused.value.stillGroup) return ''
const id = focusedPartId.value || focused.value.stillGroup.head.id
return `/api/library/stills/${id}`
return `/api/library/stills/${id}?thumb=1`
})
const focusedParts = computed(() => {
if (focused.value?.kind === 'clip') return focused.value.group?.parts || []
@@ -545,7 +548,25 @@ function toast(message: string) {
}
function applyLibrary(data: unknown) {
const payload = (data || {}) as { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[] }
const payload = (data || {}) as {
id?: string
hideInput?: boolean
hideThumbnail?: boolean
folders?: LibraryFolder[]
clips?: LibraryClip[]
stills?: LibraryStill[]
}
if (payload.id && !payload.folders && !payload.clips && !payload.stills) {
if (typeof payload.hideInput === 'boolean') {
const still = stills.value.find(item => item.id === payload.id)
if (still) still.hideInput = payload.hideInput
}
if (typeof payload.hideThumbnail === 'boolean') {
const clip = clips.value.find(item => item.id === payload.id)
if (clip) clip.hideThumbnail = payload.hideThumbnail
}
return
}
folders.value = payload.folders || []
clips.value = payload.clips || []
stills.value = payload.stills || []
@@ -585,6 +606,8 @@ function itemHidden(entry: LibraryEntry) {
}
function thumbHidden(entry: LibraryEntry) {
const id = entry.kind === 'clip' ? entry.group?.head.id : entry.stillGroup?.head.id
if (id && forceHiddenIds.value.includes(id)) return true
return itemHidden(entry) && !revealAll.value
}
@@ -592,11 +615,46 @@ const hasHiddenThumbs = computed(() => entries.value.some(entry => itemHidden(en
function toggleRevealAll() {
revealAll.value = !revealAll.value
forceHiddenIds.value = []
if (focused.value && itemHidden(focused.value)) {
detailsHidden.value = !revealAll.value
}
}
async function toggleFocusedHidden() {
const entry = focused.value
if (!entry || hidingFocused.value) return
const hide = !detailsHidden.value
detailsHidden.value = hide
const id = entry.kind === 'clip'
? (focusedPartId.value || entry.group?.head.id || '')
: (focusedStill.value?.id || '')
if (hide && revealAll.value && id && !forceHiddenIds.value.includes(id)) {
forceHiddenIds.value = [...forceHiddenIds.value, id]
} else if (!hide && id) {
forceHiddenIds.value = forceHiddenIds.value.filter(item => item !== id)
}
hidingFocused.value = true
try {
if (entry.kind === 'clip' && entry.group) {
const id = focusedPartId.value || entry.group.head.id
const clip = clips.value.find(item => item.id === id)
if (clip) clip.hideThumbnail = hide
applyLibrary(await $fetch(`/api/library/clips/${id}`, { method: 'PATCH', body: { hideThumbnail: hide } }))
} else if (focusedStill.value) {
const id = focusedStill.value.id
const still = stills.value.find(item => item.id === id)
if (still) still.hideInput = hide
applyLibrary(await $fetch(`/api/library/stills/${id}`, { method: 'PATCH', body: { hideInput: hide } }))
}
} catch (error: any) {
detailsHidden.value = !hide
toast(error?.data?.statusMessage || 'Could not update thumbnail')
} finally {
hidingFocused.value = false
}
}
function thumbSrc(entry: LibraryEntry) {
if (entry.kind === 'clip' && entry.group?.head.hasThumbnail) return `/api/library/clips/${entry.group.head.id}/thumb`
if (entry.stillGroup) return `/api/library/stills/${entry.stillGroup.head.id}?thumb=1`