diff --git a/components/ImagePickerModal.vue b/components/ImagePickerModal.vue
index 6730a6b..49c3b27 100644
--- a/components/ImagePickerModal.vue
+++ b/components/ImagePickerModal.vue
@@ -122,16 +122,30 @@
-
-
-
-
-
+
@@ -195,6 +209,7 @@ const tab = ref<'input' | 'output'>('input')
const revealAll = ref(false)
const revealedIds = ref([])
const picking = ref(false)
+const keepingId = ref('')
function onKey(event: KeyboardEvent) {
if (!props.open) return
@@ -223,7 +238,7 @@ const pickerHint = computed(() => {
if (props.stillOnly) return 'Pick a still, then type a short name on the strip (or pick a GLOBAL LOCKS key). That name is what the shot prompt inherits.'
return tab.value === 'input'
? 'Uploaded stills only. Generated frames and edits live under Output.'
- : 'Generated stills from image edits. Video frames stay with their clips.'
+ : 'Generated stills. Checkmark loads the current frame. Arrow copies the image into Input for later. Video frames stay with their clips.'
})
const hasHiddenItems = computed(() => visibleStills.value.some(still => stillHidden(still)))
@@ -301,6 +316,19 @@ function onDrop(event: DragEvent) {
if (next) void ingest(next)
}
+async function keepAsInput(still: PickerStill) {
+ if (keepingId.value) return
+ keepingId.value = still.id
+ try {
+ emit('updated', await $fetch(`/api/library/stills/${still.id}/keep-input`, { method: 'POST' }))
+ tab.value = 'input'
+ } catch (error: any) {
+ emit('error', error?.data?.statusMessage || 'Could not save that still as input')
+ } finally {
+ keepingId.value = ''
+ }
+}
+
async function pickStill(still: PickerStill) {
if (picking.value) return
picking.value = true
diff --git a/pages/index.vue b/pages/index.vue
index 34ad3d0..1ef6f29 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -1384,6 +1384,15 @@
>
Use the image I just generated as input
+
+ {{ keepingInputBusy ? 'Saving…' : 'Save as input' }}
+
@@ -1532,6 +1541,14 @@
>
Use as input still
+
+ {{ keepingInputBusy ? 'Saving…' : 'Save as input' }}
+
Open
Use as input
+ Save as input
{{ expandedDetails[entry.key] ? 'Hide details' : 'Details' }}
@@ -1972,6 +1996,13 @@
Open
Use as input
+
Save as input
{{ expandedDetails[item.id] ? 'Hide details' : 'Details' }}
@@ -2395,6 +2426,7 @@ const editLightbox = ref
(null)
const editLightboxUrl = ref('')
const file = ref(null)
const inputStillId = ref('')
+const keepingInputBusy = ref(false)
const renamingInputStill = ref(false)
const inputStillNameDraft = ref('')
const inputStillRenameEl = ref(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) {
+ 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(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)
diff --git a/pages/library.vue b/pages/library.vue
index fa3a963..95fa2e7 100644
--- a/pages/library.vue
+++ b/pages/library.vue
@@ -245,6 +245,13 @@
Open in Studio
Use as input
+
{{ keepingInputBusy ? 'Saving…' : 'Save as input' }}
(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>([])
let toastId = 0
let searchTimer: ReturnType | 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
diff --git a/server/api/library/stills/[id]/keep-input.post.ts b/server/api/library/stills/[id]/keep-input.post.ts
new file mode 100644
index 0000000..7a63430
--- /dev/null
+++ b/server/api/library/stills/[id]/keep-input.post.ts
@@ -0,0 +1,10 @@
+import { assertFolderAccess, getStill, keepStillAsInput, libraryOwnerKey, publicLibrary } from '~/server/utils/library'
+
+export default defineEventHandler(async (event) => {
+ const id = String(getRouterParam(event, 'id') || '')
+ const owner = libraryOwnerKey(event)
+ const still = getStill(owner, id)
+ assertFolderAccess(event, still.folderId)
+ const saved = await keepStillAsInput(owner, id)
+ return { still: saved, ...publicLibrary(event) }
+})
diff --git a/server/utils/library.ts b/server/utils/library.ts
index d793bbf..b6dd51b 100644
--- a/server/utils/library.ts
+++ b/server/utils/library.ts
@@ -1280,6 +1280,29 @@ export async function saveStill(params: {
return still
}
+export async function keepStillAsInput(owner: string, id: string) {
+ const still = getStill(owner, id)
+ if (stillRole(still) === 'input') return still
+ const path = stillPath(owner, still.id)
+ if (!existsSync(path)) throw createError({ statusCode: 404, statusMessage: 'Image file missing' })
+ const data = readFileSync(path)
+ const name = (still.name || still.filename.replace(/\.[a-z0-9]+$/i, '') || 'still').trim().slice(0, 80)
+ const base = lastFrameStillName(name) || clipIdFromStillFilename(`${name}.png`) ? 'still' : name
+ const saved = await saveStill({
+ ownerKey: owner,
+ folderId: still.folderId,
+ filename: `${base}.png`,
+ data,
+ width: still.width,
+ height: still.height,
+ hideInput: still.hideInput,
+ role: 'input',
+ name
+ })
+ if (!saved) throw createError({ statusCode: 400, statusMessage: 'Could not save that still as input' })
+ return saved
+}
+
export async function rememberInputStill(params: {
ownerKey: string
folderId: string