Files
llink/js/desktop/src/hooks/use-playback-keys.ts
T
Arjun PatelandGitHub 095b9876f9 feat: stream list view and tasks (#279)
* first attempt at stream sidebar, tasks, and events

* fix folder from root

* cleanup folders and events, and condense changes

* cleanup and add toggle for sidebar

* cleanup

* fix nits
2026-06-12 12:26:49 -07:00

112 lines
2.9 KiB
TypeScript

import {
useCallback,
useEffect,
useRef,
useState,
type RefObject,
} from 'react';
import type { MediaParticleHandle } from '@/features/particles/media-particle-view';
import { useSuspendPlayback } from '@/hooks/use-suspend-playback';
import {
selectIsPaused,
usePlaybackPauseStore,
} from '@/stores/playback-pause-store';
import { isTypingTarget } from '@/lib/keyboard';
const SPACE_TAP_THRESHOLD_MS = 250;
interface UsePlaybackKeysOptions {
mediaRef: RefObject<MediaParticleHandle | null>;
}
interface UsePlaybackKeysResult {
fastPlayback: boolean;
/** True while playback is paused by the user's space toggle/hold. */
spacePaused: boolean;
/** Clear the user's space pause (e.g. clicking the on-screen play button). */
resume: () => void;
}
/**
* Hold-space to pause, hold-shift for 1.5x. Space is always handled (it
* manages its own suspender via useSuspendPlayback); other keys bail when
* playback is already paused for an external reason.
*/
export function usePlaybackKeys({
mediaRef,
}: UsePlaybackKeysOptions): UsePlaybackKeysResult {
const [spaceHeld, setSpaceHeld] = useState(false);
const [fastPlayback, setFastPlayback] = useState(false);
const spaceStartRef = useRef(0);
useSuspendPlayback(spaceHeld, 'hold-space');
const resume = useCallback(() => {
setSpaceHeld(false);
spaceStartRef.current = 0;
}, []);
useEffect(() => {
const isExternallyPaused = () =>
selectIsPaused(usePlaybackPauseStore.getState()) && !spaceHeld;
const onKeyDown = (e: KeyboardEvent) => {
if (isTypingTarget(e)) return;
if (e.key === ' ') {
e.preventDefault();
if (!e.repeat) {
if (spaceHeld) {
setSpaceHeld(false);
spaceStartRef.current = 0;
} else {
setSpaceHeld(true);
spaceStartRef.current = Date.now();
}
}
return;
}
if (isExternallyPaused()) return;
if (e.key === 'Shift' && !e.repeat) {
mediaRef.current?.setPlaybackRate(1.5);
setFastPlayback(true);
}
};
const onKeyUp = (e: KeyboardEvent) => {
if (isTypingTarget(e)) return;
if (e.key === ' ') {
e.preventDefault();
// keep space-held if it was a quick tap, to allow for space-to-toggle behavior
if (Date.now() - spaceStartRef.current < SPACE_TAP_THRESHOLD_MS) {
return;
}
setSpaceHeld(false);
spaceStartRef.current = 0;
return;
}
if (isExternallyPaused()) return;
if (e.key === 'Shift') {
mediaRef.current?.setPlaybackRate(1);
setFastPlayback(false);
}
};
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
return () => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
};
}, [mediaRef, spaceHeld]);
return { fastPlayback, spacePaused: spaceHeld, resume };
}