From b5a4882a9ca79fef3112e5f7cba6b0a5ef9d94d0 Mon Sep 17 00:00:00 2001 From: Towsty Date: Fri, 11 Sep 2026 19:04:03 -0500 Subject: [PATCH] Keep Studio 2 family tiles tied to existing library files --- server/api/studio-2/clear-finished.post.ts | 15 +++++++++++++++ server/api/studio-2/jobs.get.ts | 2 +- server/api/studio-2/library.get.ts | 7 +++++++ shared/studio2/viewer.mjs | 16 +++++++--------- tests/studio2-canvas-seed.test.mjs | 2 +- 5 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 server/api/studio-2/clear-finished.post.ts create mode 100644 server/api/studio-2/library.get.ts diff --git a/server/api/studio-2/clear-finished.post.ts b/server/api/studio-2/clear-finished.post.ts new file mode 100644 index 0000000..91628bc --- /dev/null +++ b/server/api/studio-2/clear-finished.post.ts @@ -0,0 +1,15 @@ +import { records, saveRecord } from '../../utils/studio2/store'; +import { listStudioJobs } from '../../utils/studioQueue'; +export default defineEventHandler(event => { + const { owner } = assertLibraryOwner(event), queue=listStudioJobs(owner); + let cleared=0; + for (const r of records(owner)) { + try { assertFolderAccess(event,r.request.folderId); } catch { continue; } + const status=queue.find(q=>q.id===r.queueId)?.status; + if (['complete','failed','cancelled'].includes(r.state) || ['cancelled','error'].includes(status || '')) { + r.dismissed=true; saveRecord(r); cleared++; + } + } + // Retain internal manifests for cleanup retries; remove these records from the visible jobs list. + return {cleared}; +}); diff --git a/server/api/studio-2/jobs.get.ts b/server/api/studio-2/jobs.get.ts index 00abf7e..78d99ce 100644 --- a/server/api/studio-2/jobs.get.ts +++ b/server/api/studio-2/jobs.get.ts @@ -1,6 +1,6 @@ import { records } from '../../utils/studio2/store'; import { listStudioJobs } from '../../utils/studioQueue'; -export default defineEventHandler(event => { const { owner } = assertLibraryOwner(event); const rows = listStudioJobs(owner); return records(owner).filter(r => { try { +export default defineEventHandler(event => { const { owner } = assertLibraryOwner(event); const rows = listStudioJobs(owner); return records(owner).filter(r => !r.dismissed).filter(r => { try { assertFolderAccess(event, r.request.folderId); return true; } diff --git a/server/api/studio-2/library.get.ts b/server/api/studio-2/library.get.ts new file mode 100644 index 0000000..607ceb3 --- /dev/null +++ b/server/api/studio-2/library.get.ts @@ -0,0 +1,7 @@ +import { existsSync } from 'node:fs'; +import { publicLibrary, stillPath, clipVideoPath } from '../../utils/library'; +export default defineEventHandler(event => { + const { owner } = assertLibraryOwner(event); + const library = publicLibrary(event); + return {...library, stills:library.stills.filter(s=>existsSync(stillPath(owner,s.id))), clips:library.clips.filter(c=>existsSync(clipVideoPath(owner,c.id)))}; +}); diff --git a/shared/studio2/viewer.mjs b/shared/studio2/viewer.mjs index 8a80765..80b98a1 100644 --- a/shared/studio2/viewer.mjs +++ b/shared/studio2/viewer.mjs @@ -1,14 +1,12 @@ export function folderAssets(library, jobs, folderId) { - const assets=new Map() - for (const kind of ['still','clip']) for (const asset of library[kind==='still'?'stills':'clips'] || []) { - if (asset.folderId===folderId) assets.set(asset.id,{...asset,kind}) - } - for (const job of jobs) if (job.request.folderId===folderId) for (const asset of job.outputs || []) assets.set(asset.id,{...assets.get(asset.id),...asset}) - return [...assets.values()].sort((a,b)=>(a.createdAt || a.studio2?.savedAt || 0)-(b.createdAt || b.studio2?.savedAt || 0)) + const metadata=new Map(jobs.flatMap(j=>j.outputs || []).map(a=>[a.id,a.studio2])) + return ['still','clip'].flatMap(kind=>(library[kind==='still'?'stills':'clips'] || []).filter(a=>a.id && a.folderId===folderId && a.hasVideo!==false && a.available!==false).map(a=>({...a,kind,studio2:a.studio2 || metadata.get(a.id)}))).sort((a,b)=>(a.createdAt || a.studio2?.savedAt || 0)-(b.createdAt || b.studio2?.savedAt || 0)) } export function newestOutput(jobs, seen, folderId) { - const fresh=jobs.filter(j=>j.request.folderId===folderId).flatMap(j=>j.outputs || []).filter(a=>!seen.has(a.id)) - return fresh.sort((a,b)=>(a.studio2?.savedAt || 0)-(b.studio2?.savedAt || 0)).at(-1) || null + return jobs.filter(j=>j.request.folderId===folderId).flatMap(j=>j.outputs || []).filter(a=>!seen.has(a.id)).sort((a,b)=>(a.studio2?.savedAt || 0)-(b.studio2?.savedAt || 0)).at(-1) || null } +export function reconcileSelection(selected, assets) { return assets.find(a=>a.id===selected?.id) || assets.at(-1) || null } +export function activeJobs(jobs) { return jobs.filter(j=>['waking','submitting','rendering','saving'].includes(j.state)) } +export function previewHidden(asset, xaigen, hideInput, hideOutput) { return !!xaigen && (asset?.role==='input' ? !!hideInput : !!hideOutput) } export function selectResult(state, asset) { return {...state,selected:asset} } -export function assetLabel(asset) { return asset?.name || asset?.originalName || (asset ? `Still ${asset.id.slice(0,8)}` : 'None') } +export function assetLabel(asset) { return asset?.name || asset?.originalName || asset?.filename || (asset ? (asset.kind==='clip'?'Clip ':'Still ')+asset.id.slice(0,8) : 'None') } diff --git a/tests/studio2-canvas-seed.test.mjs b/tests/studio2-canvas-seed.test.mjs index 2b1fa53..550f145 100644 --- a/tests/studio2-canvas-seed.test.mjs +++ b/tests/studio2-canvas-seed.test.mjs @@ -7,7 +7,7 @@ import {applyResolvedImageSize} from '../shared/studio2/graphs.mjs' const base={mode:'generate',engine:'flux',folderId:'f',prompt:'Walk'} test('completed outputs join all folder assets and select only the output canvas',()=>{ const source={id:'source',folderId:'f'},output={id:'result',kind:'still',studio2:{savedAt:2}} - const jobs=[{request:{folderId:'f'},outputs:[output]}],library={stills:[source,{id:'other',folderId:'f'}],clips:[{id:'video',folderId:'f'}]} + const jobs=[{request:{folderId:'f'},outputs:[output]}],library={stills:[source,{id:'other',folderId:'f'},{id:'result',folderId:'f'}],clips:[{id:'video',folderId:'f'}]} const strip=folderAssets(library,jobs,'f');assert.equal(strip.length,4) const state=selectResult({imageAId:'source',startClipId:'existing'},newestOutput(jobs,new Set(),'f')) assert.equal(state.selected.id,'result');assert.equal(state.imageAId,'source');assert.equal(state.startClipId,'existing')