Keep image jobs on the studio queue and confirm each job or library delete. Label stitch-frame slots when thumbs are hidden, and open output stills in a lightbox.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 09:04:08 -05:00
co-authored by Cursor
parent 8598dab516
commit fb748e9b6e
10 changed files with 996 additions and 369 deletions
+48 -39
View File
@@ -1,15 +1,17 @@
<template>
<div class="shrink-0">
<span v-if="hidden" class="sensitive-glow block h-12 w-12 overflow-hidden rounded-lg" />
<div v-else class="flex items-start gap-1.5">
<button
v-if="stillId && !failed.still"
type="button"
class="flex w-12 flex-col items-center gap-0.5"
aria-label="Start still"
@click.stop="openPreview(stillFull, 'Start')"
>
<span class="relative h-12 w-12 overflow-hidden rounded-lg bg-zinc-900">
<div class="flex items-start gap-1.5">
<div v-if="stillId && (hidden || !failed.still)" class="flex w-12 flex-col items-center gap-0.5">
<span v-if="hidden" class="relative h-12 w-12 overflow-hidden rounded-lg">
<span class="sensitive-glow block h-full w-full" />
</span>
<button
v-else
type="button"
class="relative h-12 w-12 overflow-hidden rounded-lg bg-zinc-900"
aria-label="Start still"
@click.stop="openPreview(stillFull, 'Start')"
>
<img
:src="stillThumb"
alt=""
@@ -18,17 +20,20 @@
class="h-full w-full object-cover"
@error="failed.still = true"
>
</span>
</button>
<span class="text-[10px] leading-none text-zinc-500">Start</span>
</button>
<button
v-if="fromClipId && !failed.from"
type="button"
class="flex w-12 flex-col items-center gap-0.5"
aria-label="Last frame of previous clip"
@click.stop="openPreview(fromFull, 'From prev')"
>
<span class="relative h-12 w-12 overflow-hidden rounded-lg bg-zinc-900">
</div>
<div v-if="fromClipId && (hidden || !failed.from)" class="flex w-12 flex-col items-center gap-0.5">
<span v-if="hidden" class="relative h-12 w-12 overflow-hidden rounded-lg">
<span class="sensitive-glow block h-full w-full" />
</span>
<button
v-else
type="button"
class="relative h-12 w-12 overflow-hidden rounded-lg bg-zinc-900"
aria-label="Last frame of previous clip"
@click.stop="openPreview(fromFull, 'From prev')"
>
<img
:src="fromThumb"
alt=""
@@ -37,17 +42,20 @@
class="h-full w-full object-cover"
@error="failed.from = true"
>
</span>
</button>
<span class="text-[10px] leading-none text-zinc-500">From prev</span>
</button>
<button
v-if="showFirstFrame && clipId && !failed.first"
type="button"
class="flex w-12 flex-col items-center gap-0.5"
aria-label="First frame of this clip"
@click.stop="openPreview(firstFull, 'This clip')"
>
<span class="relative h-12 w-12 overflow-hidden rounded-lg bg-zinc-900">
</div>
<div v-if="showFirstFrame && clipId && (hidden || !failed.first)" class="flex w-12 flex-col items-center gap-0.5">
<span v-if="hidden" class="relative h-12 w-12 overflow-hidden rounded-lg">
<span class="sensitive-glow block h-full w-full" />
</span>
<button
v-else
type="button"
class="relative h-12 w-12 overflow-hidden rounded-lg bg-zinc-900"
aria-label="First frame of this clip"
@click.stop="openPreview(firstFull, 'This clip')"
>
<img
:src="firstThumb"
alt=""
@@ -56,9 +64,9 @@
class="h-full w-full object-cover"
@error="failed.first = true"
>
</span>
</button>
<span class="text-[10px] leading-none text-zinc-500">This clip</span>
</button>
</div>
</div>
<Teleport to="body">
@@ -90,12 +98,12 @@ const props = defineProps<{
const failed = reactive({ still: false, from: false, first: false })
const preview = ref<{ src: string; label: string } | null>(null)
const stillThumb = computed(() => props.stillId ? `/api/library/stills/${props.stillId}?thumb=1` : '')
const stillFull = computed(() => props.stillId ? `/api/library/stills/${props.stillId}` : '')
const fromThumb = computed(() => props.fromClipId ? `/api/library/clips/${props.fromClipId}/last-frame?thumb=1` : '')
const fromFull = computed(() => props.fromClipId ? `/api/library/clips/${props.fromClipId}/last-frame` : '')
const firstThumb = computed(() => props.clipId ? `/api/library/clips/${props.clipId}/first-frame?thumb=1` : '')
const firstFull = computed(() => props.clipId ? `/api/library/clips/${props.clipId}/first-frame` : '')
const stillThumb = computed(() => !props.hidden && props.stillId ? `/api/library/stills/${props.stillId}?thumb=1` : '')
const stillFull = computed(() => !props.hidden && props.stillId ? `/api/library/stills/${props.stillId}` : '')
const fromThumb = computed(() => !props.hidden && props.fromClipId ? `/api/library/clips/${props.fromClipId}/last-frame?thumb=1` : '')
const fromFull = computed(() => !props.hidden && props.fromClipId ? `/api/library/clips/${props.fromClipId}/last-frame` : '')
const firstThumb = computed(() => !props.hidden && props.clipId ? `/api/library/clips/${props.clipId}/first-frame?thumb=1` : '')
const firstFull = computed(() => !props.hidden && props.clipId ? `/api/library/clips/${props.clipId}/first-frame` : '')
watch(
() => [props.hidden, props.stillId, props.fromClipId, props.clipId, props.showFirstFrame],
@@ -103,11 +111,12 @@ watch(
failed.still = false
failed.from = false
failed.first = false
if (props.hidden) closePreview()
}
)
function openPreview(src: string, label: string) {
if (!src) return
if (props.hidden || !src) return
preview.value = { src, label }
}