Put still actions on the stage and split output vs input hide.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+82
-4
@@ -363,13 +363,21 @@
|
||||
left: 10px;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stage-chrome > * {
|
||||
.stage-chrome-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stage-chrome-top > *,
|
||||
.stage-actions {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -397,6 +405,33 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stage-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
max-width: calc(100% - 108px);
|
||||
}
|
||||
|
||||
.stage-actions button {
|
||||
min-height: 36px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #000a;
|
||||
color: var(--text);
|
||||
padding: 0 10px;
|
||||
font: inherit;
|
||||
font-size: var(--type-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stage-actions button:hover {
|
||||
border-color: var(--amber);
|
||||
}
|
||||
|
||||
.stage-actions .stage-action-danger {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.eye-viewer {
|
||||
display: grid;
|
||||
width: 44px;
|
||||
@@ -444,6 +479,49 @@
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.photos-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.photos-inputs-toggle {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
padding: 4px 8px;
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.photos-inputs-toggle:hover {
|
||||
border-color: var(--amber);
|
||||
color: var(--amber);
|
||||
}
|
||||
|
||||
.photos-collapsed {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
font-size: var(--type-sm);
|
||||
}
|
||||
|
||||
.photos-add-link {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--amber);
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
font-size: var(--type-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filmstrip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -79,15 +79,34 @@
|
||||
|
||||
<template v-else>
|
||||
<section v-if="photosPlanState.show" class="photos-section" aria-label="Photos">
|
||||
<div class="photos-head">
|
||||
<span class="field-label">Photos</span>
|
||||
<button
|
||||
v-if="!photosPlanState.handoffReadonly"
|
||||
type="button"
|
||||
class="photos-inputs-toggle"
|
||||
@click="hideInputs=!hideInputs"
|
||||
>{{ hideInputs ? 'Show inputs' : 'Hide inputs' }}</button>
|
||||
</div>
|
||||
<p v-if="photosPlanState.handoffReadonly" class="text-[var(--type-sm)] text-[var(--muted)]">Continues from last frame</p>
|
||||
<template v-else-if="hideInputs">
|
||||
<p class="photos-collapsed">
|
||||
<span>{{ photoFilledCount }} photo{{ photoFilledCount===1 ? '' : 's' }}</span>
|
||||
<button
|
||||
v-if="form.photos.length < photosPlanState.max"
|
||||
type="button"
|
||||
class="photos-add-link"
|
||||
@click="hideInputs=false; addPhoto()"
|
||||
>+ Add photo</button>
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p v-if="!form.photos.length && photosPlanState.emptyHint" class="text-[var(--type-sm)] text-[var(--muted)]">{{ photosPlanState.emptyHint }}</p>
|
||||
<Studio2AssetSlot
|
||||
v-for="(card, index) in form.photos"
|
||||
:key="'photo-'+index+'-'+card.stillId"
|
||||
:asset="stillById(card.stillId)"
|
||||
:hidden="slotHidden(card.stillId)"
|
||||
:hidden="false"
|
||||
:role="card.role"
|
||||
:role-options="photoRoleOptions"
|
||||
@pick="picker='photo:'+index"
|
||||
@@ -200,6 +219,7 @@
|
||||
<div class="center-stack">
|
||||
<div class="viewer-frame">
|
||||
<div class="stage-chrome" aria-label="Stage controls">
|
||||
<div class="stage-chrome-top">
|
||||
<button
|
||||
type="button"
|
||||
class="stage-library"
|
||||
@@ -210,13 +230,24 @@
|
||||
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)"
|
||||
:aria-pressed="eyeOpen"
|
||||
:aria-label="eyeOpen ? 'Hide output' : 'Show output'"
|
||||
@click.stop="toggleReveal()"
|
||||
>
|
||||
<img :src="isHidden(selected) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="20" height="20">
|
||||
<img :src="eyeOpen ? '/icons/display/eye.svg' : '/icons/display/eye-off.svg'" alt="" width="20" height="20">
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="selected && selected.kind!=='track'"
|
||||
class="stage-actions"
|
||||
aria-label="Selected still actions"
|
||||
>
|
||||
<button type="button" @click.stop="applyUseAsSource">Use as photo</button>
|
||||
<button type="button" @click.stop="copySelectedPrompt">Copy prompt</button>
|
||||
<button type="button" @click.stop="loadSelectedJob">Load</button>
|
||||
<button type="button" class="stage-action-danger" @click.stop="deleteOutput">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="family.length > 1"
|
||||
@@ -234,7 +265,7 @@
|
||||
>›</button>
|
||||
|
||||
<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' : 'Output hidden' }}</p>
|
||||
</template>
|
||||
<template v-else-if="selected?.kind==='track'">
|
||||
<div class="w-full max-w-xl px-6 py-4">
|
||||
@@ -273,13 +304,14 @@
|
||||
:assets="family"
|
||||
:selected="selected"
|
||||
:folder-id="form.folderId"
|
||||
:is-hidden="isHidden"
|
||||
:preview-hidden="!eyeOpen"
|
||||
@close="libraryOpen=false"
|
||||
@select="selectOutput"
|
||||
@use-as-photo="onLibraryUseAsPhoto"
|
||||
@copy-prompt="onLibraryCopyPrompt"
|
||||
@load="onLibraryLoad"
|
||||
@delete="onLibraryDelete"
|
||||
@toggle-preview="toggleReveal()"
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -456,6 +488,7 @@ const liveMusic=ref<Record<string,any>>({})
|
||||
const negativeDraft=ref('')
|
||||
const libraryOpen=ref(false)
|
||||
const eyeOpen=ref(!xaigen)
|
||||
const hideInputs=ref(!!xaigen)
|
||||
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})
|
||||
@@ -554,6 +587,7 @@ const resolvedSize=computed(()=>resolveSize(form.settings.aspect,sourceSize.valu
|
||||
const negativeChips=computed(()=>form.imageStyles.negative || [])
|
||||
const photosPlanState=computed(()=>photosPlan({mode:form.mode,engine:form.engine}))
|
||||
const photoRoleOptions=computed(()=>(photosPlanState.value.roles||[]).map((id:string)=>({id,label:PHOTO_ROLE_LABEL[id]||id})))
|
||||
const photoFilledCount=computed(()=>(form.photos||[]).filter((p:any)=>p?.stillId).length)
|
||||
const photoRoleWarning=computed(()=>{
|
||||
const dup=duplicatePhotoRole(form.photos)
|
||||
return dup?`Two photos are set to the same job (${dup}). Change one.`:''
|
||||
@@ -823,14 +857,24 @@ function pickerThumbHidden(asset:any){
|
||||
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 slotHidden(_id:string){
|
||||
// Photos inspector uses hideInputs — never the output eye.
|
||||
return hideInputs.value
|
||||
}
|
||||
function toggleReveal(_id?:string){
|
||||
eyeOpen.value=!eyeOpen.value
|
||||
}
|
||||
async function deleteOutput(){
|
||||
if(!selected.value)return
|
||||
const label=assetLabel(selected.value)
|
||||
if(import.meta.client && !window.confirm(`Delete ${label}?`))return
|
||||
try{
|
||||
const kind=selected.value.kind==='clip'?'clips/':selected.value.kind==='track'?'tracks/':'stills/'
|
||||
const id=selected.value.id
|
||||
await $fetch('/api/library/'+kind+id,{method:'DELETE'})
|
||||
await refresh()
|
||||
}catch(e:any){error.value=e.data?.statusMessage||e.message}
|
||||
}
|
||||
function stepFamily(delta:number){
|
||||
const list=family.value
|
||||
if(list.length < 2)return
|
||||
@@ -970,14 +1014,6 @@ function restore(item:any){Object.assign(form,restoreAll(form,item,xaigen,loraMe
|
||||
function focusJob(job:any){selectOutput(job.outputs?.at(-1)||null);focusedJobId.value=job.id}
|
||||
async function loadLoras(fresh=false){try{loraNames.value=await $fetch('/api/studio-2/loras'+(fresh?'?fresh=1':''))}catch{error.value='Could not refresh LoRAs. The last loaded list is still available.'}}
|
||||
async function clearFinished(){await $fetch('/api/studio-2/clear-finished',{method:'POST'});await refresh()}
|
||||
async function deleteOutput(){
|
||||
if(!selected.value)return
|
||||
try{
|
||||
const kind=selected.value.kind==='clip'?'clips/':selected.value.kind==='track'?'tracks/':'stills/'
|
||||
await $fetch('/api/library/'+kind+selected.value.id,{method:'DELETE'})
|
||||
await refresh()
|
||||
}catch(e:any){error.value=e.data?.statusMessage||e.message}
|
||||
}
|
||||
async function queue(){
|
||||
submitting.value=true;error.value=''
|
||||
try{
|
||||
@@ -1078,8 +1114,13 @@ const mobileCtx=computed(()=>({
|
||||
family:family.value,
|
||||
familyStillCount:familyStillCount.value,
|
||||
libraryOpen:libraryOpen.value,
|
||||
eyeOpen:eyeOpen.value,
|
||||
hideInputs:hideInputs.value,
|
||||
photoFilledCount:photoFilledCount.value,
|
||||
openLibrary:()=>{libraryOpen.value=true},
|
||||
closeLibrary:()=>{libraryOpen.value=false},
|
||||
toggleHideInputs:()=>{hideInputs.value=!hideInputs.value},
|
||||
showInputsAndAddPhoto:()=>{hideInputs.value=false;addPhoto()},
|
||||
sourceSlot:sourceSlot.value,
|
||||
video:video.value,
|
||||
submitting:submitting.value,
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
placeholder="Name or prompt"
|
||||
aria-label="Search library"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="lib-modal-preview-all"
|
||||
@click="$emit('toggle-preview')"
|
||||
>{{ previewHidden ? 'Show all previews' : 'Hide all previews' }}</button>
|
||||
<div class="lib-modal-filters" role="group" aria-label="Type">
|
||||
<button
|
||||
v-for="opt in filterOptions"
|
||||
@@ -95,6 +100,7 @@
|
||||
<button type="button" @click="$emit('use-as-photo', detailAsset)">Use as photo</button>
|
||||
<button type="button" @click="$emit('copy-prompt', detailAsset)">Copy prompt</button>
|
||||
<button type="button" @click="$emit('load', detailAsset)">Load</button>
|
||||
<button type="button" @click="$emit('toggle-preview')">{{ previewHidden ? 'Show output' : 'Hide output' }}</button>
|
||||
<button type="button" class="danger" @click="onDeleteClick">Delete</button>
|
||||
<button type="button" class="primary" @click="useAndClose">Use and close</button>
|
||||
</div>
|
||||
@@ -126,6 +132,7 @@ const props = defineProps<{
|
||||
assets?: any[]
|
||||
selected?: any
|
||||
folderId?: string
|
||||
previewHidden?: boolean
|
||||
isHidden?: (asset: any) => boolean
|
||||
}>()
|
||||
|
||||
@@ -136,6 +143,7 @@ const emit = defineEmits<{
|
||||
'copy-prompt': [asset: any]
|
||||
load: [asset: any]
|
||||
delete: [asset: any]
|
||||
'toggle-preview': []
|
||||
}>()
|
||||
|
||||
const filterOptions = [
|
||||
@@ -402,6 +410,21 @@ onBeforeUnmount(() => {
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
.lib-modal-preview-all {
|
||||
border: 1px solid #2a2a2e;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 8px 10px;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.lib-modal-preview-all:hover {
|
||||
border-color: #f5a524;
|
||||
color: #f5a524;
|
||||
}
|
||||
.lib-modal-filters { display: inline-flex; gap: 4px; }
|
||||
.lib-modal-chip {
|
||||
border: 1px solid #2a2a2e;
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
|
||||
<div class="m-stage">
|
||||
<div class="m-stage-chrome" aria-label="Stage controls">
|
||||
<div class="m-stage-chrome-top">
|
||||
<button
|
||||
type="button"
|
||||
class="m-library"
|
||||
@@ -59,12 +60,23 @@
|
||||
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)"
|
||||
:aria-label="ctx.eyeOpen ? 'Hide output' : 'Show output'"
|
||||
@click.stop="ctx.toggleReveal()"
|
||||
>
|
||||
<img :src="ctx.isHidden(ctx.selected) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="20" height="20">
|
||||
<img :src="ctx.eyeOpen ? '/icons/display/eye.svg' : '/icons/display/eye-off.svg'" alt="" width="20" height="20">
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="ctx.selected && ctx.selected.kind!=='track'"
|
||||
class="m-stage-actions"
|
||||
aria-label="Selected still actions"
|
||||
>
|
||||
<button type="button" @click.stop="ctx.applyUseAsSource">Use as photo</button>
|
||||
<button type="button" @click.stop="ctx.copySelectedPrompt">Copy prompt</button>
|
||||
<button type="button" @click.stop="ctx.loadSelectedJob">Load</button>
|
||||
<button type="button" class="m-danger" @click.stop="ctx.deleteOutput">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="(ctx.family || []).length > 1"
|
||||
type="button"
|
||||
@@ -80,7 +92,7 @@
|
||||
@click.stop="ctx.stepFamily(1)"
|
||||
>›</button>
|
||||
<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' : 'Output hidden' }}</p>
|
||||
</template>
|
||||
<template v-else-if="ctx.selected?.kind==='track'">
|
||||
<div class="m-audio">
|
||||
@@ -103,13 +115,14 @@
|
||||
:assets="ctx.family || []"
|
||||
:selected="ctx.selected"
|
||||
:folder-id="ctx.form?.folderId"
|
||||
:is-hidden="ctx.isHidden"
|
||||
:preview-hidden="!ctx.eyeOpen"
|
||||
@close="ctx.closeLibrary()"
|
||||
@select="ctx.selectOutput"
|
||||
@use-as-photo="ctx.onLibraryUseAsPhoto"
|
||||
@copy-prompt="ctx.onLibraryCopyPrompt"
|
||||
@load="ctx.onLibraryLoad"
|
||||
@delete="ctx.onLibraryDelete"
|
||||
@toggle-preview="ctx.toggleReveal()"
|
||||
/>
|
||||
|
||||
<div class="m-dock">
|
||||
@@ -132,13 +145,30 @@
|
||||
<template v-else-if="ctx.photosPlanState.handoffReadonly">
|
||||
<p class="m-muted">Continues from last frame</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="m-photos-head">
|
||||
<button type="button" class="m-link" @click="ctx.toggleHideInputs()">
|
||||
{{ ctx.hideInputs ? 'Show inputs' : 'Hide inputs' }}
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="ctx.hideInputs">
|
||||
<p class="m-muted">
|
||||
{{ ctx.photoFilledCount || 0 }} photo{{ (ctx.photoFilledCount||0)===1 ? '' : 's' }}
|
||||
<button
|
||||
v-if="(ctx.form.photos?.length||0) < ctx.photosPlanState.max"
|
||||
type="button"
|
||||
class="m-amber"
|
||||
@click="ctx.showInputsAndAddPhoto()"
|
||||
>+ Add photo</button>
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p v-if="!ctx.form.photos?.length && ctx.photosPlanState.emptyHint" class="m-muted">{{ ctx.photosPlanState.emptyHint }}</p>
|
||||
<Studio2AssetSlot
|
||||
v-for="(card, index) in ctx.form.photos"
|
||||
:key="'m-photo-'+index+'-'+card.stillId"
|
||||
:asset="ctx.stillById(card.stillId)"
|
||||
:hidden="ctx.slotHidden(card.stillId)"
|
||||
:hidden="false"
|
||||
:role="card.role"
|
||||
:role-options="ctx.photoRoleOptions"
|
||||
@pick="ctx.setPicker('photo:'+index)"
|
||||
@@ -148,6 +178,7 @@
|
||||
/>
|
||||
<button v-if="(ctx.form.photos?.length||0) < ctx.photosPlanState.max" type="button" class="m-amber" @click="ctx.addPhoto">+ Add photo</button>
|
||||
<p v-if="ctx.photoRoleWarning" class="m-hint">{{ ctx.photoRoleWarning }}</p>
|
||||
</template>
|
||||
<template v-if="ctx.showIdentityLocks">
|
||||
<label class="m-check"><input v-model="ctx.form.lockFace" type="checkbox"> Lock face</label>
|
||||
<label class="m-check"><input v-model="ctx.form.lockOutfit" type="checkbox"> Lock outfit</label>
|
||||
@@ -421,12 +452,20 @@ watch(() => props.ctx.form.mode, () => { sheet.value = '' })
|
||||
left: 8px;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.m-stage-chrome > * { pointer-events: auto; }
|
||||
.m-stage-chrome-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.m-stage-chrome-top > *,
|
||||
.m-stage-actions {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.m-library {
|
||||
display: inline-flex;
|
||||
min-height: 48px;
|
||||
@@ -454,6 +493,23 @@ watch(() => props.ctx.form.mode, () => { sheet.value = '' })
|
||||
border-radius: 8px;
|
||||
background: #000a;
|
||||
}
|
||||
.m-stage-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
max-width: calc(100% - 56px);
|
||||
}
|
||||
.m-stage-actions button {
|
||||
min-height: 44px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #000a;
|
||||
color: var(--text);
|
||||
padding: 0 10px;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
.m-stage-actions .m-danger { color: #fca5a5; }
|
||||
.m-nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -472,6 +528,7 @@ watch(() => props.ctx.form.mode, () => { sheet.value = '' })
|
||||
}
|
||||
.m-nav-prev { left: 8px; }
|
||||
.m-nav-next { right: 8px; }
|
||||
.m-photos-head { margin-bottom: 8px; }
|
||||
.m-dock {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr) 120px;
|
||||
|
||||
Reference in New Issue
Block a user