import { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useShortcutContext } from "../../contexts/ShortcutContext"; import { useEditor } from "./EditorContext"; export const useEditorShortcuts = (videoRef: React.RefObject) => { const navigate = useNavigate(); const { registerCategory, unregisterCategory, setShowHelp } = useShortcutContext(); const { activeTab, togglePlay, undo, redo, canUndo, canRedo, stepFrames, setWalkthroughMode, setEditingHotspots, setPlacingHotspot, startWalkthrough, handleExportMarkdown, handleMarkStep, selectedStep, duplicateStep, } = useEditor(); const [copiedStepId, setCopiedStepId] = useState(null); const isVideoTab = activeTab === "video" || activeTab === "both"; const isTypingInEditableElement = useCallback(() => { const active = document.activeElement as HTMLElement | null; if (!active) return false; return ( active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.tagName === "SELECT" || active.isContentEditable ); }, []); useEffect(() => { registerCategory({ name: "Playback", shortcuts: { "play-pause": { key: " ", handler: togglePlay, description: "Play/pause video", }, "frame-backward": { key: "ArrowLeft", handler: () => { if (!videoRef.current) return; videoRef.current.pause(); stepFrames(-1); }, description: "Step backward one frame", enabled: isVideoTab, }, "frame-forward": { key: "ArrowRight", handler: () => { if (!videoRef.current) return; videoRef.current.pause(); stepFrames(1); }, description: "Step forward one frame", enabled: isVideoTab, }, "slow-playback": { key: "j", handler: () => { if (!videoRef.current) return; videoRef.current.playbackRate = 0.5; void videoRef.current.play(); }, description: "Slow playback (0.5x)", enabled: isVideoTab, }, pause: { key: "k", handler: () => videoRef.current?.pause(), description: "Pause playback", enabled: isVideoTab, }, "fast-playback": { key: "l", handler: () => { if (!videoRef.current) return; videoRef.current.playbackRate = 1.5; void videoRef.current.play(); }, description: "Fast playback (1.5x)", enabled: isVideoTab, }, }, }); registerCategory({ name: "Editor", shortcuts: { "toggle-hotspots": { key: "h", meta: true, handler: () => { setWalkthroughMode(false); setEditingHotspots((current) => !current); setPlacingHotspot(false); }, description: "Toggle hotspot editing", enabled: isVideoTab, }, walkthrough: { key: "w", meta: true, handler: startWalkthrough, description: "Play as walkthrough", enabled: isVideoTab, }, "export-markdown": { key: "e", meta: true, handler: handleExportMarkdown, description: "Export as Markdown", }, "mark-step": { key: "m", meta: true, handler: () => { void handleMarkStep(); }, description: "Insert manual step marker", }, "copy-step-meta": { key: "c", meta: true, handler: () => { if (isTypingInEditableElement()) return; if (!selectedStep) return; setCopiedStepId(selectedStep.id); }, description: "Copy selected step", enabled: Boolean(selectedStep), }, "copy-step-ctrl": { key: "c", ctrl: true, handler: () => { if (isTypingInEditableElement()) return; if (!selectedStep) return; setCopiedStepId(selectedStep.id); }, description: "Copy selected step", enabled: Boolean(selectedStep), }, "paste-step-meta": { key: "v", meta: true, handler: () => { if (isTypingInEditableElement()) return; const sourceStepId = copiedStepId ?? selectedStep?.id ?? null; if (!sourceStepId) return; duplicateStep(sourceStepId); }, description: "Paste copied step", enabled: Boolean(copiedStepId ?? selectedStep), }, "paste-step-ctrl": { key: "v", ctrl: true, handler: () => { if (isTypingInEditableElement()) return; const sourceStepId = copiedStepId ?? selectedStep?.id ?? null; if (!sourceStepId) return; duplicateStep(sourceStepId); }, description: "Paste copied step", enabled: Boolean(copiedStepId ?? selectedStep), }, "undo-meta": { key: "z", meta: true, handler: () => { if (isTypingInEditableElement()) return; undo(); }, description: "Undo", enabled: canUndo, }, "undo-ctrl": { key: "z", ctrl: true, handler: () => { if (isTypingInEditableElement()) return; undo(); }, description: "Undo", enabled: canUndo, }, "redo-meta-shift": { key: "z", meta: true, shift: true, handler: () => { if (isTypingInEditableElement()) return; redo(); }, description: "Redo", enabled: canRedo, }, "redo-ctrl-shift": { key: "z", ctrl: true, shift: true, handler: () => { if (isTypingInEditableElement()) return; redo(); }, description: "Redo", enabled: canRedo, }, "redo-ctrl-y": { key: "y", ctrl: true, handler: () => { if (isTypingInEditableElement()) return; redo(); }, description: "Redo", enabled: canRedo, }, }, }); registerCategory({ name: "Recording", shortcuts: { "new-recording": { key: "n", meta: true, handler: () => navigate("/record"), description: "New recording", }, }, }); registerCategory({ name: "Navigation", shortcuts: { help: { key: "?", meta: true, shift: true, handler: () => setShowHelp(true), description: "Show keyboard shortcuts", }, library: { key: "L", meta: true, handler: () => navigate("/library"), description: "Back to library", }, }, }); return () => { unregisterCategory("Playback"); unregisterCategory("Editor"); unregisterCategory("Recording"); unregisterCategory("Navigation"); }; }, [ registerCategory, unregisterCategory, setShowHelp, isVideoTab, navigate, stepFrames, togglePlay, undo, redo, canUndo, canRedo, setWalkthroughMode, setEditingHotspots, setPlacingHotspot, startWalkthrough, handleExportMarkdown, handleMarkStep, isTypingInEditableElement, selectedStep, duplicateStep, copiedStepId, videoRef, ]); };