Keep image jobs on the studio queue and confirm each job or library delete. Label stitch-frame slots when thumbs are hidden, and open output stills in a lightbox.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+294
-53
@@ -693,7 +693,7 @@
|
||||
:disabled="studioMode === 'edit' ? editBlocked : videoGenerateDisabled"
|
||||
@click="studioMode === 'edit' ? editImage() : generate()"
|
||||
>
|
||||
{{ studioMode === 'edit' ? (editBusy ? 'Editing…' : editLabel) : generateLabel }}
|
||||
{{ studioMode === 'edit' ? editSubmitLabel : generateLabel }}
|
||||
</button>
|
||||
<button
|
||||
v-if="videoBusy || editBusy"
|
||||
@@ -705,9 +705,10 @@
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="studioMode === 'video' && videoBusy" class="text-xs text-zinc-400">The current job keeps running in Output. This submit waits in the job queue.</p>
|
||||
<p v-else-if="studioMode === 'edit' && (videoBusy || editBusy)" class="text-xs text-zinc-400">The current job keeps running in Output. This submit waits in the job queue.</p>
|
||||
<p v-if="studioMode === 'edit' && editBlockReason" class="text-xs text-zinc-400">{{ editBlockReason }}</p>
|
||||
|
||||
<div v-if="studioMode === 'video' && studioJobs.length" class="rounded-2xl border border-white/10 bg-zinc-950/40 p-4">
|
||||
<div v-if="studioJobs.length" class="rounded-2xl border border-white/10 bg-zinc-950/40 p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-zinc-100">Job queue</h3>
|
||||
@@ -729,6 +730,7 @@
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium text-zinc-100">{{ job.name }}</p>
|
||||
<p class="mt-0.5 text-xs text-zinc-500">
|
||||
<span v-if="job.kind === 'edit'" class="mr-1 text-amber-200/80">{{ (job.shotCount || 1) > 1 ? 'EDIT' : 'IMAGE' }} ·</span>
|
||||
{{ studioJobStatusLabel(job) }}
|
||||
· {{ studioJobShotLine(job) }}
|
||||
</p>
|
||||
@@ -755,7 +757,7 @@
|
||||
|
||||
<div v-if="drafts.length" class="rounded-2xl border border-amber-300/20 bg-amber-400/5 p-4">
|
||||
<h3 class="text-sm font-semibold text-amber-100">Held jobs</h3>
|
||||
<p class="mt-1 text-xs text-zinc-400">Comfy was still busy after 3 minutes. Click one to restore the still, prompt, and settings.</p>
|
||||
<p class="mt-1 text-xs text-zinc-400">Jobs saved to retry later. Click one to restore the still, prompt, and settings.</p>
|
||||
<ul class="mt-3 space-y-2">
|
||||
<li
|
||||
v-for="draft in drafts"
|
||||
@@ -971,13 +973,29 @@
|
||||
:class="{ 'sensitive-glow': editAwaitingReveal || editLockedSave }"
|
||||
style="min-height: 12rem"
|
||||
>
|
||||
<img
|
||||
<button
|
||||
v-if="editResultUrl && !editLockedSave"
|
||||
:src="editResultUrl"
|
||||
alt="Edited still"
|
||||
class="mx-auto max-h-[min(70dvh,24rem)] w-full bg-black object-contain"
|
||||
:class="{ 'opacity-0': editAwaitingReveal }"
|
||||
type="button"
|
||||
class="group relative mx-auto block w-full cursor-zoom-in bg-black"
|
||||
:class="{ 'pointer-events-none opacity-0': editAwaitingReveal }"
|
||||
aria-label="View still fullscreen"
|
||||
@click="openEditLightbox"
|
||||
>
|
||||
<img
|
||||
:src="editResultUrl"
|
||||
alt="Edited still"
|
||||
class="mx-auto max-h-[min(70dvh,24rem)] w-full object-contain"
|
||||
>
|
||||
<span
|
||||
v-if="!editAwaitingReveal"
|
||||
class="pointer-events-none absolute right-3 top-3 rounded-full border border-white/15 bg-zinc-950/75 p-1.5 text-amber-100 opacity-0 shadow-lg transition group-hover:opacity-100"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.75" stroke="currentColor" class="h-4 w-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="editAwaitingReveal && !editLockedSave"
|
||||
type="button"
|
||||
@@ -1100,7 +1118,7 @@
|
||||
{{ folder.name }}{{ folder.protected && !folder.unlocked ? ' · locked' : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" class="text-red-300 hover:text-red-200" @click="removeSelected">Delete</button>
|
||||
<button type="button" class="text-red-300 hover:text-red-200" @click="askRemoveSelected">Delete</button>
|
||||
<button type="button" class="text-zinc-400 hover:text-white" @click="clearSelection">Clear</button>
|
||||
</div>
|
||||
<ul class="mt-4 space-y-3">
|
||||
@@ -1190,7 +1208,7 @@
|
||||
{{ folder.name }}{{ folder.protected && !folder.unlocked ? ' · locked' : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" class="text-xs text-red-300" @click="removeClip(entry.group.head, true)">Delete</button>
|
||||
<button type="button" class="text-xs text-red-300" @click="askRemoveClip(entry.group.head, true)">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1198,14 +1216,14 @@
|
||||
<li
|
||||
v-for="(item, partIndex) in chronologicalParts(entry.group.parts)"
|
||||
:key="item.id"
|
||||
class="flex gap-3 rounded-xl border border-white/5 bg-black/20 p-2"
|
||||
class="flex flex-wrap items-start gap-3 rounded-xl border border-white/5 bg-black/20 p-2"
|
||||
>
|
||||
<ClipFrameThumbs
|
||||
:hidden="item.hideThumbnail"
|
||||
:still-id="item.hideThumbnail || partIndex !== 0 ? undefined : item.stillId"
|
||||
:from-clip-id="item.hideThumbnail ? undefined : previousChronoClipId(entry.group.parts, partIndex)"
|
||||
:still-id="partIndex === 0 ? item.stillId : undefined"
|
||||
:from-clip-id="previousChronoClipId(entry.group.parts, partIndex)"
|
||||
:clip-id="item.id"
|
||||
:show-first-frame="!item.hideThumbnail && partIndex > 0"
|
||||
:show-first-frame="partIndex > 0 || !item.stillId"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start gap-2">
|
||||
@@ -1250,7 +1268,7 @@
|
||||
{{ folder.name }}{{ folder.protected && !folder.unlocked ? ' · locked' : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" class="text-xs text-red-300" @click="removeClip(item)">Delete</button>
|
||||
<button type="button" class="text-xs text-red-300" @click="askRemoveClip(item)">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -1335,7 +1353,7 @@
|
||||
{{ folder.name }}{{ folder.protected && !folder.unlocked ? ' · locked' : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" class="text-xs text-red-300" @click="removeStill(entry.stillGroup.head, true)">Delete</button>
|
||||
<button type="button" class="text-xs text-red-300" @click="askRemoveStill(entry.stillGroup.head, true)">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1383,7 +1401,7 @@
|
||||
{{ folder.name }}{{ folder.protected && !folder.unlocked ? ' · locked' : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" class="text-xs text-red-300" @click="removeStill(item)">Delete</button>
|
||||
<button type="button" class="text-xs text-red-300" @click="askRemoveStill(item)">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -1428,6 +1446,60 @@
|
||||
@pick="onPickedPermanence"
|
||||
/>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="editLightboxUrl"
|
||||
ref="editLightbox"
|
||||
class="fixed inset-0 z-[60] flex items-center justify-center bg-black/85 p-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Edited still fullscreen"
|
||||
@click.self="closeEditLightbox"
|
||||
>
|
||||
<img
|
||||
:src="editLightboxUrl"
|
||||
alt="Edited still"
|
||||
class="max-h-[90vh] max-w-[min(96vw,80rem)] object-contain"
|
||||
@click.stop
|
||||
>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="pendingLibraryDelete"
|
||||
class="fixed inset-0 z-[70] flex items-center justify-center bg-black/70 p-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="library-delete-title"
|
||||
@click.self="pendingLibraryDelete = null"
|
||||
>
|
||||
<div class="glass w-full max-w-md rounded-3xl p-5 sm:p-6">
|
||||
<h2 id="library-delete-title" class="font-display text-xl font-bold">{{ pendingLibraryDelete.title }}</h2>
|
||||
<p class="mt-2 text-sm font-medium text-zinc-100">{{ pendingLibraryDelete.name }}</p>
|
||||
<p v-if="pendingLibraryDelete.detail" class="mt-1 text-xs text-zinc-500">{{ pendingLibraryDelete.detail }}</p>
|
||||
<p class="mt-3 text-sm text-zinc-400">{{ pendingLibraryDelete.consequence }}</p>
|
||||
<div class="mt-5 flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-white/15 px-4 py-2 text-sm text-zinc-300 hover:text-white"
|
||||
@click="pendingLibraryDelete = null"
|
||||
>
|
||||
Keep it
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-red-400/40 bg-red-400/10 px-4 py-2 text-sm font-semibold text-red-200 hover:border-red-300 hover:text-red-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="libraryDeleteBusy"
|
||||
@click="confirmLibraryDelete"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<div class="pointer-events-none fixed bottom-4 right-4 z-50 space-y-2">
|
||||
<div
|
||||
v-for="toast in toasts"
|
||||
@@ -1565,6 +1637,17 @@ interface QueuedEditPass {
|
||||
prompt: string
|
||||
}
|
||||
|
||||
type PendingLibraryDelete = {
|
||||
title: string
|
||||
name: string
|
||||
detail: string
|
||||
consequence: string
|
||||
mode: 'clip' | 'still' | 'selected'
|
||||
clip?: LibraryClip
|
||||
still?: LibraryStill
|
||||
family?: boolean
|
||||
}
|
||||
|
||||
const PRESETS: Record<Exclude<AspectId, 'auto'>, { width: number; height: number }> = {
|
||||
'16:9': { width: 1344, height: 768 },
|
||||
'9:16': { width: 768, height: 1344 },
|
||||
@@ -1580,6 +1663,8 @@ const aspects = [
|
||||
|
||||
const player = ref<HTMLVideoElement | null>(null)
|
||||
const outputPanel = ref<HTMLElement | null>(null)
|
||||
const editLightbox = ref<HTMLElement | null>(null)
|
||||
const editLightboxUrl = ref('')
|
||||
const file = ref<File | null>(null)
|
||||
const preview = ref('')
|
||||
const dragging = ref(false)
|
||||
@@ -1728,6 +1813,7 @@ const liveQueue = ref<{
|
||||
const studioJobs = ref<Array<{
|
||||
id: string
|
||||
status: string
|
||||
kind?: string
|
||||
name: string
|
||||
prompt: string
|
||||
shotCount: number
|
||||
@@ -1748,6 +1834,8 @@ const pickerSlot = ref<'main' | 'editRef' | number | 'perm' | `perm:${number}` |
|
||||
const libraryFilter = ref<'all' | 'images' | 'videos'>('all')
|
||||
const selectedKeys = ref<string[]>([])
|
||||
const lastSelectedKey = ref('')
|
||||
const pendingLibraryDelete = ref<PendingLibraryDelete | null>(null)
|
||||
const libraryDeleteBusy = ref(false)
|
||||
const videoEngine = ref<VideoEngineId>('minimax')
|
||||
const videoStart = ref<VideoStartId>('still')
|
||||
const minimaxGraph = ref<MiniMaxGraphId>('v1')
|
||||
@@ -1946,7 +2034,6 @@ const recommendHint = computed(() => {
|
||||
return 'Send this still and draft to Sidecar Qwen VL.'
|
||||
})
|
||||
const editBlocked = computed(() => {
|
||||
if (editBusy.value) return true
|
||||
if (!file.value || !prompt.value.trim() || !folderId.value) return true
|
||||
if (editPassQueue.value.some(item => !item.prompt.trim())) return true
|
||||
if (!comfyOk.value && !imageComfyOk.value) return true
|
||||
@@ -1980,6 +2067,12 @@ const editLabel = computed(() => {
|
||||
if (!n) return 'Edit image'
|
||||
return n === 1 ? 'Edit image + 1 pass' : `Edit image + ${n} passes`
|
||||
})
|
||||
const editSubmitLabel = computed(() => {
|
||||
const occupied = videoBusy.value || editBusy.value || studioJobs.value.some(job => job.status === 'running')
|
||||
if (!occupied) return editLabel.value
|
||||
const n = editPassQueue.value.length
|
||||
return n ? `Queue ${n + 1} passes` : 'Queue edit'
|
||||
})
|
||||
const parsedShots = computed(() => parseShotScript(prompt.value))
|
||||
const recommendedShots = computed(() => {
|
||||
if (!recommendText.value || recommendBusy.value) return []
|
||||
@@ -2448,6 +2541,7 @@ onMounted(async () => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('pointerdown', onSitePointerDown, true)
|
||||
window.removeEventListener('keydown', onEditLightboxKey)
|
||||
if (healthTimer) clearInterval(healthTimer)
|
||||
healthTimer = null
|
||||
if (libraryRefreshTimer) clearTimeout(libraryRefreshTimer)
|
||||
@@ -2482,6 +2576,15 @@ watch(videoBusy, (busy) => {
|
||||
if (busy) outputFocus.value = 'video'
|
||||
})
|
||||
|
||||
watch(editLightboxUrl, (value) => {
|
||||
if (value) window.addEventListener('keydown', onEditLightboxKey)
|
||||
else window.removeEventListener('keydown', onEditLightboxKey)
|
||||
})
|
||||
|
||||
watch([editResultUrl, editAwaitingReveal, editLockedSave], () => {
|
||||
if (!editResultUrl.value || editAwaitingReveal.value || editLockedSave.value) closeEditLightbox()
|
||||
})
|
||||
|
||||
watch(hideInputPreview, (value) => {
|
||||
try {
|
||||
localStorage.setItem('aigen-hide-input-preview', String(value))
|
||||
@@ -2967,7 +3070,11 @@ function studioJobStatusLabel(job: { status: string; cutIn?: boolean; holdForCut
|
||||
return job.status
|
||||
}
|
||||
|
||||
function studioJobShotLine(job: { shotCount: number; completedCount?: number; status: string }) {
|
||||
function studioJobShotLine(job: { kind?: string; shotCount: number; completedCount?: number; status: string }) {
|
||||
if (job.kind === 'edit') {
|
||||
const total = job.shotCount || 1
|
||||
return total <= 1 ? 'image edit' : `${total} passes`
|
||||
}
|
||||
const total = job.shotCount || 1
|
||||
const done = job.completedCount || 0
|
||||
if (total <= 1) return '1 clip'
|
||||
@@ -2980,6 +3087,7 @@ async function refreshStudioQueue() {
|
||||
jobs?: Array<{
|
||||
id: string
|
||||
status: string
|
||||
kind?: string
|
||||
name: string
|
||||
prompt: string
|
||||
shotCount: number
|
||||
@@ -2997,7 +3105,19 @@ async function refreshStudioQueue() {
|
||||
}>('/api/studio-queue').catch(() => ({ jobs: [] }))
|
||||
studioJobs.value = data.jobs || []
|
||||
const running = studioJobs.value.find(job => job.status === 'running' && job.liveJobId)
|
||||
if (running?.liveJobId && running.liveJobId !== videoJobId.value && !videoBusy.value) {
|
||||
if (running?.kind === 'edit') {
|
||||
if (running.liveJobId && running.liveJobId !== editJobId.value && !editBusy.value) {
|
||||
outputStudioJobId.value = running.id
|
||||
editJobId.value = running.liveJobId
|
||||
persistActiveJob('edit', running.liveJobId, running.hideThumbnail === true, running.folderLocked === true)
|
||||
listen('edit', running.liveJobId, running.hideThumbnail === true, running.folderLocked === true)
|
||||
editBusy.value = true
|
||||
editStatusMessage.value = 'Starting queued edit…'
|
||||
startTimer('edit')
|
||||
} else if (running.id && running.liveJobId === editJobId.value) {
|
||||
outputStudioJobId.value = running.id
|
||||
}
|
||||
} else if (running?.liveJobId && running.liveJobId !== videoJobId.value && !videoBusy.value) {
|
||||
outputStudioJobId.value = running.id
|
||||
videoJobId.value = running.liveJobId
|
||||
persistActiveJob('video', running.liveJobId, running.hideThumbnail === true, running.folderLocked === true)
|
||||
@@ -3223,9 +3343,24 @@ async function resumeActiveJob() {
|
||||
}
|
||||
}
|
||||
|
||||
function openEditLightbox() {
|
||||
if (!editResultUrl.value || editAwaitingReveal.value || editLockedSave.value) return
|
||||
editLightboxUrl.value = editResultUrl.value
|
||||
}
|
||||
|
||||
function closeEditLightbox() {
|
||||
editLightboxUrl.value = ''
|
||||
}
|
||||
|
||||
function onEditLightboxKey(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') closeEditLightbox()
|
||||
}
|
||||
|
||||
function onSitePointerDown(event: PointerEvent) {
|
||||
const root = outputPanel.value
|
||||
const target = event.target as Node | null
|
||||
if (editLightbox.value && target && editLightbox.value.contains(target)) return
|
||||
if (editLightboxUrl.value) closeEditLightbox()
|
||||
const root = outputPanel.value
|
||||
if (root && target && root.contains(target)) return
|
||||
if (concealOutput.value && !awaitingReveal.value && !lockedSave.value && videoUrl.value) {
|
||||
concealAndPause()
|
||||
@@ -3599,12 +3734,102 @@ async function useStillAsInput(still: LibraryStill) {
|
||||
}
|
||||
}
|
||||
|
||||
function libraryPromptSnippet(text?: string) {
|
||||
const value = String(text || '').replace(/\s+/g, ' ').trim()
|
||||
if (!value) return ''
|
||||
return value.length > 120 ? `${value.slice(0, 117)}…` : value
|
||||
}
|
||||
|
||||
function clipDeleteName(item: LibraryClip) {
|
||||
return item.name || libraryPromptSnippet(item.prompt) || 'Untitled clip'
|
||||
}
|
||||
|
||||
function stillDeleteName(still: LibraryStill) {
|
||||
return still.name || libraryPromptSnippet(still.prompt) || stillTitle(still)
|
||||
}
|
||||
|
||||
function askRemoveClip(item: LibraryClip, family = false) {
|
||||
const parts = family ? familyPartsFor(item) : [item]
|
||||
const collection = Boolean(family)
|
||||
pendingLibraryDelete.value = {
|
||||
title: collection ? 'Delete this collection?' : 'Delete this clip?',
|
||||
name: collection
|
||||
? (collectionTitle({ head: item, parts }) || clipDeleteName(item))
|
||||
: clipDeleteName(item),
|
||||
detail: collection
|
||||
? `${parts.length} ${parts.length === 1 ? 'part' : 'parts'}`
|
||||
: (item.name ? libraryPromptSnippet(item.prompt) : ''),
|
||||
consequence: collection
|
||||
? 'This removes the whole collection from the library.'
|
||||
: 'This removes only this clip from the library.',
|
||||
mode: 'clip',
|
||||
clip: item,
|
||||
family
|
||||
}
|
||||
}
|
||||
|
||||
function askRemoveStill(still: LibraryStill, family = false) {
|
||||
const parts = family ? stillFamilyPartsFor(still) : [still]
|
||||
const collection = Boolean(family)
|
||||
pendingLibraryDelete.value = {
|
||||
title: collection ? 'Delete this collection?' : 'Delete this still?',
|
||||
name: collection
|
||||
? stillCollectionTitle({ head: still, parts })
|
||||
: stillDeleteName(still),
|
||||
detail: collection
|
||||
? `${parts.length} ${parts.length === 1 ? 'part' : 'parts'}`
|
||||
: (still.name ? libraryPromptSnippet(still.prompt) : ''),
|
||||
consequence: collection
|
||||
? 'This removes the whole collection from the library.'
|
||||
: 'This removes only this still from the library.',
|
||||
mode: 'still',
|
||||
still,
|
||||
family
|
||||
}
|
||||
}
|
||||
|
||||
function askRemoveSelected() {
|
||||
const items = selectedLibraryEntries()
|
||||
if (!items.length) return
|
||||
if (items.length === 1) {
|
||||
const entry = items[0]
|
||||
if (entry.kind === 'clip') askRemoveClip(entry.group.head, true)
|
||||
else askRemoveStill(entry.stillGroup!.head, true)
|
||||
const pending = pendingLibraryDelete.value
|
||||
if (pending) pendingLibraryDelete.value = { ...pending, mode: 'selected' }
|
||||
return
|
||||
}
|
||||
const partCount = items.reduce((count, entry) => (
|
||||
count + (entry.kind === 'clip' ? entry.group.parts.length : entry.stillGroup!.parts.length)
|
||||
), 0)
|
||||
const names = items.slice(0, 3).map(entry => (
|
||||
entry.kind === 'clip' ? collectionTitle(entry.group) : stillCollectionTitle(entry.stillGroup!)
|
||||
)).filter(Boolean)
|
||||
pendingLibraryDelete.value = {
|
||||
title: 'Delete these items?',
|
||||
name: names.join(', ') + (items.length > 3 ? ` and ${items.length - 3} more` : ''),
|
||||
detail: `${items.length} collections · ${partCount} ${partCount === 1 ? 'part' : 'parts'}`,
|
||||
consequence: 'This removes the selected collections from the library.',
|
||||
mode: 'selected'
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmLibraryDelete() {
|
||||
const pending = pendingLibraryDelete.value
|
||||
if (!pending || libraryDeleteBusy.value) return
|
||||
pendingLibraryDelete.value = null
|
||||
libraryDeleteBusy.value = true
|
||||
try {
|
||||
if (pending.mode === 'selected') await removeSelected()
|
||||
else if (pending.mode === 'clip' && pending.clip) await removeClip(pending.clip, Boolean(pending.family))
|
||||
else if (pending.mode === 'still' && pending.still) await removeStill(pending.still, Boolean(pending.family))
|
||||
} finally {
|
||||
libraryDeleteBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeStill(still: LibraryStill, family = false) {
|
||||
const watched = family ? stillFamilyPartsFor(still) : [still]
|
||||
const label = family && watched.length > 1
|
||||
? `this ${watched.length}-pass edit`
|
||||
: `“${stillTitle(still)}”`
|
||||
if (!confirm(`Remove ${label} from this folder?`)) return
|
||||
try {
|
||||
await $fetch(`/api/library/stills/${still.id}`, {
|
||||
method: 'DELETE',
|
||||
@@ -3773,8 +3998,6 @@ async function onMoveSelected(event: Event) {
|
||||
async function removeSelected() {
|
||||
const items = selectedLibraryEntries()
|
||||
if (!items.length) return
|
||||
const label = items.length === 1 ? 'this item' : `these ${items.length} items`
|
||||
if (!confirm(`Remove ${label} from this folder?`)) return
|
||||
const clipIds = new Set<string>()
|
||||
const stillIds = new Set<string>()
|
||||
const results = await Promise.allSettled(items.map(entry => {
|
||||
@@ -3952,27 +4175,7 @@ async function editImage() {
|
||||
return
|
||||
}
|
||||
const extraPasses = editPassQueue.value.map(item => ({ prompt: item.prompt.trim() }))
|
||||
editStatusBusy.value = false
|
||||
editBusy.value = true
|
||||
stopListen('edit')
|
||||
editSettledUi = false
|
||||
editProgress.value = 2
|
||||
editResultUrl.value = ''
|
||||
editAwaitingReveal.value = false
|
||||
concealEditOutput.value = hideThumbnail.value
|
||||
editLockedSave.value = false
|
||||
editStatusMessage.value = 'Checking Beast ComfyUI...'
|
||||
editMaxStep.value = editSteps.value
|
||||
editChainStep.value = 1
|
||||
editChainTotal.value = 1 + extraPasses.length
|
||||
editChainLabel.value = extraPasses.length ? 'Pass 1' : ''
|
||||
editOverallProgress.value = 0
|
||||
editCompletedChainStep.value = 0
|
||||
editActiveChainPlan.value = [
|
||||
{ label: 'Pass 1', prompt: prompt.value.trim() },
|
||||
...extraPasses.map((item, index) => ({ label: `Pass ${index + 2}`, prompt: item.prompt }))
|
||||
]
|
||||
startTimer('edit')
|
||||
const hideOut = hideThumbnail.value
|
||||
try {
|
||||
const body = new FormData()
|
||||
body.append('image', file.value)
|
||||
@@ -3987,16 +4190,54 @@ async function editImage() {
|
||||
body.append('hideThumbnail', String(showPrivacyToggles.value && hideThumbnail.value))
|
||||
body.append('name', clipName.value.trim())
|
||||
if (extraPasses.length) body.append('passes', JSON.stringify(extraPasses))
|
||||
editDownloadName.value = clipFileName(clipName.value.trim() || prompt.value.split(/[.!?\n]/)[0] || 'aigen-edit')
|
||||
const started = await $fetch<{ jobId: string; steps: number; hideThumbnail?: boolean; folderLocked?: boolean }>('/api/edit', {
|
||||
const downloadName = clipFileName(clipName.value.trim() || prompt.value.split(/[.!?\n]/)[0] || 'aigen-edit')
|
||||
const started = await $fetch<{
|
||||
jobId: string
|
||||
studioJobId?: string
|
||||
queued?: boolean
|
||||
steps: number
|
||||
hideThumbnail?: boolean
|
||||
folderLocked?: boolean
|
||||
}>('/api/edit', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
editJobId.value = started.jobId
|
||||
if (started.queued) {
|
||||
const n = 1 + extraPasses.length
|
||||
toast(n > 1
|
||||
? `Queued ${n} edit passes behind the current job.`
|
||||
: 'Queued behind the current job.')
|
||||
await refreshStudioQueue()
|
||||
return
|
||||
}
|
||||
editStatusBusy.value = false
|
||||
editBusy.value = true
|
||||
stopListen('edit')
|
||||
editSettledUi = false
|
||||
editProgress.value = 2
|
||||
editResultUrl.value = ''
|
||||
editAwaitingReveal.value = false
|
||||
concealEditOutput.value = hideOut
|
||||
editLockedSave.value = false
|
||||
editStatusMessage.value = 'Checking Beast ComfyUI...'
|
||||
editMaxStep.value = started.steps
|
||||
persistActiveJob('edit', started.jobId, started.hideThumbnail === true || hideThumbnail.value, started.folderLocked === true)
|
||||
listen('edit', started.jobId, started.hideThumbnail === true || hideThumbnail.value, started.folderLocked === true)
|
||||
editChainStep.value = 1
|
||||
editChainTotal.value = 1 + extraPasses.length
|
||||
editChainLabel.value = extraPasses.length ? 'Pass 1' : ''
|
||||
editOverallProgress.value = 0
|
||||
editCompletedChainStep.value = 0
|
||||
editActiveChainPlan.value = [
|
||||
{ label: 'Pass 1', prompt: prompt.value.trim() },
|
||||
...extraPasses.map((item, index) => ({ label: `Pass ${index + 2}`, prompt: item.prompt }))
|
||||
]
|
||||
startTimer('edit')
|
||||
editDownloadName.value = downloadName
|
||||
editJobId.value = started.jobId
|
||||
outputStudioJobId.value = started.studioJobId || ''
|
||||
persistActiveJob('edit', started.jobId, started.hideThumbnail === true || hideOut, started.folderLocked === true)
|
||||
listen('edit', started.jobId, started.hideThumbnail === true || hideOut, started.folderLocked === true)
|
||||
void pollComfyHealth()
|
||||
void refreshStudioQueue()
|
||||
} catch (error: any) {
|
||||
editSettledUi = true
|
||||
const message = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Image edit failed'
|
||||
|
||||
Reference in New Issue
Block a user