From 7e4b69677f073f95b4920565a70c64e80f07a570 Mon Sep 17 00:00:00 2001 From: Towsty Date: Thu, 10 Sep 2026 07:09:52 -0500 Subject: [PATCH] Add output navigation preserving viewer visibility --- pages/index.vue | 54 ++++++++++++++++++++++++++--- tests/output-still-actions.test.mjs | 25 +++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/pages/index.vue b/pages/index.vue index 86abe06..1f2db9b 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -1261,6 +1261,11 @@

Output

+
+ + {{ outputNavigationIndex + 1 }} / {{ outputNavigationItems.length }} + +
@@ -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(() => 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 (outputFamilyStills.value.length < 2) 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() - stepOutputFamily(event.key === 'ArrowLeft' ? -1 : 1) + 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) { diff --git a/tests/output-still-actions.test.mjs b/tests/output-still-actions.test.mjs index 0727788..091dc80 100644 --- a/tests/output-still-actions.test.mjs +++ b/tests/output-still-actions.test.mjs @@ -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) + }) + } + } +}