Copy generated stills into Input without loading the current frame.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-31 22:57:54 -05:00
co-authored by Cursor
parent 835a7bcd68
commit 5037c4fdeb
5 changed files with 169 additions and 11 deletions
+70
View File
@@ -1384,6 +1384,15 @@
>
Use the image I just generated as input
</button>
<button
v-if="canUseGeneratedStill"
type="button"
class="inline-flex rounded-2xl border border-white/15 px-4 py-2 text-sm hover:border-amber-300/50 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="keepingInputBusy"
@click="saveEditAsInput"
>
{{ keepingInputBusy ? 'Saving…' : 'Save as input' }}
</button>
</div>
<div v-if="extendOpen" class="mt-4 space-y-4 rounded-2xl border border-white/10 bg-zinc-950/50 p-4">
<div class="flex items-start justify-between gap-3">
@@ -1532,6 +1541,14 @@
>
Use as input still
</button>
<button
type="button"
class="inline-flex rounded-2xl border border-white/15 px-4 py-2 text-sm hover:border-amber-300/50 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="keepingInputBusy"
@click="saveEditAsInput"
>
{{ keepingInputBusy ? 'Saving…' : 'Save as input' }}
</button>
<button
type="button"
class="inline-flex rounded-2xl border border-amber-300/40 px-4 py-2 text-sm text-amber-100 hover:border-amber-300/70"
@@ -1903,6 +1920,13 @@
</button>
<button type="button" class="text-xs text-amber-300" @click="openStill(entry.stillGroup.head)">Open</button>
<button type="button" class="text-xs text-amber-300" @click="useStillAsInput(entry.stillGroup.head)">Use as input</button>
<button
v-if="entry.stillGroup.head.role !== 'input'"
type="button"
class="text-xs text-amber-300"
:disabled="keepingInputBusy"
@click="saveStillAsInput(entry.stillGroup.head)"
>Save as input</button>
<button type="button" class="text-xs text-zinc-400" @click="toggleDetails(entry.key)">
{{ expandedDetails[entry.key] ? 'Hide details' : 'Details' }}
</button>
@@ -1972,6 +1996,13 @@
<div class="mt-1.5 flex flex-wrap items-center gap-3">
<button type="button" class="text-xs text-amber-300" @click="openStill(item)">Open</button>
<button type="button" class="text-xs text-amber-300" @click="useStillAsInput(item)">Use as input</button>
<button
v-if="item.role !== 'input'"
type="button"
class="text-xs text-amber-300"
:disabled="keepingInputBusy"
@click="saveStillAsInput(item)"
>Save as input</button>
<button type="button" class="text-xs text-zinc-400" @click="toggleDetails(item.id)">
{{ expandedDetails[item.id] ? 'Hide details' : 'Details' }}
</button>
@@ -2395,6 +2426,7 @@ const editLightbox = ref<HTMLElement | null>(null)
const editLightboxUrl = ref('')
const file = ref<File | null>(null)
const inputStillId = ref('')
const keepingInputBusy = ref(false)
const renamingInputStill = ref(false)
const inputStillNameDraft = ref('')
const inputStillRenameEl = ref<HTMLInputElement | null>(null)
@@ -5850,6 +5882,44 @@ function stillUseAsInputMessage(still: LibraryStill) {
return 'Loaded as input. It stays in Output, not the upload library.'
}
async function saveStillAsInput(still: Pick<LibraryStill, 'id'>) {
if (keepingInputBusy.value) return
keepingInputBusy.value = true
try {
applyLibrary(await $fetch(`/api/library/stills/${still.id}/keep-input`, { method: 'POST' }))
toast('Saved to Input. Pick it later from Choose an image → Input.')
} catch (error: any) {
toast(error?.data?.statusMessage || 'Could not save that still as input')
} finally {
keepingInputBusy.value = false
}
}
async function saveEditAsInput() {
const stillId = stillIdFromLibraryUrl(editResultUrl.value)
if (stillId) {
await saveStillAsInput({ id: stillId })
return
}
if (!editResultUrl.value) return
if (keepingInputBusy.value) return
keepingInputBusy.value = true
try {
const blob = await $fetch<Blob>(editResultUrl.value, { responseType: 'blob' })
if (!blob?.size) throw new Error('empty still')
const body = new FormData()
body.append('image', new File([blob], `${editDownloadName.value || 'edited'}.png`, { type: blob.type || 'image/png' }))
body.append('folderId', folderId.value)
body.append('hideInput', String(hideInputPreview.value))
applyLibrary(await $fetch('/api/library/stills', { method: 'POST', body }))
toast('Saved to Input. Pick it later from Choose an image → Input.')
} catch (error: any) {
toast(error?.data?.statusMessage || 'Could not save that still as input')
} finally {
keepingInputBusy.value = false
}
}
async function useStillAsInput(still: LibraryStill) {
try {
applyStillSettings(still)
+27
View File
@@ -245,6 +245,13 @@
<div class="mt-4 flex flex-wrap gap-2">
<button type="button" class="rounded-full border border-amber-300/40 px-3 py-1.5 text-xs text-amber-100" @click="openInStudio(focused)">Open in Studio</button>
<button type="button" class="rounded-full border border-white/10 px-3 py-1.5 text-xs text-zinc-300 hover:text-white" @click="useInStudio(focused)">Use as input</button>
<button
v-if="focused.kind === 'still' && focusedStillRole !== 'input'"
type="button"
class="rounded-full border border-white/10 px-3 py-1.5 text-xs text-zinc-300 hover:text-white"
:disabled="keepingInputBusy"
@click="saveFocusedAsInput"
>{{ keepingInputBusy ? 'Saving…' : 'Save as input' }}</button>
<a
v-if="downloadHref"
:href="downloadHref"
@@ -374,6 +381,7 @@ const renameInput = ref<HTMLInputElement | null>(null)
const deleteBusy = ref(false)
const detailsHidden = ref(false)
const pendingDelete = ref<{ title: string; name: string; detail?: string; consequence: string; keys: string[] } | null>(null)
const keepingInputBusy = ref(false)
const toasts = ref<Array<{ id: number; message: string }>>([])
let toastId = 0
let searchTimer: ReturnType<typeof setTimeout> | null = null
@@ -420,6 +428,11 @@ const folderChipCounts = computed(() => {
})
const moveFolders = computed(() => folders.value.filter(folder => folder.id !== folderId.value))
const focused = computed(() => entries.value.find(entry => entry.key === focusedKey.value) || null)
const focusedStill = computed(() => {
if (focused.value?.kind !== 'still' || !focused.value.stillGroup) return null
return focused.value.stillGroup.parts.find(part => part.id === focusedPartId.value) || focused.value.stillGroup.head
})
const focusedStillRole = computed(() => focusedStill.value?.role === 'input' ? 'input' : (focusedStill.value ? (focusedStill.value.role || 'output') : ''))
const emptyLabel = computed(() => {
if (search.value.trim()) return 'Nothing matches that search.'
if (filter.value === 'images') return 'No generated stills here yet.'
@@ -716,6 +729,20 @@ function useInStudio(entry: LibraryEntry) {
}
}
async function saveFocusedAsInput() {
const still = focusedStill.value
if (!still || keepingInputBusy.value) return
keepingInputBusy.value = true
try {
applyLibrary(await $fetch(`/api/library/stills/${still.id}/keep-input`, { method: 'POST' }))
toast('Saved to Input. Pick it later from Choose an image → Input.')
} catch (error: any) {
toast(error?.data?.statusMessage || 'Could not save that still as input')
} finally {
keepingInputBusy.value = false
}
}
function startRename() {
if (!focused.value) return
renaming.value = true