Compare commits
2
Commits
e06afe4a17
...
5b0d7370ed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b0d7370ed | ||
|
|
7242d0faef |
@@ -30,11 +30,13 @@ Mobile is a **viewer-first phone app** with sheets for the inspector and the que
|
|||||||
├───────────────────────────────────────────────────────────┤
|
├───────────────────────────────────────────────────────────┤
|
||||||
│ │
|
│ │
|
||||||
│ STAGE (contain, full remaining height) │
|
│ STAGE (contain, full remaining height) │
|
||||||
│ │
|
│ Library (stage chrome) · eye ≥48px top-right │
|
||||||
├ filmstrip scroll-x, tiles 72px ───────────────────────────┤
|
|
||||||
├ [Slots] [Prompt] [Look] [Gen] Generate (amber, 48px) ─┤
|
├ [Slots] [Prompt] [Look] [Gen] Generate (amber, 48px) ─┤
|
||||||
└───────────────────────────────────────────────────────────┘
|
└───────────────────────────────────────────────────────────┘
|
||||||
```
|
|
||||||
|
Library is a **stage button** that opens the Library modal/sheet (same tiles,
|
||||||
|
filters, Use as photo / Copy / Load / Delete). The under-stage filmstrip row
|
||||||
|
is revoked — recents live in the modal (newest first).```
|
||||||
|
|
||||||
Bottom **dock** is 4 tabs that open sheets, plus Generate always visible:
|
Bottom **dock** is 4 tabs that open sheets, plus Generate always visible:
|
||||||
|
|
||||||
|
|||||||
+49
-37
@@ -332,7 +332,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stage: fills center column above filmstrip + transport */
|
/* Stage: fills center column above transport */
|
||||||
.viewer-frame {
|
.viewer-frame {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -356,31 +356,65 @@
|
|||||||
object-position: center;
|
object-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eye-viewer {
|
.stage-chrome {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
|
left: 10px;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-chrome > * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-library {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 44px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #000a;
|
||||||
|
padding: 0 14px;
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-library:hover {
|
||||||
|
border-color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-library-count {
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eye-viewer {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 28px;
|
width: 44px;
|
||||||
height: 28px;
|
height: 44px;
|
||||||
|
flex-shrink: 0;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background: #000a;
|
background: #000a;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.eye-viewer:hover {
|
||||||
|
border-color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
.filmstrip {
|
.filmstrip {
|
||||||
display: flex;
|
display: none;
|
||||||
flex-shrink: 0;
|
|
||||||
gap: 8px;
|
|
||||||
height: 80px;
|
|
||||||
align-items: center;
|
|
||||||
overflow-x: auto;
|
|
||||||
border-top: 1px solid var(--line);
|
|
||||||
padding: 8px 10px;
|
|
||||||
background: var(--panel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile {
|
.tile {
|
||||||
@@ -396,7 +430,6 @@
|
|||||||
|
|
||||||
@media (min-width: 1440px) {
|
@media (min-width: 1440px) {
|
||||||
.tile { width: 120px; height: 80px; }
|
.tile { width: 120px; height: 80px; }
|
||||||
.filmstrip { height: 96px; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile[data-active='true'] {
|
.tile[data-active='true'] {
|
||||||
@@ -438,28 +471,7 @@
|
|||||||
.tile .tile-trash { right: 22px; }
|
.tile .tile-trash { right: 22px; }
|
||||||
|
|
||||||
.strip-actions {
|
.strip-actions {
|
||||||
display: flex;
|
display: none;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-top: 1px solid var(--line);
|
|
||||||
padding: 8px 10px;
|
|
||||||
background: var(--panel);
|
|
||||||
}
|
|
||||||
|
|
||||||
.strip-actions button {
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text);
|
|
||||||
padding: 4px 10px;
|
|
||||||
font-size: var(--type-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strip-actions button:hover {
|
|
||||||
border-color: var(--amber);
|
|
||||||
color: var(--amber);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.neg-add-row {
|
.neg-add-row {
|
||||||
|
|||||||
@@ -199,16 +199,24 @@
|
|||||||
|
|
||||||
<div class="center-stack">
|
<div class="center-stack">
|
||||||
<div class="viewer-frame">
|
<div class="viewer-frame">
|
||||||
<button
|
<div class="stage-chrome" aria-label="Stage controls">
|
||||||
v-if="selected && xaigen"
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="eye-viewer"
|
class="stage-library"
|
||||||
:aria-pressed="!isHidden(selected)"
|
:aria-expanded="libraryOpen ? 'true' : 'false'"
|
||||||
:aria-label="isHidden(selected) ? 'Show preview' : 'Hide preview'"
|
@click="libraryOpen=true"
|
||||||
@click.stop="toggleReveal(selected.id)"
|
>Library<span v-if="familyStillCount" class="stage-library-count">{{ familyStillCount }}</span></button>
|
||||||
>
|
<button
|
||||||
<img :src="isHidden(selected) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="16" height="16">
|
v-if="selected"
|
||||||
</button>
|
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)">
|
<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>
|
<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">
|
<img :src="stillUrl(selected.id)" alt="Selected still">
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filmstrip" ref="filmstripEl" aria-label="Current family outputs">
|
<div v-if="selected?.kind==='clip'" class="transport">
|
||||||
<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">
|
|
||||||
<span>{{ transportLabel }}</span>
|
<span>{{ transportLabel }}</span>
|
||||||
<div class="ops">
|
<div class="ops">
|
||||||
<button type="button" :aria-pressed="viewerMuted" :aria-label="viewerMuted ? 'Unmute' : 'Mute'" @click="viewerMuted=!viewerMuted">{{ viewerMuted ? '🔇' : '🔊' }}</button>
|
<button type="button" :aria-pressed="viewerMuted" :aria-label="viewerMuted ? 'Unmute' : 'Mute'" @click="viewerMuted=!viewerMuted">{{ viewerMuted ? '🔇' : '🔊' }}</button>
|
||||||
@@ -284,6 +252,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
|
|
||||||
<!-- RIGHT queue -->
|
<!-- RIGHT queue -->
|
||||||
@@ -457,8 +439,8 @@ const library=ref<any>({folders:[],stills:[],clips:[],tracks:[]}),jobs=ref<any[]
|
|||||||
const studioQueue=ref<any[]>([])
|
const studioQueue=ref<any[]>([])
|
||||||
const liveMusic=ref<Record<string,any>>({})
|
const liveMusic=ref<Record<string,any>>({})
|
||||||
const negativeDraft=ref('')
|
const negativeDraft=ref('')
|
||||||
const filmstripEl=ref<HTMLElement|null>(null)
|
const libraryOpen=ref(false)
|
||||||
const tileRefs=new Map<string,HTMLElement>()
|
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 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 promptArea=ref<HTMLTextAreaElement|null>(null)
|
||||||
const promptCaret=ref({start:0,end:0})
|
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 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 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))
|
||||||
@@ -653,19 +636,6 @@ function addNegative(){
|
|||||||
}
|
}
|
||||||
negativeDraft.value=''
|
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}
|
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. */
|
/** Photos array is source of truth — never resurrect cards from leftover imageAId. */
|
||||||
function syncPhotosBindings(){
|
function syncPhotosBindings(){
|
||||||
@@ -817,19 +787,19 @@ async function applyIncomingGenerationLoad(){
|
|||||||
const outId=outputs.at(-1)?.id
|
const outId=outputs.at(-1)?.id
|
||||||
const output=outId?family.value.find((a:any)=>a.id===outId):null
|
const output=outId?family.value.find((a:any)=>a.id===outId):null
|
||||||
applyHydratedJob(request,output||undefined)
|
applyHydratedJob(request,output||undefined)
|
||||||
scrollFilmstripToNewest()
|
|
||||||
}
|
}
|
||||||
function focusQueueCard(card:any){
|
function focusQueueCard(card:any){
|
||||||
focusedJobId.value=card.id
|
focusedJobId.value=card.id
|
||||||
if(card.job?.outputs?.length)focusJob(card.job)
|
if(card.job?.outputs?.length)focusJob(card.job)
|
||||||
}
|
}
|
||||||
function isHidden(asset:any){
|
function isHidden(asset:any){
|
||||||
if(!xaigen || !asset?.id)return false
|
if(!asset?.id)return false
|
||||||
if(isClassic.value){
|
if(isClassic.value){
|
||||||
const input=asset.role==='input'
|
const input=asset.role==='input'
|
||||||
return input?classicHideInput.value:classicHideOutput.value
|
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){
|
function pickerThumbHidden(asset:any){
|
||||||
if(pickerRevealAll.value)return false
|
if(pickerRevealAll.value)return false
|
||||||
@@ -842,10 +812,41 @@ function slotHidden(id:string){
|
|||||||
}
|
}
|
||||||
function toggleReveal(id:string){
|
function toggleReveal(id:string){
|
||||||
if(!id)return
|
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)
|
if(next.has(id))next.delete(id);else next.add(id)
|
||||||
revealed.value=next
|
previewForcedHidden.value=next
|
||||||
if(import.meta.client)localStorage.setItem(`studio2-revealed:${form.folderId}`,JSON.stringify([...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(){
|
function closePicker(){
|
||||||
const key=picker.value
|
const key=picker.value
|
||||||
@@ -1005,8 +1006,6 @@ async function refresh(){
|
|||||||
let next=existingFresh || reconcileSelection(selected.value,family.value)
|
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
|
if(form.mode!=='music' && next?.kind==='track') next=family.value.find((a:any)=>a.kind!=='track') || null
|
||||||
selected.value=next
|
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'})
|
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(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'
|
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()
|
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 randomizeSeed(){form.settings.seedMode='random';form.settings.seed=randomSeed()}
|
||||||
function setPicker(key:string){picker.value=key}
|
function setPicker(key:string){picker.value=key}
|
||||||
@@ -1047,6 +1049,10 @@ const mobileCtx=computed(()=>({
|
|||||||
canClearFinished:canClearFinished.value,
|
canClearFinished:canClearFinished.value,
|
||||||
selected:selected.value,
|
selected:selected.value,
|
||||||
family:family.value,
|
family:family.value,
|
||||||
|
familyStillCount:familyStillCount.value,
|
||||||
|
libraryOpen:libraryOpen.value,
|
||||||
|
openLibrary:()=>{libraryOpen.value=true},
|
||||||
|
closeLibrary:()=>{libraryOpen.value=false},
|
||||||
sourceSlot:sourceSlot.value,
|
sourceSlot:sourceSlot.value,
|
||||||
video:video.value,
|
video:video.value,
|
||||||
submitting:submitting.value,
|
submitting:submitting.value,
|
||||||
@@ -1087,6 +1093,10 @@ const mobileCtx=computed(()=>({
|
|||||||
applyUseAsSource,
|
applyUseAsSource,
|
||||||
copySelectedPrompt,
|
copySelectedPrompt,
|
||||||
loadSelectedJob,
|
loadSelectedJob,
|
||||||
|
onLibraryUseAsPhoto,
|
||||||
|
onLibraryCopyPrompt,
|
||||||
|
onLibraryLoad,
|
||||||
|
onLibraryDelete,
|
||||||
randomizeSeed,
|
randomizeSeed,
|
||||||
setPicker,
|
setPicker,
|
||||||
setBatchText:(v:string)=>{batchText.value=v},
|
setBatchText:(v:string)=>{batchText.value=v},
|
||||||
|
|||||||
@@ -5,6 +5,18 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props=defineProps<{metadata?:any}>()
|
const props=defineProps<{metadata?:any}>()
|
||||||
|
function promptRows(m:any){
|
||||||
|
const used=String(m.prompt || m.compiledPrompt || '').trim()
|
||||||
|
const typed=String(m.promptRaw || '').trim()
|
||||||
|
if(typed && typed!==used){
|
||||||
|
return [
|
||||||
|
{label:'Typed',value:typed},
|
||||||
|
...(used?[{label:'Used',value:used}]:[])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if(used) return [{label:'Prompt',value:used}]
|
||||||
|
return []
|
||||||
|
}
|
||||||
const rows=computed(()=>{const m=props.metadata||{},s=m.settings||{};const time=(v:any)=>v?new Date(v).toLocaleString():null;return [
|
const rows=computed(()=>{const m=props.metadata||{},s=m.settings||{};const time=(v:any)=>v?new Date(v).toLocaleString():null;return [
|
||||||
{label:'Kind / mode',value:[m.kind,m.mode].filter(Boolean).join(' / ')},
|
{label:'Kind / mode',value:[m.kind,m.mode].filter(Boolean).join(' / ')},
|
||||||
{label:'Engine',value:m.engine},{label:'Graph ID',value:m.graphId},
|
{label:'Engine',value:m.engine},{label:'Graph ID',value:m.graphId},
|
||||||
@@ -19,8 +31,7 @@ const rows=computed(()=>{const m=props.metadata||{},s=m.settings||{};const time=
|
|||||||
{label:'Start frame',value:m.startFrameSource?JSON.stringify(m.startFrameSource):'None'},
|
{label:'Start frame',value:m.startFrameSource?JSON.stringify(m.startFrameSource):'None'},
|
||||||
{label:'Handoff source',value:m.handoffSource || 'None'},{label:'Handoff fitted (resampled)',value:m.fitted==null?'Not recorded':m.fitted?'Yes':'No'},
|
{label:'Handoff source',value:m.handoffSource || 'None'},{label:'Handoff fitted (resampled)',value:m.fitted==null?'Not recorded':m.fitted?'Yes':'No'},
|
||||||
{label:'End still',value:m.endStillId||'None'},{label:'Guide stills',value:m.guides?.length?JSON.stringify(m.guides):'None'},
|
{label:'End still',value:m.endStillId||'None'},{label:'Guide stills',value:m.guides?.length?JSON.stringify(m.guides):'None'},
|
||||||
{label:'Prompt',value:m.prompt || m.compiledPrompt},
|
...promptRows(m),
|
||||||
{label:'Typed',value:m.promptRaw || null},
|
|
||||||
{label:'Task',value:m.task || m.mode || null},
|
{label:'Task',value:m.task || m.mode || null},
|
||||||
{label:'Enhance prompt',value:m.enhancePrompt==null?null:m.enhancePrompt?'On':'Off'},
|
{label:'Enhance prompt',value:m.enhancePrompt==null?null:m.enhancePrompt?'On':'Off'},
|
||||||
{label:'Enhance',value:m.enhance?.refused?'Enhance skipped (model refused) — used your prompt.':m.enhance?.skippedAsDescribe?'Enhance skipped (describe rewrite) — keep stanza + typed only.':m.enhance?.used?'Applied':m.enhance?.parse_ok==null?null:m.enhance.parse_ok?'Applied':'Parse incomplete'},
|
{label:'Enhance',value:m.enhance?.refused?'Enhance skipped (model refused) — used your prompt.':m.enhance?.skippedAsDescribe?'Enhance skipped (describe rewrite) — keep stanza + typed only.':m.enhance?.used?'Applied':m.enhance?.parse_ok==null?null:m.enhance.parse_ok?'Applied':'Parse incomplete'},
|
||||||
|
|||||||
@@ -0,0 +1,398 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="open"
|
||||||
|
class="lib-modal-root"
|
||||||
|
role="presentation"
|
||||||
|
@keydown.esc.prevent="close"
|
||||||
|
>
|
||||||
|
<div class="lib-modal-backdrop" @click="close" />
|
||||||
|
<section
|
||||||
|
ref="dialogEl"
|
||||||
|
class="lib-modal"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="lib-modal-title"
|
||||||
|
tabindex="-1"
|
||||||
|
@keydown.tab="onTab"
|
||||||
|
>
|
||||||
|
<header class="lib-modal-head">
|
||||||
|
<h2 id="lib-modal-title">Library</h2>
|
||||||
|
<div class="lib-modal-head-actions">
|
||||||
|
<NuxtLink class="lib-modal-full" :to="{ path: '/library', query: folderId ? { folder: folderId } : {} }" @click="close">Full library</NuxtLink>
|
||||||
|
<button type="button" class="lib-modal-x" aria-label="Close" @click="close">×</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="lib-modal-toolbar">
|
||||||
|
<input
|
||||||
|
v-model="query"
|
||||||
|
type="search"
|
||||||
|
class="lib-modal-search"
|
||||||
|
placeholder="Name or prompt"
|
||||||
|
aria-label="Search library"
|
||||||
|
>
|
||||||
|
<div class="lib-modal-filters" role="group" aria-label="Type">
|
||||||
|
<button
|
||||||
|
v-for="opt in typeOptions"
|
||||||
|
:key="opt.id"
|
||||||
|
type="button"
|
||||||
|
class="lib-modal-chip"
|
||||||
|
:data-on="filter === opt.id ? 'true' : undefined"
|
||||||
|
@click="filter = opt.id"
|
||||||
|
>{{ opt.label }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lib-modal-body">
|
||||||
|
<div class="lib-modal-grid" aria-label="Folder assets">
|
||||||
|
<button
|
||||||
|
v-for="item in filtered"
|
||||||
|
:key="item.id"
|
||||||
|
type="button"
|
||||||
|
class="lib-modal-tile"
|
||||||
|
:data-active="selectedId === item.id ? 'true' : undefined"
|
||||||
|
@click="$emit('select', item)"
|
||||||
|
>
|
||||||
|
<span v-if="isHidden?.(item)" class="lib-modal-hid">Hidden</span>
|
||||||
|
<span v-else-if="item.kind === 'track'" class="lib-modal-hid">Audio</span>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
:src="item.kind === 'clip' ? `/api/library/clips/${item.id}/thumb` : stillUrl(item.id)"
|
||||||
|
alt=""
|
||||||
|
>
|
||||||
|
<span class="lib-modal-caption">{{ label(item) }}</span>
|
||||||
|
</button>
|
||||||
|
<p v-if="!filtered.length" class="lib-modal-empty">No items in this folder yet.</p>
|
||||||
|
</div>
|
||||||
|
<aside v-if="selected" class="lib-modal-detail">
|
||||||
|
<p class="lib-modal-detail-name">{{ label(selected) }}</p>
|
||||||
|
<div v-if="selected.kind !== 'track'" class="lib-modal-actions">
|
||||||
|
<button type="button" @click="$emit('use-as-photo', selected)">Use as photo</button>
|
||||||
|
<button type="button" @click="$emit('copy-prompt', selected)">Copy prompt</button>
|
||||||
|
<button type="button" @click="$emit('load', selected)">Load</button>
|
||||||
|
<button type="button" class="danger" @click="$emit('delete', selected)">Delete</button>
|
||||||
|
<button type="button" class="primary" @click="useAndClose">Use and close</button>
|
||||||
|
</div>
|
||||||
|
<LibrarySettingsDetails :rows="detailRows" empty="No generator settings were saved with this item." />
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { clipSettingsRows, stillSettingsRows, trackSettingsRows } from '~/utils/libraryGeneration'
|
||||||
|
import { assetLabel } from '~/shared/studio2/viewer.mjs'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
open: boolean
|
||||||
|
assets: any[]
|
||||||
|
selected?: any
|
||||||
|
folderId?: string
|
||||||
|
isHidden?: (asset: any) => boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
select: [asset: any]
|
||||||
|
'use-as-photo': [asset: any]
|
||||||
|
'copy-prompt': [asset: any]
|
||||||
|
load: [asset: any]
|
||||||
|
delete: [asset: any]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const query = ref('')
|
||||||
|
const filter = ref<'all' | 'images' | 'videos' | 'music'>('all')
|
||||||
|
const dialogEl = ref<HTMLElement | null>(null)
|
||||||
|
const typeOptions = [
|
||||||
|
{ id: 'all' as const, label: 'All' },
|
||||||
|
{ id: 'images' as const, label: 'Images' },
|
||||||
|
{ id: 'videos' as const, label: 'Videos' },
|
||||||
|
{ id: 'music' as const, label: 'Music' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const selectedId = computed(() => props.selected?.id || '')
|
||||||
|
const stillUrl = (id: string) => `/api/library/stills/${id}`
|
||||||
|
const label = (item: any) => assetLabel(item)
|
||||||
|
|
||||||
|
const filtered = computed(() => {
|
||||||
|
const q = query.value.trim().toLowerCase()
|
||||||
|
let list = [...(props.assets || [])]
|
||||||
|
if (filter.value === 'images') list = list.filter(a => a.kind === 'still')
|
||||||
|
if (filter.value === 'videos') list = list.filter(a => a.kind === 'clip')
|
||||||
|
if (filter.value === 'music') list = list.filter(a => a.kind === 'track')
|
||||||
|
if (q) {
|
||||||
|
list = list.filter(a => {
|
||||||
|
const hay = [
|
||||||
|
a.name, a.filename, a.prompt, a.promptRaw,
|
||||||
|
a.studio2?.prompt, a.studio2?.promptRaw, a.studio2?.compiledPrompt,
|
||||||
|
a.studio2?.promptSections?.action
|
||||||
|
].filter(Boolean).join(' ').toLowerCase()
|
||||||
|
return hay.includes(q)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Newest first
|
||||||
|
return list.sort((a, b) => (b.createdAt || b.studio2?.savedAt || 0) - (a.createdAt || a.studio2?.savedAt || 0))
|
||||||
|
})
|
||||||
|
|
||||||
|
const detailRows = computed(() => {
|
||||||
|
const item = props.selected
|
||||||
|
if (!item) return []
|
||||||
|
if (item.kind === 'clip') return clipSettingsRows(item)
|
||||||
|
if (item.kind === 'track') return trackSettingsRows(item)
|
||||||
|
return stillSettingsRows(item)
|
||||||
|
})
|
||||||
|
|
||||||
|
function close() { emit('close') }
|
||||||
|
|
||||||
|
function useAndClose() {
|
||||||
|
if (props.selected) emit('select', props.selected)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTab(e: KeyboardEvent) {
|
||||||
|
const root = dialogEl.value
|
||||||
|
if (!root) return
|
||||||
|
const focusable = [...root.querySelectorAll<HTMLElement>(
|
||||||
|
'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||||
|
)].filter(el => el.offsetParent !== null || el === document.activeElement)
|
||||||
|
if (!focusable.length) return
|
||||||
|
const first = focusable[0]
|
||||||
|
const last = focusable[focusable.length - 1]
|
||||||
|
if (e.shiftKey && document.activeElement === first) {
|
||||||
|
e.preventDefault()
|
||||||
|
last.focus()
|
||||||
|
} else if (!e.shiftKey && document.activeElement === last) {
|
||||||
|
e.preventDefault()
|
||||||
|
first.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.open, (on) => {
|
||||||
|
if (!import.meta.client) return
|
||||||
|
if (on) {
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
nextTick(() => dialogEl.value?.focus())
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (import.meta.client) document.body.style.overflow = ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.lib-modal-root {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 80;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.lib-modal-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: #000a;
|
||||||
|
}
|
||||||
|
.lib-modal {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
width: min(1100px, 100%);
|
||||||
|
max-height: min(92vh, 900px);
|
||||||
|
margin: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #2a2a2e;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #141416;
|
||||||
|
color: #f3f1ea;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.lib-modal-root { align-items: flex-end; }
|
||||||
|
.lib-modal {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 100dvh;
|
||||||
|
height: 100dvh;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.lib-modal-head {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
border-bottom: 1px solid #2a2a2e;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
.lib-modal-head h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.lib-modal-head-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.lib-modal-full {
|
||||||
|
color: #f5a524;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.lib-modal-x {
|
||||||
|
display: grid;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid #2a2a2e;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.lib-modal-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #2a2a2e;
|
||||||
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
|
.lib-modal-search {
|
||||||
|
min-width: 12rem;
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #2a2a2e;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0b0b0d;
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
.lib-modal-filters { display: inline-flex; gap: 4px; }
|
||||||
|
.lib-modal-chip {
|
||||||
|
border: 1px solid #2a2a2e;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
color: #9a9588;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.lib-modal-chip[data-on='true'] {
|
||||||
|
border-color: #f5a524;
|
||||||
|
background: #f5a52422;
|
||||||
|
color: #f5a524;
|
||||||
|
}
|
||||||
|
.lib-modal-body {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.lib-modal-body { flex-direction: column; }
|
||||||
|
}
|
||||||
|
.lib-modal-grid {
|
||||||
|
display: grid;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
align-content: start;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
.lib-modal-tile {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #222;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.lib-modal-tile[data-active='true'] { border-color: #f5a524; }
|
||||||
|
.lib-modal-tile img {
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
.lib-modal-hid {
|
||||||
|
display: grid;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
place-items: center;
|
||||||
|
color: #9a9588;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.lib-modal-caption {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #9a9588;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.lib-modal-empty {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
color: #9a9588;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.lib-modal-detail {
|
||||||
|
flex: 0 0 min(320px, 38%);
|
||||||
|
min-width: 0;
|
||||||
|
overflow: auto;
|
||||||
|
border-left: 1px solid #2a2a2e;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.lib-modal-detail {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-height: 40%;
|
||||||
|
border-left: 0;
|
||||||
|
border-top: 1px solid #2a2a2e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.lib-modal-detail-name {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.lib-modal-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.lib-modal-actions button {
|
||||||
|
border: 1px solid #2a2a2e;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.lib-modal-actions button.primary {
|
||||||
|
border-color: #f5a524;
|
||||||
|
background: #f5a524;
|
||||||
|
color: #1a1204;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.lib-modal-actions button.danger { color: #fca5a5; }
|
||||||
|
</style>
|
||||||
+62
-104
@@ -48,15 +48,23 @@
|
|||||||
<div v-else class="m-chips"><span class="m-chip on">YuE2</span></div>
|
<div v-else class="m-chips"><span class="m-chip on">YuE2</span></div>
|
||||||
|
|
||||||
<div class="m-stage">
|
<div class="m-stage">
|
||||||
<button
|
<div class="m-stage-chrome" aria-label="Stage controls">
|
||||||
v-if="ctx.selected && ctx.xaigen"
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="m-eye"
|
class="m-library"
|
||||||
:aria-label="ctx.isHidden(ctx.selected) ? 'Show preview' : 'Hide preview'"
|
:aria-expanded="ctx.libraryOpen ? 'true' : 'false'"
|
||||||
@click.stop="ctx.toggleReveal(ctx.selected.id)"
|
@click="ctx.openLibrary()"
|
||||||
>
|
>Library<span v-if="ctx.familyStillCount" class="m-library-count">{{ ctx.familyStillCount }}</span></button>
|
||||||
<img :src="ctx.isHidden(ctx.selected) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="16" height="16">
|
<button
|
||||||
</button>
|
v-if="ctx.selected"
|
||||||
|
type="button"
|
||||||
|
class="m-eye"
|
||||||
|
:aria-label="ctx.isHidden(ctx.selected) ? 'Show preview' : 'Hide preview'"
|
||||||
|
@click.stop="ctx.toggleReveal(ctx.selected.id)"
|
||||||
|
>
|
||||||
|
<img :src="ctx.isHidden(ctx.selected) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="20" height="20">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<template v-if="ctx.selected && ctx.isHidden(ctx.selected)">
|
<template v-if="ctx.selected && ctx.isHidden(ctx.selected)">
|
||||||
<p class="m-muted">{{ ctx.selected.kind==='track' ? 'Track loaded · preview hidden' : 'Preview hidden' }}</p>
|
<p class="m-muted">{{ ctx.selected.kind==='track' ? 'Track loaded · preview hidden' : 'Preview hidden' }}</p>
|
||||||
</template>
|
</template>
|
||||||
@@ -73,46 +81,22 @@
|
|||||||
playsinline
|
playsinline
|
||||||
/>
|
/>
|
||||||
<img v-else-if="ctx.selected" :src="ctx.stillUrl(ctx.selected.id)" alt="Selected still">
|
<img v-else-if="ctx.selected" :src="ctx.stillUrl(ctx.selected.id)" alt="Selected still">
|
||||||
<p v-else class="m-muted">Select a still or clip</p>
|
<p v-else class="m-muted">Open Library to pick a still or clip</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-strip" aria-label="Family outputs">
|
<Studio2LibraryModal
|
||||||
<div
|
:open="!!ctx.libraryOpen"
|
||||||
v-for="o in ctx.family"
|
:assets="ctx.family || []"
|
||||||
:key="o.id"
|
:selected="ctx.selected"
|
||||||
class="m-tile"
|
:folder-id="ctx.form?.folderId"
|
||||||
:class="{ on: ctx.selected?.id===o.id }"
|
:is-hidden="ctx.isHidden"
|
||||||
>
|
@close="ctx.closeLibrary()"
|
||||||
<button type="button" class="m-tile-pick" @click="ctx.selectOutput(o)">
|
@select="ctx.selectOutput"
|
||||||
<span v-if="ctx.isHidden(o)" class="m-tile-hid">Hidden</span>
|
@use-as-photo="ctx.onLibraryUseAsPhoto"
|
||||||
<span v-else-if="o.kind==='track'" class="m-tile-hid">Audio</span>
|
@copy-prompt="ctx.onLibraryCopyPrompt"
|
||||||
<img v-else :src="o.kind==='clip' ? `/api/library/clips/${o.id}/thumb` : ctx.stillUrl(o.id)" alt="">
|
@load="ctx.onLibraryLoad"
|
||||||
</button>
|
@delete="ctx.onLibraryDelete"
|
||||||
<button
|
/>
|
||||||
v-if="ctx.xaigen"
|
|
||||||
type="button"
|
|
||||||
class="m-tile-eye"
|
|
||||||
:aria-label="ctx.isHidden(o) ? 'Show tile' : 'Hide tile'"
|
|
||||||
@click.stop="ctx.toggleReveal(o.id)"
|
|
||||||
>
|
|
||||||
<img :src="ctx.isHidden(o) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="12" height="12">
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="ctx.selected?.id===o.id"
|
|
||||||
type="button"
|
|
||||||
class="m-tile-trash"
|
|
||||||
aria-label="Delete selected"
|
|
||||||
@click.stop="ctx.deleteOutput"
|
|
||||||
>🗑</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="ctx.selected && ctx.selected.kind!=='track'" class="m-strip-actions">
|
|
||||||
<button type="button" @click="ctx.applyUseAsSource">Use as photo</button>
|
|
||||||
<button type="button" @click="ctx.copySelectedPrompt">Copy prompt</button>
|
|
||||||
<button type="button" @click="ctx.loadSelectedJob">Load</button>
|
|
||||||
<button type="button" @click="ctx.deleteOutput">Delete</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="m-dock">
|
<div class="m-dock">
|
||||||
<button type="button" :class="{ on: sheet==='slots' }" @click="toggleSheet('slots')">Photos</button>
|
<button type="button" :class="{ on: sheet==='slots' }" @click="toggleSheet('slots')">Photos</button>
|
||||||
@@ -416,56 +400,46 @@ watch(() => props.ctx.form.mode, () => { sheet.value = '' })
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
.m-audio { width: 100%; max-width: 100%; padding: 12px; }
|
.m-audio { width: 100%; max-width: 100%; padding: 12px; }
|
||||||
.m-eye {
|
.m-stage-chrome {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
|
left: 8px;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.m-stage-chrome > * { pointer-events: auto; }
|
||||||
|
.m-library {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 48px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #000a;
|
||||||
|
padding: 0 14px;
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.m-library-count {
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.m-eye {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 28px;
|
width: 48px;
|
||||||
height: 28px;
|
height: 48px;
|
||||||
|
flex-shrink: 0;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background: #000a;
|
background: #000a;
|
||||||
}
|
}
|
||||||
.m-strip {
|
|
||||||
display: flex;
|
|
||||||
height: 64px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
gap: 6px;
|
|
||||||
overflow-x: auto;
|
|
||||||
border-top: 1px solid var(--line);
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
.m-tile {
|
|
||||||
position: relative;
|
|
||||||
width: 72px;
|
|
||||||
height: 48px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #222;
|
|
||||||
}
|
|
||||||
.m-tile.on { border-color: var(--amber); }
|
|
||||||
.m-tile-pick { display: block; width: 100%; height: 100%; border: 0; padding: 0; background: transparent; }
|
|
||||||
.m-tile img { width: 100%; height: 100%; object-fit: cover; }
|
|
||||||
.m-tile-hid { display: grid; height: 100%; place-items: center; color: var(--muted); font-size: 10px; }
|
|
||||||
.m-tile-eye, .m-tile-trash {
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
right: 2px;
|
|
||||||
display: grid;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
place-items: center;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 3px;
|
|
||||||
background: #000a;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
.m-tile-trash { right: 20px; }
|
|
||||||
.m-dock {
|
.m-dock {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr) 120px;
|
grid-template-columns: repeat(4, 1fr) 120px;
|
||||||
@@ -564,22 +538,6 @@ watch(() => props.ctx.form.mode, () => { sheet.value = '' })
|
|||||||
.m-neg-chip button { border: 0; background: transparent; color: inherit; }
|
.m-neg-chip button { border: 0; background: transparent; color: inherit; }
|
||||||
.m-neg-add { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
|
.m-neg-add { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
|
||||||
.m-neg-add .m-input { flex: 1; margin-top: 0; }
|
.m-neg-add .m-input { flex: 1; margin-top: 0; }
|
||||||
.m-strip-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
border-top: 1px solid var(--line);
|
|
||||||
background: var(--panel);
|
|
||||||
}
|
|
||||||
.m-strip-actions button {
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text);
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.m-seed { display: flex; gap: 8px; margin-top: 4px; }
|
.m-seed { display: flex; gap: 8px; margin-top: 4px; }
|
||||||
.m-seed .m-input { margin-top: 0; flex: 1; }
|
.m-seed .m-input { margin-top: 0; flex: 1; }
|
||||||
.m-dice {
|
.m-dice {
|
||||||
|
|||||||
@@ -62,8 +62,13 @@ function pushPromptRows(rows: SettingsRow[], item: {
|
|||||||
post: item.promptPost
|
post: item.promptPost
|
||||||
})
|
})
|
||||||
if (parts.pre.trim()) rows.push({ label: 'Pre-text', value: parts.pre.trim() })
|
if (parts.pre.trim()) rows.push({ label: 'Pre-text', value: parts.pre.trim() })
|
||||||
if (parts.prompt.trim()) rows.push({ label: 'Prompt', value: parts.prompt.trim() })
|
const used = parts.prompt.trim()
|
||||||
if (typed && typed !== parts.prompt.trim()) rows.push({ label: 'Typed', value: typed })
|
if (typed && typed !== used) {
|
||||||
|
rows.push({ label: 'Typed', value: typed })
|
||||||
|
if (used) rows.push({ label: 'Used', value: used })
|
||||||
|
} else if (used) {
|
||||||
|
rows.push({ label: 'Prompt', value: used })
|
||||||
|
}
|
||||||
if (parts.post.trim()) rows.push({ label: 'Post-text', value: parts.post.trim() })
|
if (parts.post.trim()) rows.push({ label: 'Post-text', value: parts.post.trim() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user