Keep uploaded stills in the input picker and generated work in the library, and run image-edit passes as a chain like video extensions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-27 09:58:08 -05:00
co-authored by Cursor
parent f88dfa1146
commit 860aeafb1e
9 changed files with 1001 additions and 177 deletions
+34 -4
View File
@@ -5,7 +5,7 @@
<div class="flex items-start justify-between gap-3">
<div>
<h2 class="font-display text-2xl font-bold">{{ heading || 'Choose an image' }}</h2>
<p class="mt-1 text-sm text-zinc-400">Pick a still from this folder or upload a new one.</p>
<p class="mt-1 text-sm text-zinc-400">{{ tab === 'input' ? 'Uploaded stills only. Generated frames and edits live under Output.' : 'Generated stills from image edits. Video frames stay with their clips.' }}</p>
</div>
<div class="flex items-center gap-2">
<button
@@ -32,6 +32,25 @@
</div>
<div class="mt-5 flex flex-wrap gap-2">
<button
type="button"
class="rounded-full border px-3 py-1 text-xs font-medium"
:class="tab === 'input' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400'"
@click="tab = 'input'"
>
Input
</button>
<button
type="button"
class="rounded-full border px-3 py-1 text-xs font-medium"
:class="tab === 'output' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400'"
@click="tab = 'output'"
>
Output
</button>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<button
v-for="folder in folders"
:key="folder.id"
@@ -52,6 +71,7 @@
</form>
<div
v-if="tab === 'input'"
class="mt-5 cursor-pointer rounded-2xl border border-dashed border-white/15 bg-zinc-950/40 p-6 text-center transition hover:border-amber-300/50"
:class="{ 'border-amber-300/70 bg-amber-400/5': dragging }"
@dragover.prevent="dragging = true"
@@ -65,7 +85,7 @@
</div>
<template v-if="current && (!current.protected || current.unlocked)">
<p v-if="!visibleStills.length" class="mt-6 text-sm text-zinc-500">No uploaded stills in this folder yet.</p>
<p v-if="!visibleStills.length" class="mt-6 text-sm text-zinc-500">{{ emptyLabel }}</p>
<div v-if="visibleStills.length" class="mt-5 grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4">
<article
v-for="still in visibleStills"
@@ -112,7 +132,7 @@
</div>
</div>
<div class="flex items-center justify-between gap-2 px-3 py-2">
<span class="truncate text-xs text-zinc-400">{{ still.filename }}</span>
<span class="truncate text-xs text-zinc-400">{{ still.name || still.filename }}</span>
<button type="button" class="text-[10px] text-red-300 hover:text-red-200" @click="removeStill(still)">Delete</button>
</div>
</article>
@@ -140,6 +160,8 @@ export interface PickerStill {
height: number
createdAt: number
hideInput?: boolean
role?: 'input' | 'output'
name?: string
}
const props = defineProps<{
@@ -161,6 +183,7 @@ const fileInput = ref<HTMLInputElement | null>(null)
const dragging = ref(false)
const password = ref('')
const activeFolderId = ref(props.folderId)
const tab = ref<'input' | 'output'>('input')
const revealAll = ref(false)
const revealedIds = ref<string[]>([])
@@ -178,7 +201,14 @@ watch(() => props.folderId, (id) => {
})
const current = computed(() => props.folders.find(folder => folder.id === activeFolderId.value) || props.folders[0])
const visibleStills = computed(() => props.stills.filter(still => still.folderId === (current.value?.id || activeFolderId.value)))
const visibleStills = computed(() => props.stills.filter((still) => {
if (still.folderId !== (current.value?.id || activeFolderId.value)) return false
const role = still.role === 'output' ? 'output' : 'input'
return role === tab.value
}))
const emptyLabel = computed(() => tab.value === 'input'
? 'No uploaded stills in this folder yet.'
: 'No generated stills in this folder yet.')
const hasHiddenItems = computed(() => visibleStills.value.some(still => stillHidden(still)))
function stillHidden(still: PickerStill) {