From f9903ed76ead6730426e93f8802d6f2a03f51520 Mon Sep 17 00:00:00 2001 From: Towsty Date: Mon, 28 Sep 2026 20:40:45 -0500 Subject: [PATCH] Square Library modal thumbs and add stage previous/next. --- assets/css/display.css | 31 +++++++++ components/studio2/Bench.vue | 72 ++++++++++++++------ components/studio2/LibraryModal.vue | 102 ++++++++++++++++++++++++---- components/studio2/Mobile.vue | 32 +++++++++ 4 files changed, 203 insertions(+), 34 deletions(-) diff --git a/assets/css/display.css b/assets/css/display.css index 7ae9a5d..556e9f8 100644 --- a/assets/css/display.css +++ b/assets/css/display.css @@ -413,6 +413,37 @@ border-color: var(--amber); } +.stage-nav { + position: absolute; + top: 50%; + z-index: 4; + display: grid; + width: 44px; + height: 44px; + place-items: center; + border: 1px solid var(--line); + border-radius: 8px; + background: #000a; + color: var(--text); + font-size: 28px; + line-height: 1; + cursor: pointer; + transform: translateY(-50%); +} + +.stage-nav:hover { + border-color: var(--amber); + color: var(--amber); +} + +.stage-nav-prev { + left: 10px; +} + +.stage-nav-next { + right: 10px; +} + .filmstrip { display: none; } diff --git a/components/studio2/Bench.vue b/components/studio2/Bench.vue index 91b279d..f33e9a4 100644 --- a/components/studio2/Bench.vue +++ b/components/studio2/Bench.vue @@ -218,6 +218,21 @@ + + + @@ -440,7 +455,7 @@ const studioQueue=ref([]) const liveMusic=ref>({}) const negativeDraft=ref('') const libraryOpen=ref(false) -const previewForcedHidden=ref>(new Set()) +const eyeOpen=ref(!xaigen) const form=reactive({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(null) const promptCaret=ref({start:0,end:0}) @@ -798,30 +813,31 @@ function isHidden(asset:any){ const input=asset.role==='input' return input?classicHideInput.value:classicHideOutput.value } - if(xaigen) return previewHidden(asset,revealed.value,xaigen) - return previewForcedHidden.value.has(asset.id) + // Session chrome: eye gates the stage preview across neighbors. + return !eyeOpen.value } function pickerThumbHidden(asset:any){ if(pickerRevealAll.value)return false - return isHidden(asset) + if(!asset?.id)return false + if(isClassic.value)return classicHideInput.value + if(xaigen) return previewHidden(asset,revealed.value,xaigen) + return false } function slotHidden(id:string){ if(!xaigen || !id)return false if(isClassic.value)return classicHideInput.value return !revealed.value.has(id) } -function toggleReveal(id:string){ - if(!id)return - 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) - previewForcedHidden.value=next +function toggleReveal(_id?:string){ + eyeOpen.value=!eyeOpen.value +} +function stepFamily(delta:number){ + const list=family.value + if(list.length < 2)return + const i=list.findIndex((a:any)=>a.id===selected.value?.id) + const from=i < 0 ? 0 : i + const next=list[(from + delta + list.length) % list.length] + if(next) selectOutput(next) } function onLibraryUseAsPhoto(item:any){ if(item)selectOutput(item) @@ -839,14 +855,25 @@ function onLibraryDelete(item:any){ if(item)selectOutput(item) void deleteOutput() } -function onLibraryHotkey(e:KeyboardEvent){ - if(e.key!=='l' && e.key!=='L')return +function onStageHotkey(e:KeyboardEvent){ 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 + if(e.key==='l' || e.key==='L'){ + e.preventDefault() + libraryOpen.value=true + return + } + if(e.key==='ArrowLeft'){ + e.preventDefault() + stepFamily(-1) + return + } + if(e.key==='ArrowRight'){ + e.preventDefault() + stepFamily(1) + } } function closePicker(){ const key=picker.value @@ -1022,11 +1049,11 @@ 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('/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() - if(import.meta.client)window.addEventListener('keydown',onLibraryHotkey) + if(import.meta.client)window.addEventListener('keydown',onStageHotkey) }) onBeforeUnmount(()=>{ disposed=true;clearTimeout(timer);clearInterval(clock) - if(import.meta.client)window.removeEventListener('keydown',onLibraryHotkey) + if(import.meta.client)window.removeEventListener('keydown',onStageHotkey) }) function randomizeSeed(){form.settings.seedMode='random';form.settings.seed=randomSeed()} @@ -1078,6 +1105,7 @@ const mobileCtx=computed(()=>({ toggleReveal, chooseTask, selectOutput, + stepFamily, deleteOutput, queue, upload, diff --git a/components/studio2/LibraryModal.vue b/components/studio2/LibraryModal.vue index 12e1068..f7048d2 100644 --- a/components/studio2/LibraryModal.vue +++ b/components/studio2/LibraryModal.vue @@ -54,20 +54,20 @@
-
+
@@ -77,16 +77,15 @@ alt="" decoding="async" draggable="false" - class="h-full w-full object-cover" > - - + + {{ entry.kind === 'clip' ? 'Video' : entry.kind === 'track' ? 'Music' : 'Image' }}
-
-

{{ entry.name }}

-

{{ cardMeta(entry) }}

+
+

{{ entry.name }}

+

{{ cardMeta(entry) }}

@@ -435,11 +434,90 @@ onBeforeUnmount(() => { min-height: 0; height: 100%; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 10px; + gap: 8px; align-content: start; overflow: auto; padding: 14px 16px; } +.lib-modal-tile { + display: flex; + min-height: 120px; + flex-direction: column; + overflow: hidden; + border: 2px solid #2a2a2e; + border-radius: 8px; + background: #0b0b0d; + cursor: pointer; +} +.lib-modal-tile[data-active='true'] { + border-color: #f5a524; + box-shadow: 0 0 0 2px #f5a524; +} +.lib-modal-media { + position: relative; + aspect-ratio: 1 / 1; + min-height: 120px; + width: 100%; + overflow: hidden; + background: #18181b; +} +.lib-modal-media > img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} +.lib-modal-fallback { + display: block; + width: 100%; + height: 100%; + background: #27272a; +} +.lib-modal-track { + display: flex; + height: 100%; + width: 100%; + align-items: flex-end; + gap: 2px; + padding: 16px; +} +.lib-modal-bar { + flex: 1; + border-radius: 999px; + background: rgba(245, 165, 36, 0.7); +} +.lib-modal-kind { + position: absolute; + top: 8px; + left: 8px; + border-radius: 4px; + background: #000a; + padding: 2px 6px; + color: #e4e4e7; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.04em; +} +.lib-modal-caption { + padding: 6px 8px 8px; +} +.lib-modal-name { + margin: 0; + overflow: hidden; + color: #f4f4f5; + font-size: 12px; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} +.lib-modal-meta { + margin: 2px 0 0; + overflow: hidden; + color: #71717a; + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} .lib-modal-empty { grid-column: 1 / -1; color: #9a9588; diff --git a/components/studio2/Mobile.vue b/components/studio2/Mobile.vue index 9136456..515541b 100644 --- a/components/studio2/Mobile.vue +++ b/components/studio2/Mobile.vue @@ -65,6 +65,20 @@ + + @@ -440,6 +454,24 @@ watch(() => props.ctx.form.mode, () => { sheet.value = '' }) border-radius: 8px; background: #000a; } +.m-nav { + position: absolute; + top: 50%; + z-index: 4; + display: grid; + width: 48px; + height: 48px; + place-items: center; + border: 1px solid var(--line); + border-radius: 8px; + background: #000a; + color: var(--text); + font-size: 28px; + line-height: 1; + transform: translateY(-50%); +} +.m-nav-prev { left: 8px; } +.m-nav-next { right: 8px; } .m-dock { display: grid; grid-template-columns: repeat(4, 1fr) 120px;