Replace the under-stage filmstrip with a Library modal and stage eye.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -199,16 +199,24 @@
|
||||
|
||||
<div class="center-stack">
|
||||
<div class="viewer-frame">
|
||||
<button
|
||||
v-if="selected && xaigen"
|
||||
type="button"
|
||||
class="eye-viewer"
|
||||
:aria-pressed="!isHidden(selected)"
|
||||
:aria-label="isHidden(selected) ? 'Show preview' : 'Hide preview'"
|
||||
@click.stop="toggleReveal(selected.id)"
|
||||
>
|
||||
<img :src="isHidden(selected) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="16" height="16">
|
||||
</button>
|
||||
<div class="stage-chrome" aria-label="Stage controls">
|
||||
<button
|
||||
type="button"
|
||||
class="stage-library"
|
||||
:aria-expanded="libraryOpen ? 'true' : 'false'"
|
||||
@click="libraryOpen=true"
|
||||
>Library<span v-if="familyStillCount" class="stage-library-count">{{ familyStillCount }}</span></button>
|
||||
<button
|
||||
v-if="selected"
|
||||
type="button"
|
||||
class="eye-viewer"
|
||||
:aria-pressed="!isHidden(selected)"
|
||||
:aria-label="isHidden(selected) ? 'Show preview' : 'Hide preview'"
|
||||
@click.stop="toggleReveal(selected.id)"
|
||||
>
|
||||
<img :src="isHidden(selected) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="20" height="20">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-if="selected && isHidden(selected)">
|
||||
<p class="placeholder-still">{{ selected.kind==='track' ? 'Track loaded · preview hidden' : selected.role==='input' ? 'Still loaded · preview hidden' : 'Output hidden' }}</p>
|
||||
@@ -232,51 +240,11 @@
|
||||
<img :src="stillUrl(selected.id)" alt="Selected still">
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="placeholder-still">Select a still or clip in the strip</p>
|
||||
<p class="placeholder-still">Open Library to pick a still or clip</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="filmstrip" ref="filmstripEl" aria-label="Current family outputs">
|
||||
<div
|
||||
v-for="o in family"
|
||||
:key="o.id"
|
||||
class="tile"
|
||||
:data-active="selected?.id===o.id ? 'true' : undefined"
|
||||
:ref="el => setTileRef(o.id, el)"
|
||||
>
|
||||
<button type="button" class="pick" @click="selectOutput(o)">
|
||||
<span v-if="isHidden(o)" class="flex h-full items-center justify-center p-1 text-[10px] text-[var(--muted)]">Hidden</span>
|
||||
<span v-else-if="o.kind==='track'" class="flex h-full items-center justify-center text-[10px] text-[var(--amber)]">Audio</span>
|
||||
<img v-else :src="o.kind==='clip' ? `/api/library/clips/${o.id}/thumb` : stillUrl(o.id)" alt="">
|
||||
</button>
|
||||
<button
|
||||
v-if="xaigen"
|
||||
type="button"
|
||||
class="eye-tile"
|
||||
:aria-label="isHidden(o) ? 'Show tile' : 'Hide tile'"
|
||||
@click.stop="toggleReveal(o.id)"
|
||||
>
|
||||
<img :src="isHidden(o) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="12" height="12">
|
||||
</button>
|
||||
<button
|
||||
v-if="selected?.id===o.id"
|
||||
type="button"
|
||||
class="tile-trash"
|
||||
aria-label="Delete selected"
|
||||
title="Delete"
|
||||
@click.stop="deleteOutput"
|
||||
>🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selected && selected.kind!=='track'" class="strip-actions" aria-label="Selected still actions">
|
||||
<button type="button" @click="applyUseAsSource">Use as photo</button>
|
||||
<button type="button" @click="copySelectedPrompt">Copy prompt</button>
|
||||
<button type="button" @click="loadSelectedJob">Load</button>
|
||||
<button type="button" @click="deleteOutput">Delete</button>
|
||||
</div>
|
||||
|
||||
<div class="transport">
|
||||
<div v-if="selected?.kind==='clip'" class="transport">
|
||||
<span>{{ transportLabel }}</span>
|
||||
<div class="ops">
|
||||
<button type="button" :aria-pressed="viewerMuted" :aria-label="viewerMuted ? 'Unmute' : 'Mute'" @click="viewerMuted=!viewerMuted">{{ viewerMuted ? '🔇' : '🔊' }}</button>
|
||||
@@ -284,6 +252,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Studio2LibraryModal
|
||||
:open="libraryOpen"
|
||||
:assets="family"
|
||||
:selected="selected"
|
||||
:folder-id="form.folderId"
|
||||
:is-hidden="isHidden"
|
||||
@close="libraryOpen=false"
|
||||
@select="selectOutput"
|
||||
@use-as-photo="onLibraryUseAsPhoto"
|
||||
@copy-prompt="onLibraryCopyPrompt"
|
||||
@load="onLibraryLoad"
|
||||
@delete="onLibraryDelete"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<!-- RIGHT queue -->
|
||||
@@ -457,8 +439,8 @@ const library=ref<any>({folders:[],stills:[],clips:[],tracks:[]}),jobs=ref<any[]
|
||||
const studioQueue=ref<any[]>([])
|
||||
const liveMusic=ref<Record<string,any>>({})
|
||||
const negativeDraft=ref('')
|
||||
const filmstripEl=ref<HTMLElement|null>(null)
|
||||
const tileRefs=new Map<string,HTMLElement>()
|
||||
const libraryOpen=ref(false)
|
||||
const previewForcedHidden=ref<Set<string>>(new Set())
|
||||
const form=reactive<any>({mode:'generate',engine:'flux',enhancePrompt:readStoredEnhancePrompt(),turbo:readStoredTurbo(),imageStyles:{positive:[],negative:readStoredNegatives()},lockFace:true,lockOutfit:true,hideInput:xaigen,folderId:'',identityStillId:'',imageAId:'',imageBId:'',maskId:'',startClipId:'',endStillId:'',guides:[],photos:[],promptSections:sectionsFrom({}),settings:{aspect:'auto',fps:24,steps:24,cfg:4,seed:null,seedMode:'random',duration:5,turbo:true,loraStack:[]}})
|
||||
const promptArea=ref<HTMLTextAreaElement|null>(null)
|
||||
const promptCaret=ref({start:0,end:0})
|
||||
@@ -550,6 +532,7 @@ 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 inputStill=computed(()=>stills.value.find((s:any)=>s.id===form.imageAId))
|
||||
const family=computed(()=>folderAssets(library.value,jobs.value,form.folderId))
|
||||
const familyStillCount=computed(()=>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 sourceSize=computed(()=>measured[sourceAsset.value?.id] || sourceAsset.value)
|
||||
const resolvedSize=computed(()=>resolveSize(form.settings.aspect,sourceSize.value,undefined,form.engine))
|
||||
@@ -653,19 +636,6 @@ function addNegative(){
|
||||
}
|
||||
negativeDraft.value=''
|
||||
}
|
||||
function setTileRef(id:string,el:any){
|
||||
if(el)tileRefs.set(id,el as HTMLElement)
|
||||
else tileRefs.delete(id)
|
||||
}
|
||||
function scrollFilmstripToNewest(){
|
||||
nextTick(()=>{
|
||||
const id=selected.value?.id || family.value.at(-1)?.id
|
||||
const tile=id?tileRefs.get(id):null
|
||||
const strip=filmstripEl.value
|
||||
if(tile?.scrollIntoView)tile.scrollIntoView({inline:'end',block:'nearest',behavior:'smooth'})
|
||||
else if(strip)strip.scrollLeft=strip.scrollWidth
|
||||
})
|
||||
}
|
||||
function stillById(id:string){return id?stills.value.find((s:any)=>s.id===id)||null:null}
|
||||
/** Photos array is source of truth — never resurrect cards from leftover imageAId. */
|
||||
function syncPhotosBindings(){
|
||||
@@ -817,19 +787,19 @@ async function applyIncomingGenerationLoad(){
|
||||
const outId=outputs.at(-1)?.id
|
||||
const output=outId?family.value.find((a:any)=>a.id===outId):null
|
||||
applyHydratedJob(request,output||undefined)
|
||||
scrollFilmstripToNewest()
|
||||
}
|
||||
function focusQueueCard(card:any){
|
||||
focusedJobId.value=card.id
|
||||
if(card.job?.outputs?.length)focusJob(card.job)
|
||||
}
|
||||
function isHidden(asset:any){
|
||||
if(!xaigen || !asset?.id)return false
|
||||
if(!asset?.id)return false
|
||||
if(isClassic.value){
|
||||
const input=asset.role==='input'
|
||||
return input?classicHideInput.value:classicHideOutput.value
|
||||
}
|
||||
return previewHidden(asset,revealed.value,xaigen)
|
||||
if(xaigen) return previewHidden(asset,revealed.value,xaigen)
|
||||
return previewForcedHidden.value.has(asset.id)
|
||||
}
|
||||
function pickerThumbHidden(asset:any){
|
||||
if(pickerRevealAll.value)return false
|
||||
@@ -842,10 +812,41 @@ function slotHidden(id:string){
|
||||
}
|
||||
function toggleReveal(id:string){
|
||||
if(!id)return
|
||||
const next=new Set(revealed.value)
|
||||
if(xaigen){
|
||||
const next=new Set(revealed.value)
|
||||
if(next.has(id))next.delete(id);else next.add(id)
|
||||
revealed.value=next
|
||||
if(import.meta.client)localStorage.setItem(`studio2-revealed:${form.folderId}`,JSON.stringify([...next]))
|
||||
return
|
||||
}
|
||||
const next=new Set(previewForcedHidden.value)
|
||||
if(next.has(id))next.delete(id);else next.add(id)
|
||||
revealed.value=next
|
||||
if(import.meta.client)localStorage.setItem(`studio2-revealed:${form.folderId}`,JSON.stringify([...next]))
|
||||
previewForcedHidden.value=next
|
||||
}
|
||||
function onLibraryUseAsPhoto(item:any){
|
||||
if(item)selectOutput(item)
|
||||
applyUseAsSource()
|
||||
}
|
||||
function onLibraryCopyPrompt(item:any){
|
||||
if(item)selectOutput(item)
|
||||
void copySelectedPrompt()
|
||||
}
|
||||
function onLibraryLoad(item:any){
|
||||
if(item)selectOutput(item)
|
||||
loadSelectedJob()
|
||||
}
|
||||
function onLibraryDelete(item:any){
|
||||
if(item)selectOutput(item)
|
||||
void deleteOutput()
|
||||
}
|
||||
function onLibraryHotkey(e:KeyboardEvent){
|
||||
if(e.key!=='l' && e.key!=='L')return
|
||||
if(e.metaKey||e.ctrlKey||e.altKey)return
|
||||
const t=e.target as HTMLElement|null
|
||||
if(t && (t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.isContentEditable))return
|
||||
if(libraryOpen.value||picker.value)return
|
||||
e.preventDefault()
|
||||
libraryOpen.value=true
|
||||
}
|
||||
function closePicker(){
|
||||
const key=picker.value
|
||||
@@ -1005,8 +1006,6 @@ async function refresh(){
|
||||
let next=existingFresh || reconcileSelection(selected.value,family.value)
|
||||
if(form.mode!=='music' && next?.kind==='track') next=family.value.find((a:any)=>a.kind!=='track') || null
|
||||
selected.value=next
|
||||
if(fresh || existingFresh) scrollFilmstripToNewest()
|
||||
else if(selected.value?.id===family.value.at(-1)?.id) scrollFilmstripToNewest()
|
||||
}
|
||||
|
||||
watch(()=>form.folderId,id=>{selected.value=null;form.imageAId='';form.imageBId='';form.photos=[];form.startClipId='';loadRevealed(id);if(import.meta.client && form.engine!=='qwen21')form.identityStillId=localStorage.getItem(`studio2-hero:${id}`)||''},{flush:'sync'})
|
||||
@@ -1023,9 +1022,12 @@ onMounted(async()=>{
|
||||
if(asset){selected.value={...asset,kind:route.query.clip?'clip':route.query.track?'track':'still'};if(route.query.use==='1'&&selected.value.kind!=='track')restore(selected.value);if(xaigen)toggleReveal(asset.id)}
|
||||
if(route.query.task==='music')form.mode='music'
|
||||
const poll=async()=>{try{await refresh();const health=await $fetch<any>('/api/health');healthState.value=health}catch(e:any){healthState.value=null;error.value=e.data?.statusMessage||e.message}if(!disposed)timer=setTimeout(poll,3000)};void poll()
|
||||
scrollFilmstripToNewest()
|
||||
if(import.meta.client)window.addEventListener('keydown',onLibraryHotkey)
|
||||
})
|
||||
onBeforeUnmount(()=>{
|
||||
disposed=true;clearTimeout(timer);clearInterval(clock)
|
||||
if(import.meta.client)window.removeEventListener('keydown',onLibraryHotkey)
|
||||
})
|
||||
onBeforeUnmount(()=>{disposed=true;clearTimeout(timer);clearInterval(clock)})
|
||||
|
||||
function randomizeSeed(){form.settings.seedMode='random';form.settings.seed=randomSeed()}
|
||||
function setPicker(key:string){picker.value=key}
|
||||
@@ -1047,6 +1049,10 @@ const mobileCtx=computed(()=>({
|
||||
canClearFinished:canClearFinished.value,
|
||||
selected:selected.value,
|
||||
family:family.value,
|
||||
familyStillCount:familyStillCount.value,
|
||||
libraryOpen:libraryOpen.value,
|
||||
openLibrary:()=>{libraryOpen.value=true},
|
||||
closeLibrary:()=>{libraryOpen.value=false},
|
||||
sourceSlot:sourceSlot.value,
|
||||
video:video.value,
|
||||
submitting:submitting.value,
|
||||
@@ -1087,6 +1093,10 @@ const mobileCtx=computed(()=>({
|
||||
applyUseAsSource,
|
||||
copySelectedPrompt,
|
||||
loadSelectedJob,
|
||||
onLibraryUseAsPhoto,
|
||||
onLibraryCopyPrompt,
|
||||
onLibraryLoad,
|
||||
onLibraryDelete,
|
||||
randomizeSeed,
|
||||
setPicker,
|
||||
setBatchText:(v:string)=>{batchText.value=v},
|
||||
|
||||
Reference in New Issue
Block a user