import { useEffect, useCallback } from 'react'; import type Player from 'video.js/dist/types/player'; interface KeyboardOptions { playerRef: React.RefObject; onStepForward: () => void; onStepBackward: () => void; onCaptureFrame: () => void; onAddMemo: () => void; onToggleHelp?: () => void; containerRef: React.RefObject; } export function useKeyboard({ playerRef, onStepForward, onStepBackward, onCaptureFrame, onAddMemo, onToggleHelp, containerRef, }: KeyboardOptions) { const handleKey = useCallback( (e: KeyboardEvent) => { const target = e.target as HTMLElement; // Don't fire when typing in an input if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return; const player = playerRef.current; if (!player) return; const ct = player.currentTime() ?? 0; const dur = player.duration() ?? 0; switch (e.code) { case 'Space': e.preventDefault(); player.paused() ? player.play() : player.pause(); break; case 'ArrowLeft': e.preventDefault(); player.currentTime(Math.max(0, ct - 5)); break; case 'ArrowRight': e.preventDefault(); player.currentTime(Math.min(dur, ct + 5)); break; case 'KeyJ': player.currentTime(Math.max(0, ct - 10)); break; case 'KeyL': player.currentTime(Math.min(dur, ct + 10)); break; case 'Comma': e.preventDefault(); onStepBackward(); break; case 'Period': e.preventDefault(); onStepForward(); break; case 'BracketLeft': player.currentTime(Math.max(0, ct - 30)); break; case 'BracketRight': player.currentTime(Math.min(dur, ct + 30)); break; case 'KeyF': if (containerRef.current) { if (!document.fullscreenElement) { containerRef.current.requestFullscreen(); } else { document.exitFullscreen(); } } break; case 'KeyM': if (e.shiftKey) { e.preventDefault(); onAddMemo(); } else { player.muted(!player.muted()); } break; case 'Equal': case 'NumpadAdd': player.playbackRate(Math.min(4, (player.playbackRate() ?? 1) + 0.25)); break; case 'Minus': case 'NumpadSubtract': player.playbackRate(Math.max(0.25, (player.playbackRate() ?? 1) - 0.25)); break; case 'KeyS': if (e.shiftKey) { e.preventDefault(); onCaptureFrame(); } break; case 'Slash': if (e.shiftKey) { e.preventDefault(); onToggleHelp?.(); } break; default: if (e.code >= 'Digit0' && e.code <= 'Digit9') { const pct = parseInt(e.code.replace('Digit', ''), 10) / 10; player.currentTime(dur * pct); } } }, [playerRef, onStepForward, onStepBackward, onCaptureFrame, onAddMemo, onToggleHelp, containerRef] ); useEffect(() => { window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, [handleKey]); }