Keep fetching library pages after the first 50 so the whole folder is reachable.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -737,7 +737,12 @@ const sourceSlot=computed(()=>{const clip=clips.value.find((a:any)=>a.id===form.
|
|||||||
const pickerAssets=computed(()=>picker.value==='startClipId'?clips.value.map((a:any)=>({...a,kind:'clip'})):stills.value.map((a:any)=>({...a,kind:'still'})))
|
const pickerAssets=computed(()=>picker.value==='startClipId'?clips.value.map((a:any)=>({...a,kind:'clip'})):stills.value.map((a:any)=>({...a,kind:'still'})))
|
||||||
const inputStill=computed(()=>stills.value.find((s:any)=>s.id===form.imageAId))
|
const inputStill=computed(()=>stills.value.find((s:any)=>s.id===form.imageAId))
|
||||||
const family=computed(()=>folderAssets(library.value,jobs.value,form.folderId))
|
const family=computed(()=>folderAssets(library.value,jobs.value,form.folderId))
|
||||||
const familyStillCount=computed(()=>family.value.filter((a:any)=>a.kind==='still').length)
|
const familyStillCount=computed(()=>{
|
||||||
|
const folder=(library.value.folders||[]).find((f:any)=>f.id===form.folderId)
|
||||||
|
const fromFolder=Number(folder?.imageCount||0)
|
||||||
|
if(fromFolder>0) return fromFolder
|
||||||
|
return family.value.filter((a:any)=>a.kind==='still').length
|
||||||
|
})
|
||||||
const sourceAsset=computed(()=>form.startClipId?clips.value.find((c:any)=>c.id===form.startClipId):stills.value.find((s:any)=>s.id===(form.imageAId||form.identityStillId||form.photos?.[0]?.stillId)))
|
const sourceAsset=computed(()=>form.startClipId?clips.value.find((c:any)=>c.id===form.startClipId):stills.value.find((s:any)=>s.id===(form.imageAId||form.identityStillId||form.photos?.[0]?.stillId)))
|
||||||
const sourceSize=computed(()=>measured[sourceAsset.value?.id] || sourceAsset.value)
|
const sourceSize=computed(()=>measured[sourceAsset.value?.id] || sourceAsset.value)
|
||||||
const resolvedSize=computed(()=>resolveSize(form.settings.aspect,sourceSize.value,undefined,form.engine))
|
const resolvedSize=computed(()=>resolveSize(form.settings.aspect,sourceSize.value,undefined,form.engine))
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ function applyLibraryPage(data: any, replace: boolean) {
|
|||||||
fetchedAt.value = Date.now()
|
fetchedAt.value = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchLibraryPage(cursor: string | null, replace: boolean) {
|
async function fetchLibraryPage(cursor: string | null) {
|
||||||
const folder = activeFolderId.value && activeFolderId.value !== 'all' ? activeFolderId.value : ''
|
const folder = activeFolderId.value && activeFolderId.value !== 'all' ? activeFolderId.value : ''
|
||||||
const type = filter.value === 'all' ? 'all' : filter.value === 'images' ? 'images' : filter.value === 'videos' ? 'videos' : 'music'
|
const type = filter.value === 'all' ? 'all' : filter.value === 'images' ? 'images' : filter.value === 'videos' ? 'videos' : 'music'
|
||||||
const qs = new URLSearchParams({ limit: '50' })
|
const qs = new URLSearchParams({ limit: '50' })
|
||||||
@@ -296,28 +296,33 @@ async function fetchLibraryPage(cursor: string | null, replace: boolean) {
|
|||||||
return await $fetch<any>(`/api/library?${qs.toString()}`)
|
return await $fetch<any>(`/api/library?${qs.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let fetchGen = 0
|
||||||
|
|
||||||
async function fetchLibrary(force = false) {
|
async function fetchLibrary(force = false) {
|
||||||
if (loading.value) return
|
|
||||||
const stale = !fetchedAt.value || Date.now() - fetchedAt.value > 15_000
|
const stale = !fetchedAt.value || Date.now() - fetchedAt.value > 15_000
|
||||||
const empty = !stills.value.length && !clips.value.length && !tracks.value.length
|
const empty = !stills.value.length && !clips.value.length && !tracks.value.length
|
||||||
if (!force && !empty && !stale) return
|
if (!force && !empty && !stale) return
|
||||||
|
const gen = ++fetchGen
|
||||||
loading.value = true
|
loading.value = true
|
||||||
loadError.value = ''
|
loadError.value = ''
|
||||||
liveThumbs.value = new Set()
|
liveThumbs.value = new Set()
|
||||||
try {
|
try {
|
||||||
const first = await fetchLibraryPage(null, true)
|
const first = await fetchLibraryPage(null)
|
||||||
|
if (gen !== fetchGen) return
|
||||||
applyLibraryPage(first, true)
|
applyLibraryPage(first, true)
|
||||||
loading.value = false
|
loading.value = false
|
||||||
nextTick(() => observeThumbs())
|
nextTick(() => observeThumbs())
|
||||||
// Pull remaining pages in the background — first paint already has ≤50 items
|
// Keep fetching until the folder is exhausted — 50 is a page size, not a cap
|
||||||
let cursor = first?.nextCursor || null
|
let cursor = first?.nextCursor || null
|
||||||
while (cursor) {
|
while (cursor && gen === fetchGen) {
|
||||||
const more = await fetchLibraryPage(cursor, false)
|
const more = await fetchLibraryPage(cursor)
|
||||||
|
if (gen !== fetchGen) return
|
||||||
applyLibraryPage(more, false)
|
applyLibraryPage(more, false)
|
||||||
cursor = more?.nextCursor || null
|
cursor = more?.nextCursor || null
|
||||||
nextTick(() => observeThumbs())
|
nextTick(() => observeThumbs())
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
if (gen !== fetchGen) return
|
||||||
loadError.value = e?.data?.statusMessage || e?.message || 'Could not load library'
|
loadError.value = e?.data?.statusMessage || e?.message || 'Could not load library'
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -354,7 +359,7 @@ async function fetchMore() {
|
|||||||
if (!nextCursor.value || loading.value) return
|
if (!nextCursor.value || loading.value) return
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const more = await fetchLibraryPage(nextCursor.value, false)
|
const more = await fetchLibraryPage(nextCursor.value)
|
||||||
applyLibraryPage(more, false)
|
applyLibraryPage(more, false)
|
||||||
nextTick(() => observeThumbs())
|
nextTick(() => observeThumbs())
|
||||||
} catch { /* keep what we have */ }
|
} catch { /* keep what we have */ }
|
||||||
@@ -404,6 +409,11 @@ watch(() => props.open, (on) => {
|
|||||||
|
|
||||||
watch(entries, () => nextTick(() => observeThumbs()))
|
watch(entries, () => nextTick(() => observeThumbs()))
|
||||||
|
|
||||||
|
watch([filter, activeFolderId], () => {
|
||||||
|
if (!props.open) return
|
||||||
|
void fetchLibrary(true)
|
||||||
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
document.body.style.overflow = ''
|
document.body.style.overflow = ''
|
||||||
|
|||||||
+55
-33
@@ -634,24 +634,6 @@ const allCount = computed(() => {
|
|||||||
sort: 'date-desc'
|
sort: 'date-desc'
|
||||||
}).length
|
}).length
|
||||||
})
|
})
|
||||||
const folderChipCounts = computed(() => {
|
|
||||||
const map: Record<string, { videos: number; images: number; music: number }> = {}
|
|
||||||
for (const clip of clips.value) {
|
|
||||||
if (clip.hasVideo === false) continue
|
|
||||||
const row = map[clip.folderId] || (map[clip.folderId] = { videos: 0, images: 0, music: 0 })
|
|
||||||
row.videos += 1
|
|
||||||
}
|
|
||||||
for (const still of stills.value) {
|
|
||||||
if (still.role !== 'output') continue
|
|
||||||
const row = map[still.folderId] || (map[still.folderId] = { videos: 0, images: 0, music: 0 })
|
|
||||||
row.images += 1
|
|
||||||
}
|
|
||||||
for (const track of tracks.value) {
|
|
||||||
const row = map[track.folderId] || (map[track.folderId] = { videos: 0, images: 0, music: 0 })
|
|
||||||
row.music += 1
|
|
||||||
}
|
|
||||||
return map
|
|
||||||
})
|
|
||||||
const moveFolders = computed(() => folders.value.filter(folder => folder.id !== folderId.value))
|
const moveFolders = computed(() => folders.value.filter(folder => folder.id !== folderId.value))
|
||||||
const focused = computed(() => entries.value.find(entry => entry.key === focusedKey.value) || null)
|
const focused = computed(() => entries.value.find(entry => entry.key === focusedKey.value) || null)
|
||||||
const focusedStill = computed(() => {
|
const focusedStill = computed(() => {
|
||||||
@@ -771,7 +753,8 @@ function toast(message: string) {
|
|||||||
}, 4200)
|
}, 4200)
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyLibrary(data: unknown) {
|
function applyLibrary(data: unknown, opts?: { replace?: boolean }) {
|
||||||
|
const replace = opts?.replace !== false
|
||||||
const payload = (data || {}) as {
|
const payload = (data || {}) as {
|
||||||
id?: string
|
id?: string
|
||||||
hideInput?: boolean
|
hideInput?: boolean
|
||||||
@@ -780,6 +763,8 @@ function applyLibrary(data: unknown) {
|
|||||||
clips?: LibraryClip[]
|
clips?: LibraryClip[]
|
||||||
stills?: LibraryStill[]
|
stills?: LibraryStill[]
|
||||||
tracks?: LibraryTrack[]
|
tracks?: LibraryTrack[]
|
||||||
|
nextCursor?: string | null
|
||||||
|
hasMore?: boolean
|
||||||
}
|
}
|
||||||
if (payload.id && !payload.folders && !payload.clips && !payload.stills && !payload.tracks) {
|
if (payload.id && !payload.folders && !payload.clips && !payload.stills && !payload.tracks) {
|
||||||
if (typeof payload.hideInput === 'boolean') {
|
if (typeof payload.hideInput === 'boolean') {
|
||||||
@@ -792,29 +777,62 @@ function applyLibrary(data: unknown) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
folders.value = payload.folders || []
|
if (payload.folders?.length) folders.value = payload.folders
|
||||||
clips.value = payload.clips || []
|
if (replace) {
|
||||||
stills.value = payload.stills || []
|
clips.value = payload.clips || []
|
||||||
tracks.value = payload.tracks || []
|
stills.value = payload.stills || []
|
||||||
|
tracks.value = payload.tracks || []
|
||||||
|
} else {
|
||||||
|
const merge = <T extends { id: string }>(prev: T[], next: T[]) => {
|
||||||
|
const map = new Map<string, T>()
|
||||||
|
for (const item of prev) map.set(item.id, item)
|
||||||
|
for (const item of next) map.set(item.id, item)
|
||||||
|
return [...map.values()]
|
||||||
|
}
|
||||||
|
clips.value = merge(clips.value, payload.clips || [])
|
||||||
|
stills.value = merge(stills.value, payload.stills || [])
|
||||||
|
tracks.value = merge(tracks.value, payload.tracks || [])
|
||||||
|
}
|
||||||
|
libraryNextCursor.value = payload.nextCursor || null
|
||||||
if (folderId.value !== 'all' && !folders.value.some(folder => folder.id === folderId.value)) {
|
if (folderId.value !== 'all' && !folders.value.some(folder => folder.id === folderId.value)) {
|
||||||
folderId.value = folders.value[0]?.id || 'all'
|
folderId.value = folders.value[0]?.id || 'all'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const libraryNextCursor = ref<string | null>(null)
|
||||||
|
let libraryFetchGen = 0
|
||||||
|
|
||||||
|
async function fetchLibraryPage(cursor: string | null) {
|
||||||
|
const qs = new URLSearchParams({ limit: '50' })
|
||||||
|
if (folderId.value && folderId.value !== 'all') qs.set('folderId', folderId.value)
|
||||||
|
if (filter.value === 'images') qs.set('type', 'images')
|
||||||
|
else if (filter.value === 'videos') qs.set('type', 'videos')
|
||||||
|
else if (filter.value === 'music') qs.set('type', 'music')
|
||||||
|
if (cursor) qs.set('cursor', cursor)
|
||||||
|
return await $fetch<any>(`/api/library?${qs.toString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
async function loadLibrary() {
|
async function loadLibrary() {
|
||||||
// First page of metadata only — never follow with a full light=1 catalog scan
|
// First page paints immediately; remaining pages append in the background
|
||||||
const first = await $fetch<any>('/api/library?limit=50')
|
const gen = ++libraryFetchGen
|
||||||
applyLibrary(first)
|
const first = await fetchLibraryPage(null)
|
||||||
|
if (gen !== libraryFetchGen) return
|
||||||
|
applyLibrary(first, { replace: true })
|
||||||
|
let cursor = first?.nextCursor || null
|
||||||
|
while (cursor && gen === libraryFetchGen) {
|
||||||
|
const page = await fetchLibraryPage(cursor)
|
||||||
|
if (gen !== libraryFetchGen) return
|
||||||
|
applyLibrary(page, { replace: false })
|
||||||
|
cursor = page?.nextCursor || null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function folderChipCount(folder: LibraryFolder) {
|
function folderChipCount(folder: LibraryFolder) {
|
||||||
if (!folder.unlocked) return (folder.clipCount || 0) + (folder.imageCount || 0) + (folder.trackCount || 0)
|
// Prefer server totals so chips match the full folder before pages finish appending
|
||||||
const row = folderChipCounts.value[folder.id]
|
if (filter.value === 'videos') return folder.clipCount || 0
|
||||||
if (!row) return 0
|
if (filter.value === 'images') return folder.imageCount || 0
|
||||||
if (filter.value === 'videos') return row.videos
|
if (filter.value === 'music') return folder.trackCount || 0
|
||||||
if (filter.value === 'images') return row.images
|
return (folder.clipCount || 0) + (folder.imageCount || 0) + (folder.trackCount || 0)
|
||||||
if (filter.value === 'music') return row.music
|
|
||||||
return row.videos + row.images + row.music
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function folderChipClass(id: string) {
|
function folderChipClass(id: string) {
|
||||||
@@ -1280,6 +1298,10 @@ watch([folderId, filter], () => {
|
|||||||
watch(search, () => {
|
watch(search, () => {
|
||||||
if (scroller.value) scroller.value.scrollTop = 0
|
if (scroller.value) scroller.value.scrollTop = 0
|
||||||
})
|
})
|
||||||
|
watch([folderId, filter], () => {
|
||||||
|
if (scroller.value) scroller.value.scrollTop = 0
|
||||||
|
void loadLibrary()
|
||||||
|
})
|
||||||
watch([cols, rowH], ([nextCols, nextRowH], [prevCols, prevRowH]) => {
|
watch([cols, rowH], ([nextCols, nextRowH], [prevCols, prevRowH]) => {
|
||||||
const el = scroller.value
|
const el = scroller.value
|
||||||
if (!el || !prevRowH) return
|
if (!el || !prevRowH) return
|
||||||
|
|||||||
+11
-4
@@ -752,12 +752,18 @@ export function publicLibraryPage(
|
|||||||
|
|
||||||
let start = 0
|
let start = 0
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
const idx = rows.findIndex(row => `${row.type}:${row.id}` === cursor)
|
if (cursor.startsWith('offset:')) {
|
||||||
start = idx >= 0 ? idx + 1 : 0
|
start = Math.max(0, Math.min(rows.length, Number(cursor.slice(7)) || 0))
|
||||||
|
} else {
|
||||||
|
const idx = rows.findIndex(row => `${row.type}:${row.id}` === cursor)
|
||||||
|
// Unknown cursor → end (never restart at 0 or the client loops the first page forever)
|
||||||
|
start = idx >= 0 ? idx + 1 : rows.length
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const page = rows.slice(start, start + limit)
|
const page = rows.slice(start, start + limit)
|
||||||
const last = page[page.length - 1]
|
const nextStart = start + page.length
|
||||||
const nextCursor = page.length === limit && last ? `${last.type}:${last.id}` : null
|
const hasMore = nextStart < rows.length
|
||||||
|
const nextCursor = hasMore ? `offset:${nextStart}` : null
|
||||||
|
|
||||||
const stillIds = new Set(page.filter(r => r.type === 'still').map(r => r.id))
|
const stillIds = new Set(page.filter(r => r.type === 'still').map(r => r.id))
|
||||||
const clipIdPage = new Set(page.filter(r => r.type === 'clip').map(r => r.id))
|
const clipIdPage = new Set(page.filter(r => r.type === 'clip').map(r => r.id))
|
||||||
@@ -767,6 +773,7 @@ export function publicLibraryPage(
|
|||||||
folders,
|
folders,
|
||||||
items: page,
|
items: page,
|
||||||
nextCursor,
|
nextCursor,
|
||||||
|
hasMore,
|
||||||
limit,
|
limit,
|
||||||
stills: catalog.stills
|
stills: catalog.stills
|
||||||
.filter(s => stillIds.has(s.id))
|
.filter(s => stillIds.has(s.id))
|
||||||
|
|||||||
Reference in New Issue
Block a user