Keep the Extend panel put and name sibling clips as branches.
A finished job no longer closes a draft or jumps the page, and a second extend from the same part saves as p8b instead of another p8. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+139
-96
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<div class="min-h-dvh min-w-0">
|
||||
<div class="min-h-dvh min-w-0 lg:flex lg:h-dvh lg:flex-col lg:overflow-hidden">
|
||||
<div class="shrink-0">
|
||||
<AppHeader
|
||||
:subtitle="ltxEnabled ? 'MiniMax H3 · LTX-2.3 · Flux.2 Klein' : 'MiniMax H3 · Flux.2 Klein'"
|
||||
current="studio"
|
||||
:queue-count="queueBadgeCount"
|
||||
@gpu="pollComfyHealth"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<main class="mx-auto grid min-w-0 max-w-7xl gap-6 px-4 py-6 sm:px-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] lg:items-stretch">
|
||||
<section id="studio-input" class="glass min-w-0 max-w-full rounded-3xl p-5 sm:p-6 space-y-5">
|
||||
<main class="mx-auto grid min-w-0 max-w-7xl flex-1 gap-6 px-4 py-6 sm:px-6 lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] lg:items-stretch lg:overflow-hidden">
|
||||
<section id="studio-input" class="glass min-w-0 max-w-full space-y-5 rounded-3xl p-5 sm:p-6 lg:min-h-0 lg:overflow-y-auto">
|
||||
<div>
|
||||
<div class="mb-4 grid grid-cols-2 gap-3">
|
||||
<button
|
||||
@@ -1207,8 +1209,8 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex min-w-0 max-w-full flex-col gap-5 lg:h-full lg:min-h-0">
|
||||
<div ref="outputPanel" class="glass min-w-0 max-w-full rounded-3xl p-5 sm:p-6 lg:shrink-0">
|
||||
<section class="flex min-w-0 max-w-full flex-col gap-5 lg:h-full lg:min-h-0 lg:overflow-hidden">
|
||||
<div ref="outputPanel" class="glass min-w-0 max-w-full rounded-3xl p-5 sm:p-6 lg:max-h-[min(58dvh,38rem)] lg:shrink-0 lg:overflow-y-auto">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="font-display text-2xl font-bold">Output</h2>
|
||||
@@ -1298,15 +1300,15 @@
|
||||
|
||||
<div v-show="outputPreviewKind === 'video'">
|
||||
<div
|
||||
class="relative mx-auto mt-5 w-full max-w-full overflow-hidden rounded-2xl bg-black/50"
|
||||
class="relative mx-auto mt-5 w-full max-w-full overflow-hidden rounded-2xl bg-black/50 lg:sticky lg:top-0 lg:z-10"
|
||||
:class="{ 'sensitive-glow': awaitingReveal || lockedSave }"
|
||||
:style="{ aspectRatio: `${playerSize.width} / ${playerSize.height}`, maxHeight: 'min(70dvh, 32rem)' }"
|
||||
:style="{ aspectRatio: `${playerSize.width} / ${playerSize.height}`, maxHeight: 'min(36dvh, 20rem)' }"
|
||||
>
|
||||
<video
|
||||
v-if="videoUrl && !lockedSave"
|
||||
ref="player"
|
||||
:src="videoUrl"
|
||||
class="h-full w-full max-h-[min(70dvh,32rem)] bg-black object-contain"
|
||||
class="h-full w-full max-h-[min(36dvh,20rem)] bg-black object-contain"
|
||||
:class="{ 'opacity-0': awaitingReveal }"
|
||||
:controls="!awaitingReveal"
|
||||
:autoplay="autoplayEnabled && !awaitingReveal"
|
||||
@@ -1394,7 +1396,10 @@
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-amber-100">Extend this clip</h3>
|
||||
<p class="mt-1 text-xs text-zinc-500">Continue from the last frame with a new motion prompt, then stitch the segments together.{{ extendSaveName ? ` Saves as “${extendSaveName}”.` : '' }}</p>
|
||||
<p v-if="extendSourceLabel" class="mt-1 text-xs text-amber-200/90">
|
||||
Extending {{ extendSourceLabel }}{{ extendSaveName ? ` · next save “${extendSaveName}”` : '' }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-zinc-500">Continue from the last frame with a new motion prompt, then stitch the segments together. Other jobs finishing will not close this panel.</p>
|
||||
</div>
|
||||
<button type="button" class="text-xs text-zinc-400 hover:text-white" @click="closeExtend">Close</button>
|
||||
</div>
|
||||
@@ -1563,7 +1568,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass rounded-3xl p-5 sm:p-6 lg:flex lg:min-h-[24rem] lg:flex-1 lg:flex-col">
|
||||
<div class="glass rounded-3xl p-5 sm:p-6 lg:flex lg:min-h-0 lg:flex-1 lg:flex-col lg:overflow-hidden">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h2 class="font-display text-xl font-bold">Library</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -1594,7 +1599,7 @@
|
||||
</form>
|
||||
|
||||
<template v-else>
|
||||
<div class="lg:flex lg:min-h-0 lg:flex-1 lg:flex-col">
|
||||
<div class="lg:flex lg:min-h-0 lg:flex-1 lg:flex-col lg:overflow-hidden">
|
||||
<div class="mt-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex min-w-0 flex-wrap gap-1.5">
|
||||
<button
|
||||
@@ -1711,6 +1716,7 @@
|
||||
/>
|
||||
</div>
|
||||
<p class="truncate text-xs text-zinc-500">{{ clipMetaLine(entry.group.head) }}</p>
|
||||
<p v-if="familyBranchHint(entry.group.parts)" class="mt-1 truncate text-[11px] text-amber-200/80">{{ familyBranchHint(entry.group.parts) }}</p>
|
||||
<p v-if="entry.group.head.globalLocks" class="mt-1 text-[11px] text-zinc-600">Global locks saved with this collection.</p>
|
||||
<PermanenceRefStrip
|
||||
v-if="entry.group.parts.length === 1 && !entry.group.head.hideThumbnail"
|
||||
@@ -1725,7 +1731,7 @@
|
||||
/>
|
||||
<div class="mt-2 flex flex-wrap items-center gap-3">
|
||||
<button v-if="entry.group.parts.length > 1" type="button" class="text-xs text-amber-200" @click="toggleFamily(entry.group.key)">
|
||||
{{ expandedFamilies[entry.group.key] ? 'Collapse' : `Expand ${entry.group.parts.length} parts` }}
|
||||
{{ familyExpandLabel(entry.group.parts, expandedFamilies[entry.group.key]) }}
|
||||
</button>
|
||||
<button
|
||||
v-if="entry.group.parts.length > 1 && entry.group.parts.some(part => part.hideThumbnail)"
|
||||
@@ -1768,89 +1774,98 @@
|
||||
</div>
|
||||
<ul v-if="expandedFamilies[entry.group.key] && entry.group.parts.length > 1" class="mt-3 space-y-2 border-t border-white/5 pt-3">
|
||||
<li
|
||||
v-for="(item, partIndex) in chronologicalParts(entry.group.parts)"
|
||||
:key="item.id"
|
||||
v-for="node in treeOrderedParts(entry.group.parts)"
|
||||
:key="node.clip.id"
|
||||
class="flex cursor-pointer flex-wrap items-start gap-3 rounded-xl border p-2"
|
||||
:class="item.id === reviewingClipId ? 'border-amber-300/80 bg-amber-400/15 ring-1 ring-amber-300/60' : 'border-white/5 bg-black/20'"
|
||||
:aria-current="item.id === reviewingClipId ? 'true' : undefined"
|
||||
@click="onLibraryClipPartClick(item, $event)"
|
||||
:class="node.clip.id === reviewingClipId
|
||||
? 'border-amber-300/80 bg-amber-400/15 ring-1 ring-amber-300/60'
|
||||
: extendOpen && node.clip.id === extendClipId
|
||||
? 'border-amber-300/40 bg-amber-400/5'
|
||||
: 'border-white/5 bg-black/20'"
|
||||
:style="node.depth ? { marginLeft: `${Math.min(node.depth, 6) * 0.85}rem` } : undefined"
|
||||
:aria-current="node.clip.id === reviewingClipId ? 'true' : undefined"
|
||||
@click="onLibraryClipPartClick(node.clip, $event)"
|
||||
>
|
||||
<ClipFrameThumbs
|
||||
:hidden="item.hideThumbnail"
|
||||
:revealed="!!stitchReveal[item.id]"
|
||||
:still-id="partIndex === 0 ? item.stillId : undefined"
|
||||
:from-clip-id="previousClipIdFor(item, entry.group.parts, partIndex)"
|
||||
:clip-id="item.id"
|
||||
:show-first-frame="partIndex > 0 || !item.stillId || Boolean(item.parentClipId)"
|
||||
@update:revealed="stitchReveal[item.id] = $event"
|
||||
:hidden="node.clip.hideThumbnail"
|
||||
:revealed="!!stitchReveal[node.clip.id]"
|
||||
:still-id="node.depth === 0 ? node.clip.stillId : undefined"
|
||||
:from-clip-id="previousClipIdFor(node.clip, entry.group.parts)"
|
||||
:clip-id="node.clip.id"
|
||||
:show-first-frame="node.depth > 0 || !node.clip.stillId || Boolean(node.clip.parentClipId)"
|
||||
@update:revealed="stitchReveal[node.clip.id] = $event"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start gap-2">
|
||||
<input
|
||||
v-if="renamingId === item.id && !renamingFamily"
|
||||
v-if="renamingId === node.clip.id && !renamingFamily"
|
||||
v-model="renameDraft"
|
||||
class="min-w-0 flex-1 rounded-lg border border-amber-300/40 bg-zinc-950 px-2 py-1 text-sm outline-none ring-amber-300/40 focus:ring-2"
|
||||
maxlength="80"
|
||||
@keydown.enter.prevent="saveRename(item)"
|
||||
@keydown.enter.prevent="saveRename(node.clip)"
|
||||
@keydown.escape.prevent="cancelRename"
|
||||
@blur="saveRename(item)"
|
||||
@blur="saveRename(node.clip)"
|
||||
>
|
||||
<p v-else class="min-w-0 flex-1 truncate text-sm font-medium">{{ item.name || item.prompt }}</p>
|
||||
<p v-else class="min-w-0 flex-1 truncate text-sm font-medium">
|
||||
{{ node.clip.name || node.clip.prompt }}
|
||||
<span v-if="clipBranchFromLabel(node.clip, entry.group.parts)" class="ml-1.5 text-[11px] font-normal text-amber-200/80">{{ clipBranchFromLabel(node.clip, entry.group.parts) }}</span>
|
||||
<span v-if="extendOpen && node.clip.id === extendClipId" class="ml-1.5 text-[10px] font-semibold uppercase tracking-wide text-amber-200">extending</span>
|
||||
</p>
|
||||
<PromptPreviewTip
|
||||
v-if="formatPromptParts(item)"
|
||||
:text="formatPromptParts(item)"
|
||||
:aria-label="'Copy prompt for ' + (item.name || 'this clip')"
|
||||
v-if="formatPromptParts(node.clip)"
|
||||
:text="formatPromptParts(node.clip)"
|
||||
:aria-label="'Copy prompt for ' + (node.clip.name || 'this clip')"
|
||||
@copy="copyPrompt"
|
||||
/>
|
||||
</div>
|
||||
<p class="truncate text-xs text-zinc-500">{{ clipMetaLine(item) }}</p>
|
||||
<p class="truncate text-xs text-zinc-500">{{ clipMetaLine(node.clip) }}</p>
|
||||
<PermanenceRefStrip
|
||||
v-if="!item.hideThumbnail"
|
||||
v-if="!node.clip.hideThumbnail"
|
||||
class="mt-2"
|
||||
:refs="item.permanenceRefs || []"
|
||||
:name-options="clipBindingKeys(item, entry.group.head)"
|
||||
:refs="node.clip.permanenceRefs || []"
|
||||
:name-options="clipBindingKeys(node.clip, entry.group.head)"
|
||||
cta="Add still"
|
||||
:hidden="item.hideInput || hideInputPreview"
|
||||
@add-ref="openPermanencePicker(undefined, item.id)"
|
||||
@remove="(refIndex) => removeClipRef(item, refIndex)"
|
||||
@update-label="(refIndex, label) => updateClipRefLabel(item, refIndex, label)"
|
||||
:hidden="node.clip.hideInput || hideInputPreview"
|
||||
@add-ref="openPermanencePicker(undefined, node.clip.id)"
|
||||
@remove="(refIndex) => removeClipRef(node.clip, refIndex)"
|
||||
@update-label="(refIndex, label) => updateClipRefLabel(node.clip, refIndex, label)"
|
||||
/>
|
||||
<div class="mt-1.5 flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
v-if="item.hideThumbnail"
|
||||
v-if="node.clip.hideThumbnail"
|
||||
type="button"
|
||||
class="text-xs text-amber-300"
|
||||
@click="stitchReveal[item.id] = !stitchReveal[item.id]"
|
||||
@click="stitchReveal[node.clip.id] = !stitchReveal[node.clip.id]"
|
||||
>
|
||||
{{ stitchReveal[item.id] ? 'Hide frames' : 'Click to reveal' }}
|
||||
{{ stitchReveal[node.clip.id] ? 'Hide frames' : 'Click to reveal' }}
|
||||
</button>
|
||||
<button type="button" class="text-xs text-amber-300" @click="openClip(item)">Open</button>
|
||||
<button type="button" class="text-xs text-amber-300" @click="useClipAsInput(item)">Use as input</button>
|
||||
<button type="button" class="text-xs text-amber-300" @click="openExtend(item)">Extend</button>
|
||||
<button type="button" class="text-xs text-amber-300" @click="startRename(item)">Rename</button>
|
||||
<button type="button" class="text-xs text-zinc-400" @click="toggleDetails(item.id)">
|
||||
{{ expandedDetails[item.id] ? 'Hide details' : 'Details' }}
|
||||
<button type="button" class="text-xs text-amber-300" @click="openClip(node.clip)">Open</button>
|
||||
<button type="button" class="text-xs text-amber-300" @click="useClipAsInput(node.clip)">Use as input</button>
|
||||
<button type="button" class="text-xs text-amber-300" @click="openExtend(node.clip)">Extend</button>
|
||||
<button type="button" class="text-xs text-amber-300" @click="startRename(node.clip)">Rename</button>
|
||||
<button type="button" class="text-xs text-zinc-400" @click="toggleDetails(node.clip.id)">
|
||||
{{ expandedDetails[node.clip.id] ? 'Hide details' : 'Details' }}
|
||||
</button>
|
||||
<select
|
||||
v-if="folders.length > 1"
|
||||
class="max-w-[9.5rem] rounded-lg border border-white/10 bg-zinc-950 px-1.5 py-0.5 text-xs text-amber-300"
|
||||
:value="''"
|
||||
@change="onMoveClip(item, $event)"
|
||||
@change="onMoveClip(node.clip, $event)"
|
||||
>
|
||||
<option disabled value="">Move to…</option>
|
||||
<option
|
||||
v-for="folder in folders.filter(folder => folder.id !== item.folderId)"
|
||||
v-for="folder in folders.filter(folder => folder.id !== node.clip.folderId)"
|
||||
:key="folder.id"
|
||||
:value="folder.id"
|
||||
>
|
||||
{{ folder.name }}{{ folder.protected && !folder.unlocked ? ' · locked' : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" class="text-xs text-red-300" @click="askRemoveClip(item)">Delete</button>
|
||||
<button type="button" class="text-xs text-red-300" @click="askRemoveClip(node.clip)">Delete</button>
|
||||
</div>
|
||||
<LibrarySettingsDetails
|
||||
v-if="expandedDetails[item.id]"
|
||||
:rows="clipSettingsRows(item)"
|
||||
v-if="expandedDetails[node.clip.id]"
|
||||
:rows="clipSettingsRows(node.clip)"
|
||||
empty="No generator settings were saved with this clip."
|
||||
/>
|
||||
</div>
|
||||
@@ -2215,6 +2230,15 @@ import {
|
||||
import { parseImageAspect, generateSizeForAspect } from '~/utils/imageAspect'
|
||||
import { buildOutputBaseName, nextUniqueName } from '~/utils/outputNames'
|
||||
import { emptyKeepPrompts, insertKeepSnippet, normalizeKeepPrompts, type KeepPrompt, type KeepPromptLists } from '~/utils/keepPrompts'
|
||||
import {
|
||||
clipBranchFromLabel,
|
||||
clipPartLabel,
|
||||
familyBaseName,
|
||||
familyBranchCount,
|
||||
familyBranchHint,
|
||||
nextClipPartName,
|
||||
treeOrderedParts
|
||||
} from '~/utils/libraryBrowse'
|
||||
import {
|
||||
IMAGE_V2_CFG_DEFAULT,
|
||||
IMAGE_V2_CONSISTENCY_LORA,
|
||||
@@ -2997,7 +3021,21 @@ const canExtend = computed(() => Boolean(
|
||||
&& !lockedSave.value
|
||||
&& (currentClipId.value || clipIdFromPlayer())
|
||||
))
|
||||
const extendSaveName = computed(() => nextClipPartName(currentClip.value?.name || currentClip.value?.prompt || ''))
|
||||
const extendSourceClip = computed(() => clips.value.find(clip => clip.id === extendClipId.value) || currentClip.value)
|
||||
const extendSourceLabel = computed(() => {
|
||||
const clip = extendSourceClip.value
|
||||
if (!clip) return ''
|
||||
return clip.name || clipPartLabel(clip.name) || 'this clip'
|
||||
})
|
||||
const extendSaveName = computed(() => {
|
||||
const clip = extendSourceClip.value
|
||||
if (!clip) return ''
|
||||
const reserved = [
|
||||
...familyPartsFor(clip).map(part => part.name || ''),
|
||||
...studioJobs.value.map(job => job.name || '')
|
||||
]
|
||||
return nextClipPartName(clip.name || clip.prompt || '', reserved)
|
||||
})
|
||||
const queueReady = computed(() => {
|
||||
if (shotScriptMode.value) return parsedShots.value.length > 0 && parsedShots.value.every(shot => shot.prompt.trim())
|
||||
return extensionQueue.value.every(item => composePromptParts(item.promptPre, item.prompt, item.promptPost))
|
||||
@@ -3392,10 +3430,6 @@ function folderOutputCount(folder: LibraryFolder) {
|
||||
return videos + images
|
||||
}
|
||||
|
||||
function familyBaseName(name: string) {
|
||||
return String(name || '').trim().replace(/\s+p\d+$/i, '')
|
||||
}
|
||||
|
||||
function clipFamilyKey(clip: LibraryClip) {
|
||||
if (clip.familyId) return `family:${clip.folderId}:${clip.familyId}`
|
||||
const base = familyBaseName(clip.name || '').toLowerCase()
|
||||
@@ -3438,18 +3472,31 @@ function chronologicalParts(parts: LibraryClip[]) {
|
||||
})
|
||||
}
|
||||
|
||||
function previousClipIdFor(item: LibraryClip, parts: LibraryClip[], index: number) {
|
||||
function previousClipIdFor(item: LibraryClip, parts: LibraryClip[]) {
|
||||
if (item.parentClipId) return item.parentClipId
|
||||
if (index <= 0) return undefined
|
||||
const ordered = chronologicalParts(parts)
|
||||
const current = ordered[index]
|
||||
const currentChain = current?.chainIndex || 0
|
||||
const index = ordered.findIndex(part => part.id === item.id)
|
||||
if (index <= 0) return undefined
|
||||
const currentChain = item.chainIndex || 0
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
if ((ordered[i].chainIndex || 0) < currentChain) return ordered[i].id
|
||||
}
|
||||
return ordered[index - 1]?.id
|
||||
}
|
||||
|
||||
function familyExpandLabel(parts: LibraryClip[], expanded?: boolean) {
|
||||
if (expanded) return 'Collapse'
|
||||
const branches = familyBranchCount(parts)
|
||||
if (branches) return `Expand ${parts.length} parts · ${branches} branch${branches === 1 ? '' : 'es'}`
|
||||
return `Expand ${parts.length} parts`
|
||||
}
|
||||
|
||||
function ensureFamilyExpanded(item: LibraryClip) {
|
||||
const key = clipFamilyKey(item)
|
||||
if (expandedFamilies.value[key]) return
|
||||
expandedFamilies.value = { ...expandedFamilies.value, [key]: true }
|
||||
}
|
||||
|
||||
function collectionTitle(group: { head: LibraryClip; parts: LibraryClip[] }) {
|
||||
const named = group.parts.map(part => familyBaseName(part.name || '')).find(name => name)
|
||||
return named || group.head.name || group.head.prompt
|
||||
@@ -3946,7 +3993,7 @@ watch(hideThumbnail, (value) => {
|
||||
|
||||
watch(hideExtendLastFrame, (hidden) => {
|
||||
if (hidden || !extendOpen.value) return
|
||||
const id = currentClipId.value || clipIdFromPlayer()
|
||||
const id = extendClipId.value || currentClipId.value || clipIdFromPlayer()
|
||||
if (id && !lastFrameUrl.value && !lastFrameLoading.value) void loadLastFrame(id)
|
||||
})
|
||||
|
||||
@@ -5128,7 +5175,7 @@ async function refreshStudioQueue() {
|
||||
} else if (running.id && running.liveJobId === editJobId.value) {
|
||||
outputStudioJobId.value = running.id
|
||||
}
|
||||
} else if (running?.liveJobId && running.liveJobId !== videoJobId.value && !videoBusy.value && !watchingLiveVideo()) {
|
||||
} else if (running?.liveJobId && running.liveJobId !== videoJobId.value && !videoBusy.value && !watchingLiveVideo() && !extendOpen.value) {
|
||||
outputStudioJobId.value = running.id
|
||||
videoJobId.value = running.liveJobId
|
||||
persistActiveJob('video', running.liveJobId, running.hideThumbnail === true, running.folderLocked === true)
|
||||
@@ -5548,16 +5595,6 @@ function stillFileName(still?: Pick<LibraryStill, 'name' | 'filename'> | null) {
|
||||
return still?.filename || 'still.png'
|
||||
}
|
||||
|
||||
function nextClipPartName(name: string) {
|
||||
const trimmed = (name || '').trim()
|
||||
if (!trimmed) return ''
|
||||
const match = trimmed.match(/^(.*)\s+p(\d+)$/i)
|
||||
const base = (match ? match[1] : trimmed).trim() || trimmed
|
||||
const part = match ? Number(match[2]) + 1 : 2
|
||||
const suffix = ` p${part}`
|
||||
return `${base.slice(0, Math.max(1, 80 - suffix.length))}${suffix}`
|
||||
}
|
||||
|
||||
function clipGenerateDuration(item: LibraryClip) {
|
||||
if (item.parentClipId) {
|
||||
const parent = clips.value.find(clip => clip.id === item.parentClipId)
|
||||
@@ -5794,7 +5831,7 @@ async function loadLastFrame(id: string) {
|
||||
|
||||
function revealExtendLastFrame() {
|
||||
lastFrameRevealed.value = true
|
||||
const id = currentClipId.value || clipIdFromPlayer()
|
||||
const id = extendClipId.value || currentClipId.value || clipIdFromPlayer()
|
||||
if (id && !lastFrameUrl.value && !lastFrameLoading.value) void loadLastFrame(id)
|
||||
}
|
||||
|
||||
@@ -5808,6 +5845,7 @@ async function openExtend(item?: LibraryClip) {
|
||||
currentClipId.value = id
|
||||
extendClipId.value = id
|
||||
const clip = clips.value.find(entry => entry.id === id)
|
||||
if (clip) ensureFamilyExpanded(clip)
|
||||
const parts = restorePromptParts(clip?.prompt || prompt.value, {
|
||||
pre: clip ? (clip.promptPre || '') : promptPre.value,
|
||||
prompt: clip?.prompt || prompt.value,
|
||||
@@ -5823,8 +5861,6 @@ async function openExtend(item?: LibraryClip) {
|
||||
lastFrameUrl.value = ''
|
||||
lastFrameLoading.value = false
|
||||
player.value?.pause()
|
||||
await nextTick()
|
||||
document.getElementById('extend-clip')?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
||||
if (!hideExtendLastFrame.value) await loadLastFrame(id)
|
||||
}
|
||||
|
||||
@@ -5848,6 +5884,7 @@ async function generateExtension() {
|
||||
folderLocked?: boolean
|
||||
width?: number
|
||||
height?: number
|
||||
name?: string
|
||||
}>('/api/extend', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
@@ -5860,29 +5897,27 @@ async function generateExtension() {
|
||||
}
|
||||
})
|
||||
if (queuedOffLiveOutput(started, 'video')) {
|
||||
if (watchingLiveVideo()) {
|
||||
toast('Queued behind the current job. Output stays on the clip that is generating.')
|
||||
await refreshStudioQueue()
|
||||
return
|
||||
toast(started.name
|
||||
? `Queued “${started.name}”. Stay here to queue another branch.`
|
||||
: 'Queued behind the current job. This panel stays open.')
|
||||
if (!watchingLiveVideo()) {
|
||||
outputStudioJobId.value = started.studioJobId || ''
|
||||
outputFocus.value = 'video'
|
||||
videoAwaitingBeast.value = true
|
||||
statusMessage.value = 'Queued. Waiting for Beast to be free.'
|
||||
}
|
||||
toast('Queued behind the current job.')
|
||||
outputStudioJobId.value = started.studioJobId || ''
|
||||
outputFocus.value = 'video'
|
||||
videoAwaitingBeast.value = true
|
||||
statusMessage.value = 'Queued. Waiting for Beast to be free.'
|
||||
closeExtend()
|
||||
await refreshStudioQueue()
|
||||
return
|
||||
}
|
||||
closeExtend()
|
||||
statusBusy.value = false
|
||||
videoBusy.value = true
|
||||
watchingLibrary.value = false
|
||||
stopListen('video')
|
||||
videoSettledUi = false
|
||||
progress.value = 1
|
||||
awaitingReveal.value = false
|
||||
concealOutput.value = started.hideThumbnail === true || hideOut
|
||||
if (!extendOpen.value && !watchingLibrary.value) {
|
||||
awaitingReveal.value = false
|
||||
concealOutput.value = started.hideThumbnail === true || hideOut
|
||||
}
|
||||
lockedSave.value = false
|
||||
chainStep.value = 1
|
||||
chainTotal.value = 1
|
||||
@@ -5898,6 +5933,9 @@ async function generateExtension() {
|
||||
maxStep.value = started.steps
|
||||
persistActiveJob('video', started.jobId, started.hideThumbnail === true || concealOutput.value, started.folderLocked === true)
|
||||
listen('video', started.jobId, started.hideThumbnail === true || concealOutput.value, started.folderLocked === true)
|
||||
toast(started.name
|
||||
? `Started “${started.name}”. This panel stays open so you can queue another branch.`
|
||||
: 'Extension started. This panel stays open.')
|
||||
void pollComfyHealth()
|
||||
void refreshStudioQueue()
|
||||
} catch (error: any) {
|
||||
@@ -5916,8 +5954,8 @@ async function generateExtension() {
|
||||
function openClip(item: LibraryClip) {
|
||||
studioMode.value = 'video'
|
||||
outputFocus.value = 'video'
|
||||
closeExtend()
|
||||
watchingLibrary.value = true
|
||||
ensureFamilyExpanded(item)
|
||||
currentClipId.value = item.id
|
||||
lockedSave.value = false
|
||||
concealOutput.value = item.hideThumbnail
|
||||
@@ -7342,6 +7380,12 @@ function applyEditEvent(payload: Record<string, any>, hidden: boolean, folderLoc
|
||||
}
|
||||
}
|
||||
|
||||
function keepPlayerOnCurrentView(incomingClipId?: string) {
|
||||
if (extendOpen.value) return true
|
||||
if (watchingLibrary.value && currentClipId.value && incomingClipId && incomingClipId !== currentClipId.value) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function applyVideoEvent(payload: Record<string, any>, hidden: boolean, folderLocked: boolean) {
|
||||
if (payload.queueId) persistActiveQueue(String(payload.queueId))
|
||||
if (payload.message) statusMessage.value = payload.message
|
||||
@@ -7364,7 +7408,7 @@ function applyVideoEvent(payload: Record<string, any>, hidden: boolean, folderLo
|
||||
scheduleLibraryRefresh()
|
||||
void refreshLiveQueue()
|
||||
const locked = folderLocked || payload.folderLocked === true
|
||||
if (!watchingLibrary.value && !locked) {
|
||||
if (!keepPlayerOnCurrentView(payload.clipId) && !locked) {
|
||||
currentClipId.value = payload.clipId
|
||||
videoUrl.value = clipVideoUrl(payload.clipId)
|
||||
concealOutput.value = hidden || payload.hideThumbnail === true
|
||||
@@ -7397,7 +7441,7 @@ function applyVideoEvent(payload: Record<string, any>, hidden: boolean, folderLo
|
||||
const locked = dest
|
||||
? Boolean(dest.protected && !dest.unlocked)
|
||||
: (folderLocked || payload.folderLocked === true)
|
||||
if (!watchingLibrary.value && !locked && payload.clipId) {
|
||||
if (!keepPlayerOnCurrentView(payload.clipId) && !locked && payload.clipId) {
|
||||
currentClipId.value = payload.clipId
|
||||
videoUrl.value = clipVideoUrl(payload.clipId)
|
||||
concealOutput.value = hidden || payload.hideThumbnail === true
|
||||
@@ -7424,8 +7468,7 @@ function applyVideoEvent(payload: Record<string, any>, hidden: boolean, folderLo
|
||||
const locked = dest
|
||||
? Boolean(dest.protected && !dest.unlocked)
|
||||
: (folderLocked || payload.folderLocked === true)
|
||||
const keepLibraryView = watchingLibrary.value && Boolean(currentClipId.value) && payload.clipId !== currentClipId.value
|
||||
if (keepLibraryView) {
|
||||
if (keepPlayerOnCurrentView(payload.clipId)) {
|
||||
videoBusy.value = false
|
||||
statusBusy.value = false
|
||||
stopTimer('video')
|
||||
|
||||
Reference in New Issue
Block a user