Add output navigation preserving viewer visibility

This commit is contained in:
Towsty
2026-09-10 07:09:52 -05:00
parent 9b1d8ae798
commit 7e4b69677f
2 changed files with 74 additions and 5 deletions
+47 -3
View File
@@ -1261,6 +1261,11 @@
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
<h2 class="font-display text-2xl font-bold">Output</h2>
<div v-if="outputNavigationIndex >= 0 && outputNavigationItems.length > 1" class="flex items-center gap-2" aria-label="Browse outputs">
<button type="button" aria-label="Previous output" title="Previous output" class="h-9 w-9 rounded-full border border-white/20 hover:border-amber-300/60" @click="stepOutput(-1)">←</button>
<span class="text-xs tabular-nums text-zinc-400">{{ outputNavigationIndex + 1 }} / {{ outputNavigationItems.length }}</span>
<button type="button" aria-label="Next output" title="Next output" class="h-9 w-9 rounded-full border border-white/20 hover:border-amber-300/60" @click="stepOutput(1)">→</button>
</div>
<button
v-if="canRestoreOutput"
type="button"
@@ -1593,9 +1598,11 @@
Edited still appears here
</div>
<FamilyNav
:count="outputFamilyStills.length"
:index="outputFamilyIndex"
@step="stepOutputFamily"
:count="outputNavigationIndex >= 0 ? outputNavigationItems.length : 0"
:index="outputNavigationIndex"
prev-label="Previous output"
next-label="Next output"
@step="stepOutput"
/>
</div>
<div v-if="editResultUrl && !editLockedSave && !editAwaitingReveal" class="mt-4 flex flex-wrap gap-3">
@@ -3536,6 +3543,15 @@ const reviewingStillId = computed(() => {
if (outputPreviewKind.value !== 'edit' || editLockedSave.value) return ''
return stillIdFromLibraryUrl(editResultUrl.value)
})
type OutputNavigationItem = { kind: 'still'; item: LibraryStill } | { kind: 'clip'; item: LibraryClip }
const outputNavigationItems = computed<OutputNavigationItem[]>(() => libraryEntries.value.flatMap((entry): OutputNavigationItem[] =>
entry.kind === 'still'
? chronologicalStills(entry.stillGroup.parts).map(item => ({ kind: 'still' as const, item }))
: chronologicalParts(entry.group.parts).map(item => ({ kind: 'clip' as const, item }))
))
const outputNavigationIndex = computed(() => outputNavigationItems.value.findIndex(entry =>
entry.kind === 'still' ? entry.item.id === reviewingStillId.value : entry.item.id === reviewingClipId.value
))
const STUDIO_LIBRARY_CAP = 36
const visibleLibraryEntries = computed(() => libraryEntries.value.slice(0, STUDIO_LIBRARY_CAP))
const libraryCapped = computed(() => libraryEntries.value.length > STUDIO_LIBRARY_CAP)
@@ -5872,9 +5888,37 @@ function onStudioKey(event: KeyboardEvent) {
}
if (typingInField(event)) return
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
if (editLightboxUrl.value) {
if (outputFamilyStills.value.length < 2) return
event.preventDefault()
stepOutputFamily(event.key === 'ArrowLeft' ? -1 : 1)
return
}
if (outputNavigationIndex.value < 0 || outputNavigationItems.value.length < 2) return
event.preventDefault()
stepOutput(event.key === 'ArrowLeft' ? -1 : 1)
}
function stepOutput(delta: number) {
const items = outputNavigationItems.value
const index = outputNavigationIndex.value
if (index < 0 || items.length < 2) return
const next = items[(index + delta + items.length) % items.length]
if (!next) return
const covered = outputPreviewKind.value === 'edit' ? editAwaitingReveal.value : awaitingReveal.value
if (next.kind === 'still') {
openStill(next.item)
lastV2StillId.value = next.item.id
editAwaitingReveal.value = covered
concealEditOutput.value = covered || next.item.hideInput === true
if (!editBusy.value) editStatusMessage.value = covered ? 'Hidden still ready' : 'Loaded from library'
} else {
player.value?.pause()
openClip(next.item)
awaitingReveal.value = covered
concealOutput.value = covered || next.item.hideThumbnail === true
if (!videoBusy.value) statusMessage.value = covered ? 'Hidden clip ready' : 'Loaded from library'
}
}
function showOutputStill(still: LibraryStill) {
+25
View File
@@ -49,3 +49,28 @@ test('full input action restores the displayed image settings instead of the pre
await action('useOutputWithSettings', f.scope)()
assert.deepEqual(f.calls, ['displayed'])
})
for (const from of ['edit', 'video']) {
for (const kind of ['still', 'clip']) {
for (const covered of [false, true]) {
test(`output navigation carries ${covered ? 'covered' : 'revealed'} state from ${from} to ${kind}`, () => {
const target = { id: 'next', hideInput: true, hideThumbnail: true }
const ref = value => ({ value })
const scope = {
outputNavigationItems: ref([{ kind, item: { id: 'first' } }, { kind, item: target }]),
outputNavigationIndex: ref(0), outputPreviewKind: ref(from),
editAwaitingReveal: ref(covered), awaitingReveal: ref(covered),
concealEditOutput: ref(false), concealOutput: ref(false),
lastV2StillId: ref('first'), editBusy: ref(false), videoBusy: ref(false),
editStatusMessage: ref(''), statusMessage: ref(''), player: ref({ pause() {} }),
openStill(item) { assert.equal(item, target); scope.editAwaitingReveal.value = true },
openClip(item) { assert.equal(item, target); scope.awaitingReveal.value = true }
}
action('stepOutput', scope)(-1)
assert.equal(kind === 'still' ? scope.editAwaitingReveal.value : scope.awaitingReveal.value, covered)
assert.equal(target.hideInput, true)
assert.equal(target.hideThumbnail, true)
})
}
}
}