Skip to content

Commit 07cb6bc

Browse files
committed
Enable timeline seeking from bottom bar
1 parent e32c685 commit 07cb6bc

1 file changed

Lines changed: 109 additions & 11 deletions

File tree

src/components/bottom-bar.tsx

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@ import { useProjectId, useVideoProjectStore } from "@/data/store";
1111
import { cn, resolveDuration } from "@/lib/utils";
1212
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
1313
import { useTranslations } from "next-intl";
14-
import { type DragEventHandler, useEffect, useMemo, useState } from "react";
14+
import {
15+
type DragEventHandler,
16+
type KeyboardEventHandler,
17+
type MouseEventHandler,
18+
useEffect,
19+
useMemo,
20+
useRef,
21+
useState,
22+
} from "react";
1523
import { VideoControls } from "./video-controls";
1624
import { TimelineRuler } from "./video/timeline";
1725
import { VideoTrackRow } from "./video/track";
@@ -20,13 +28,26 @@ export default function BottomBar() {
2028
const t = useTranslations("app.bottomBar");
2129
const queryClient = useQueryClient();
2230
const projectId = useProjectId();
31+
const player = useVideoProjectStore((s) => s.player);
2332
const playerCurrentTimestamp = useVideoProjectStore(
2433
(s) => s.playerCurrentTimestamp,
2534
);
26-
const formattedTimestamp =
27-
(playerCurrentTimestamp < 10 ? "0" : "") +
28-
playerCurrentTimestamp.toFixed(2);
29-
const minTrackWidth = `${((2 / 30) * 100).toFixed(2)}%`;
35+
const setPlayerCurrentTimestamp = useVideoProjectStore(
36+
(s) => s.setPlayerCurrentTimestamp,
37+
);
38+
const timelineWrapperRef = useRef<HTMLDivElement>(null);
39+
const timelineDurationSeconds = 30;
40+
const FPS = 30;
41+
const minTrackWidth = `${((2 / timelineDurationSeconds) * 100).toFixed(2)}%`;
42+
const currentMinutes = Math.floor(playerCurrentTimestamp / 60);
43+
const formattedCurrentMinutes = currentMinutes.toString().padStart(2, "0");
44+
const currentSeconds = playerCurrentTimestamp % 60;
45+
const formattedCurrentSeconds = currentSeconds.toFixed(2).padStart(5, "0");
46+
const totalMinutes = Math.floor(timelineDurationSeconds / 60);
47+
const formattedTotalMinutes = totalMinutes.toString().padStart(2, "0");
48+
const formattedTotalSeconds = (timelineDurationSeconds % 60)
49+
.toFixed(2)
50+
.padStart(5, "0");
3051
const [dragOverTracks, setDragOverTracks] = useState(false);
3152

3253
const limitAllKeyframesToThirtySeconds = useMutation({
@@ -181,16 +202,81 @@ export default function BottomBar() {
181202
return true;
182203
};
183204

205+
const seekToTimestamp = (nextTimestamp: number) => {
206+
const clampedTimestamp = Math.max(
207+
0,
208+
Math.min(nextTimestamp, timelineDurationSeconds),
209+
);
210+
211+
if (player) {
212+
const frame = Math.round(clampedTimestamp * FPS);
213+
player.seekTo(frame);
214+
}
215+
216+
setPlayerCurrentTimestamp(clampedTimestamp);
217+
};
218+
219+
const handleTimelineClick: MouseEventHandler<HTMLDivElement> = (event) => {
220+
const timelineElement = timelineWrapperRef.current;
221+
if (!timelineElement) return;
222+
223+
const rect = timelineElement.getBoundingClientRect();
224+
if (rect.width === 0) return;
225+
226+
const relativeX = event.clientX - rect.left;
227+
const clampedX = Math.min(Math.max(relativeX, 0), rect.width);
228+
const ratio = clampedX / rect.width;
229+
const nextTimestamp = timelineDurationSeconds * ratio;
230+
231+
seekToTimestamp(nextTimestamp);
232+
};
233+
234+
const handleTimelineKeyDown: KeyboardEventHandler<HTMLDivElement> = (
235+
event,
236+
) => {
237+
switch (event.key) {
238+
case "ArrowLeft":
239+
event.preventDefault();
240+
seekToTimestamp(playerCurrentTimestamp - 1);
241+
break;
242+
case "ArrowRight":
243+
event.preventDefault();
244+
seekToTimestamp(playerCurrentTimestamp + 1);
245+
break;
246+
case "Home":
247+
event.preventDefault();
248+
seekToTimestamp(0);
249+
break;
250+
case "End":
251+
event.preventDefault();
252+
seekToTimestamp(timelineDurationSeconds);
253+
break;
254+
default:
255+
break;
256+
}
257+
};
258+
259+
const timelineProgressPercent = (
260+
(Math.max(0, Math.min(playerCurrentTimestamp, timelineDurationSeconds)) /
261+
timelineDurationSeconds) *
262+
100
263+
).toFixed(2);
264+
184265
return (
185266
<div className="border-t pb-2 border-border flex flex-col bg-background-light ">
186267
<div className="border-b border-border bg-background-dark px-2 flex flex-row gap-8 py-2 justify-between items-center flex-1">
187268
<div className="h-full flex flex-col justify-center px-4 bg-muted/50 rounded-md font-mono cursor-default select-none shadow-inner">
188269
<div className="flex flex-row items-baseline font-thin tabular-nums">
189-
<span className="text-muted-foreground">00:</span>
190-
<span>{formattedTimestamp}</span>
270+
<span className="text-muted-foreground">
271+
{formattedCurrentMinutes}:
272+
</span>
273+
<span>{formattedCurrentSeconds}</span>
191274
<span className="text-muted-foreground/50 mx-2">/</span>
192275
<span className="text-sm opacity-50">
193-
<span className="text-muted-foreground">00:</span>30.00
276+
<span className="text-muted-foreground">
277+
{formattedTotalMinutes}:
278+
</span>
279+
{formattedTotalSeconds}
194280
</span>
195281
</div>
196282
</div>
@@ -207,14 +293,26 @@ export default function BottomBar() {
207293
onDragLeave={() => setDragOverTracks(false)}
208294
onDrop={handleOnDrop}
209295
>
210-
<div className="flex flex-col justify-start w-full h-full relative">
296+
<div
297+
ref={timelineWrapperRef}
298+
className="flex flex-col justify-start w-full h-full relative"
299+
role="slider"
300+
aria-label="Timeline"
301+
aria-valuemin={0}
302+
aria-valuemax={timelineDurationSeconds}
303+
aria-valuenow={playerCurrentTimestamp}
304+
aria-valuetext={`${formattedCurrentMinutes}:${formattedCurrentSeconds}`}
305+
tabIndex={0}
306+
onClick={handleTimelineClick}
307+
onKeyDown={handleTimelineKeyDown}
308+
>
211309
<div
212310
className="absolute z-[32] top-6 bottom-0 w-[2px] bg-white/30 ms-4"
213311
style={{
214-
left: `${((playerCurrentTimestamp / 30) * 100).toFixed(2)}%`,
312+
left: `${timelineProgressPercent}%`,
215313
}}
216314
/>
217-
<TimelineRuler className="z-30 pointer-events-none" />
315+
<TimelineRuler className="z-30" />
218316
<div className="flex timeline-container flex-col h-full mx-4 mt-10 gap-2 z-[31] pb-2">
219317
{Object.entries(trackObj).map(([trackType, track]) =>
220318
track ? (

0 commit comments

Comments
 (0)