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 +
@@ -1532,6 +1541,14 @@ > Use as input still + + @@ -1972,6 +1996,13 @@
+ @@ -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 @@
+ (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