65 lines
1.8 KiB
TypeScript
65 lines
1.8 KiB
TypeScript
import { useEffect, type RefObject } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import type { MediaParticleHandle } from "@/features/particles/media-particle-view";
|
|
import { isTypingTarget } from "@/lib/keyboard";
|
|
|
|
const SEEK_DELTA_SEC = 5;
|
|
|
|
interface UseStreamNavigationKeysOptions {
|
|
next: () => void;
|
|
prev: () => void;
|
|
currentIndex: number;
|
|
childrenLength: number;
|
|
mediaRef: RefObject<MediaParticleHandle | null>;
|
|
}
|
|
|
|
/**
|
|
* Arrow keys (with shift+arrow seek), Escape. Skipped while playback is
|
|
* paused for any reason (overlay, hold-space, compose).
|
|
*/
|
|
export function useStreamNavigationKeys({
|
|
next,
|
|
prev,
|
|
currentIndex,
|
|
childrenLength,
|
|
mediaRef,
|
|
}: UseStreamNavigationKeysOptions) {
|
|
const navigate = useNavigate();
|
|
|
|
useEffect(() => {
|
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
if (isTypingTarget(e)) return;
|
|
|
|
const hasNext = currentIndex >= 0 && currentIndex < childrenLength - 1;
|
|
|
|
switch (e.key) {
|
|
case "ArrowRight":
|
|
e.preventDefault();
|
|
if (!e.shiftKey || !mediaRef.current?.seek(SEEK_DELTA_SEC)) {
|
|
if (hasNext) next();
|
|
}
|
|
break;
|
|
case "ArrowDown":
|
|
e.preventDefault();
|
|
if (hasNext) next();
|
|
break;
|
|
case "ArrowLeft":
|
|
e.preventDefault();
|
|
if (!e.shiftKey || !mediaRef.current?.seek(-SEEK_DELTA_SEC)) prev();
|
|
break;
|
|
case "ArrowUp":
|
|
e.preventDefault();
|
|
prev();
|
|
break;
|
|
case "Escape":
|
|
e.preventDefault();
|
|
navigate(-1);
|
|
break;
|
|
}
|
|
};
|
|
|
|
window.addEventListener("keydown", onKeyDown);
|
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
}, [next, prev, currentIndex, childrenLength, mediaRef, navigate]);
|
|
}
|