Skip to content

Commit f8f8574

Browse files
committed
Change prev/next second controls with prev/next segment
This way it matches the keyboard shortcuts. If someone wants to jump back and forth a bit just to go through the source a bit, they can use the prev/next keyframe controls.
1 parent 41b1f04 commit f8f8574

4 files changed

Lines changed: 49 additions & 36 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ This allows you to preview your Sources in various comparison modes. This should
146146
try and get the temporal sync aligned with all sources. It allows you to seek through your
147147
video and make sure the frames line up.
148148

149-
You can zoom, pan, scrub, seek by frame, second, or keyframe. You can copy the shown frame,
149+
You can zoom, pan, scrub, seek by frame, segment, or keyframe. You can copy the shown frame,
150150
go fullscreen (in different ways).
151151

152152
The zoom % amount listed on the top right shows you the relative scale of the shown image.
@@ -189,7 +189,7 @@ It contains the following components:
189189
- Play/Pause: To enable playback of the project preview (note: its slow, film-strip will lag)
190190
- Playhead: Lists the current project playback timestamp and frame number.
191191
- Seekbar: Allows you to drag or click to seek across the project.
192-
- Buttons: These let you seek back or forward by 1 frame, 1 second, or 1 keyframe.
192+
- Buttons: These let you seek back or forward by 1 frame, 1 segment, or 1 keyframe.
193193

194194
### Export
195195

ui/components/tabs/preview/Controls.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ interface Props {
8181
tracks: SeekTrack[];
8282
onJumpKeyframe?: (dir: 1 | -1) => void;
8383
canKeyframe?: boolean;
84+
onJumpSegment?: (dir: 1 | -1) => void;
85+
canSegment?: boolean;
8486
playing?: boolean;
8587
onTogglePlay?: () => void;
8688
}
@@ -92,6 +94,8 @@ export function Controls({
9294
tracks,
9395
onJumpKeyframe,
9496
canKeyframe,
97+
onJumpSegment,
98+
canSegment,
9599
playing = false,
96100
onTogglePlay,
97101
}: Props) {
@@ -100,7 +104,6 @@ export function Controls({
100104
const seeking = useRef(false);
101105
const enabled = maxBase > 0;
102106

103-
const secondFrames = Math.max(1, Math.round(clockFps));
104107
const step = (delta: number) => setBase(clampNum(base + delta, 0, maxBase));
105108

106109
const nFromX = (clientX: number, rect: DOMRect) =>
@@ -196,17 +199,17 @@ export function Controls({
196199
)}
197200
{navBtn(
198201
<ChevronsLeft className="size-3.5" />,
199-
"Back 1 second",
200-
() => step(-secondFrames),
201-
!enabled,
202+
"Previous segment",
203+
() => onJumpSegment?.(-1),
204+
!canSegment,
202205
)}
203206
{navBtn(<ChevronLeft className="size-3.5" />, "Back 1 frame", () => step(-1), !enabled)}
204207
{navBtn(<ChevronRight className="size-3.5" />, "Forward 1 frame", () => step(1), !enabled)}
205208
{navBtn(
206209
<ChevronsRight className="size-3.5" />,
207-
"Forward 1 second",
208-
() => step(secondFrames),
209-
!enabled,
210+
"Next segment",
211+
() => onJumpSegment?.(1),
212+
!canSegment,
210213
)}
211214
{navBtn(
212215
<SkipForward className="size-3.5" />,

ui/components/tabs/preview/PreviewTab.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const PreviewTab = forwardRef<PreviewTabHandle, Props>(function PreviewTa
5959
bands,
6060
);
6161

62-
const { jumpKeyframe } = usePreviewKeyboard({
62+
const { jumpKeyframe, jumpSegment } = usePreviewKeyboard({
6363
keyframes,
6464
shownSource,
6565
base,
@@ -124,6 +124,8 @@ export const PreviewTab = forwardRef<PreviewTabHandle, Props>(function PreviewTa
124124
maxBase={maxBase}
125125
onJumpKeyframe={jumpKeyframe}
126126
canKeyframe={keyframes.length > 0}
127+
onJumpSegment={jumpSegment}
128+
canSegment={(shownSource?.segments?.length ?? 0) > 0}
127129
playing={playing}
128130
onTogglePlay={togglePlay}
129131
tracks={sources.map((s, i) => ({

ui/hooks/usePreviewKeyboard.ts

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useEffect, type Dispatch, type SetStateAction } from "react";
2-
import { frameAt, projectFrameOf } from "@/lib/frames";
2+
import { projectFrameOf } from "@/lib/frames";
33
import type { UiSource } from "@/state/AppState";
44

55
const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));
@@ -23,45 +23,53 @@ export function usePreviewKeyboard({
2323
active,
2424
togglePlay,
2525
}: KeyboardArgs) {
26+
const seekTo = useCallback(
27+
(targets: Iterable<number>, dir: 1 | -1) => {
28+
const sorted = [...new Set(targets)].sort((a, b) => a - b);
29+
const target =
30+
dir > 0
31+
? sorted.find((b) => b > base)
32+
: [...sorted].reverse().find((b) => b < base);
33+
if (target != null) setBase(target);
34+
},
35+
[base, setBase],
36+
);
37+
2638
const jumpKeyframe = useCallback(
2739
(dir: 1 | -1) => {
2840
if (!keyframes.length) return;
2941
const segs = shownSource?.segments ?? [];
30-
const cur = frameAt(segs, base);
31-
if (cur == null) return;
32-
let target: number | undefined;
33-
if (dir > 0) target = keyframes.find((k) => k > cur);
34-
else {
35-
const prev = keyframes.filter((k) => k < cur);
36-
target = prev.length ? prev[prev.length - 1] : undefined;
42+
if (!segs.length) return;
43+
let first = maxBase;
44+
let last = 0;
45+
for (const s of segs) {
46+
first = Math.min(first, clamp(s.pos, 0, maxBase));
47+
last = Math.max(last, clamp(s.pos + s.len - 1, 0, maxBase));
3748
}
38-
if (target == null) return;
39-
const t = projectFrameOf(segs, target);
40-
if (t == null) return;
41-
setBase(clamp(t, 0, maxBase));
49+
const targets = [first, last];
50+
for (const k of keyframes) {
51+
const t = projectFrameOf(segs, k);
52+
if (t != null) targets.push(clamp(t, 0, maxBase));
53+
}
54+
seekTo(targets, dir);
4255
},
43-
[keyframes, base, shownSource, maxBase, setBase],
56+
[keyframes, shownSource, maxBase, seekTo],
4457
);
4558

4659
const jumpSegment = useCallback(
4760
(dir: 1 | -1) => {
4861
const segs = shownSource?.segments ?? [];
4962
if (!segs.length) return;
50-
const bounds = new Set<number>();
51-
let lastFrame = 0;
63+
const targets: number[] = [];
64+
let last = 0;
5265
for (const s of segs) {
53-
bounds.add(clamp(s.pos, 0, maxBase));
54-
lastFrame = Math.max(lastFrame, s.pos + s.len - 1);
66+
targets.push(clamp(s.pos, 0, maxBase));
67+
last = Math.max(last, s.pos + s.len - 1);
5568
}
56-
bounds.add(clamp(lastFrame, 0, maxBase));
57-
const sorted = [...bounds].sort((a, b) => a - b);
58-
const target =
59-
dir > 0
60-
? sorted.find((b) => b > base)
61-
: [...sorted].reverse().find((b) => b < base);
62-
if (target != null) setBase(target);
69+
targets.push(clamp(last, 0, maxBase));
70+
seekTo(targets, dir);
6371
},
64-
[shownSource, base, maxBase, setBase],
72+
[shownSource, maxBase, seekTo],
6573
);
6674

6775
useEffect(() => {
@@ -99,5 +107,5 @@ export function usePreviewKeyboard({
99107
return () => window.removeEventListener("keydown", onKey);
100108
}, [active, maxBase, jumpKeyframe, jumpSegment, togglePlay, setBase]);
101109

102-
return { jumpKeyframe };
110+
return { jumpKeyframe, jumpSegment };
103111
}

0 commit comments

Comments
 (0)