t2-mapper/src/components/DemoPlaybackControls.tsx

110 lines
2.8 KiB
TypeScript
Raw Normal View History

2026-03-04 12:15:24 -08:00
import { useCallback, useEffect, type ChangeEvent } from "react";
2026-02-28 17:58:09 -08:00
import {
2026-03-09 12:38:40 -07:00
usePlaybackActions,
useCurrentTime,
useDuration,
useIsPlaying,
useRecording,
useSpeed,
} from "./RecordingProvider";
import styles from "./DemoPlaybackControls.module.css";
2026-02-20 15:48:15 -08:00
const SPEED_OPTIONS = [0.25, 0.5, 1, 2, 4];
function formatTime(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, "0")}`;
}
2026-03-09 12:38:40 -07:00
export function DemoPlaybackControls() {
const recording = useRecording();
const isPlaying = useIsPlaying();
const currentTime = useCurrentTime();
const duration = useDuration();
const speed = useSpeed();
const { play, pause, seek, setSpeed } = usePlaybackActions();
2026-03-04 12:15:24 -08:00
// Spacebar toggles play/pause during demo playback.
useEffect(() => {
if (!recording) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.code !== "Space") return;
const target = e.target as HTMLElement;
if (
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.tagName === "SELECT" ||
target.tagName === "BUTTON" ||
target.isContentEditable
) {
return;
}
e.preventDefault();
if (isPlaying) {
pause();
} else {
play();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [recording, isPlaying, play, pause]);
2026-02-20 15:48:15 -08:00
const handleSeek = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
seek(parseFloat(e.target.value));
},
[seek],
);
const handleSpeedChange = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
setSpeed(parseFloat(e.target.value));
},
[setSpeed],
);
2026-03-09 12:38:40 -07:00
if (!recording || !Number.isFinite(recording.duration)) return null;
2026-02-20 15:48:15 -08:00
return (
<div
2026-03-01 09:40:17 -08:00
className={styles.Root}
2026-02-20 15:48:15 -08:00
onKeyDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<button
2026-03-01 09:40:17 -08:00
className={styles.PlayPause}
2026-02-20 15:48:15 -08:00
onClick={isPlaying ? pause : play}
aria-label={isPlaying ? "Pause" : "Play"}
>
{isPlaying ? "\u275A\u275A" : "\u25B6"}
</button>
2026-03-01 09:40:17 -08:00
<span className={styles.Time}>
2026-02-28 17:58:09 -08:00
{`${formatTime(currentTime)} / ${formatTime(duration)}`}
2026-02-20 15:48:15 -08:00
</span>
<input
2026-03-01 09:40:17 -08:00
className={styles.Seek}
2026-02-20 15:48:15 -08:00
type="range"
min={0}
max={duration}
step={0.01}
value={currentTime}
onChange={handleSeek}
/>
<select
2026-03-01 09:40:17 -08:00
className={styles.Speed}
2026-02-20 15:48:15 -08:00
value={speed}
onChange={handleSpeedChange}
>
{SPEED_OPTIONS.map((s) => (
<option key={s} value={s}>
{s}x
</option>
))}
</select>
</div>
);
}