Add output navigation preserving viewer visibility
This commit is contained in:
+47
-3
@@ -1261,6 +1261,11 @@
|
|||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<h2 class="font-display text-2xl font-bold">Output</h2>
|
<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
|
<button
|
||||||
v-if="canRestoreOutput"
|
v-if="canRestoreOutput"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1593,9 +1598,11 @@
|
|||||||
Edited still appears here
|
Edited still appears here
|
||||||
</div>
|
</div>
|
||||||
<FamilyNav
|
<FamilyNav
|
||||||
:count="outputFamilyStills.length"
|
:count="outputNavigationIndex >= 0 ? outputNavigationItems.length : 0"
|
||||||
:index="outputFamilyIndex"
|
:index="outputNavigationIndex"
|
||||||
@step="stepOutputFamily"
|
prev-label="Previous output"
|
||||||
|
next-label="Next output"
|
||||||
|
@step="stepOutput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="editResultUrl && !editLockedSave && !editAwaitingReveal" class="mt-4 flex flex-wrap gap-3">
|
<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 ''
|
if (outputPreviewKind.value !== 'edit' || editLockedSave.value) return ''
|
||||||
return stillIdFromLibraryUrl(editResultUrl.value)
|
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 STUDIO_LIBRARY_CAP = 36
|
||||||
const visibleLibraryEntries = computed(() => libraryEntries.value.slice(0, STUDIO_LIBRARY_CAP))
|
const visibleLibraryEntries = computed(() => libraryEntries.value.slice(0, STUDIO_LIBRARY_CAP))
|
||||||
const libraryCapped = computed(() => libraryEntries.value.length > 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 (typingInField(event)) return
|
||||||
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
|
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
|
||||||
|
if (editLightboxUrl.value) {
|
||||||
if (outputFamilyStills.value.length < 2) return
|
if (outputFamilyStills.value.length < 2) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
stepOutputFamily(event.key === 'ArrowLeft' ? -1 : 1)
|
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) {
|
function showOutputStill(still: LibraryStill) {
|
||||||
|
|||||||
@@ -49,3 +49,28 @@ test('full input action restores the displayed image settings instead of the pre
|
|||||||
await action('useOutputWithSettings', f.scope)()
|
await action('useOutputWithSettings', f.scope)()
|
||||||
assert.deepEqual(f.calls, ['displayed'])
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user