From 8bc26539ebe44d6ff0d74d86ef316934c7f18faf Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 14:37:08 +0200 Subject: cleanup (this doesn't work) --- apps/web/src/hooks/use-keyboard-shortcuts.ts | 349 +++++++++++++++++++++++++++ apps/web/src/hooks/use-playback-controls.ts | 64 ----- 2 files changed, 349 insertions(+), 64 deletions(-) create mode 100644 apps/web/src/hooks/use-keyboard-shortcuts.ts (limited to 'apps/web/src/hooks') diff --git a/apps/web/src/hooks/use-keyboard-shortcuts.ts b/apps/web/src/hooks/use-keyboard-shortcuts.ts new file mode 100644 index 00000000..86190c94 --- /dev/null +++ b/apps/web/src/hooks/use-keyboard-shortcuts.ts @@ -0,0 +1,349 @@ +"use client"; + +import { useEffect, useCallback } from "react"; +import { useTimelineStore } from "@/stores/timeline-store"; +import { usePlaybackStore } from "@/stores/playback-store"; +import { useProjectStore } from "@/stores/project-store"; +import { toast } from "sonner"; + +export interface KeyboardShortcut { + id: string; + keys: string[]; + description: string; + category: string; + action: () => void; + enabled?: boolean; + requiresSelection?: boolean; + icon?: React.ReactNode; +} + +interface UseKeyboardShortcutsOptions { + enabled?: boolean; + context?: "global" | "timeline" | "editor"; +} + +export const useKeyboardShortcuts = ( + options: UseKeyboardShortcutsOptions = {} +) => { + const { enabled = true, context = "editor" } = options; + + const { + tracks, + selectedElements, + clearSelectedElements, + setSelectedElements, + removeElementFromTrack, + splitElement, + addElementToTrack, + snappingEnabled, + toggleSnapping, + undo, + redo, + } = useTimelineStore(); + + const { currentTime, duration, isPlaying, toggle, seek } = usePlaybackStore(); + + const { activeProject } = useProjectStore(); + + // Check if user is typing in an input field + const isInputFocused = useCallback(() => { + const activeElement = document.activeElement as HTMLElement; + return ( + activeElement && + (activeElement.tagName === "INPUT" || + activeElement.tagName === "TEXTAREA" || + activeElement.contentEditable === "true") + ); + }, []); + + // Define all shortcuts in one place + const shortcuts: KeyboardShortcut[] = [ + // Playback Controls + { + id: "play-pause", + keys: ["Space"], + description: "Play/Pause", + category: "Playback", + action: () => { + toggle(); + toast.info(isPlaying ? "Paused" : "Playing", { duration: 1000 }); + }, + }, + { + id: "rewind", + keys: ["j", "J"], + description: "Rewind 1 second", + category: "Playback", + action: () => { + seek(Math.max(0, currentTime - 1)); + toast.info("Rewind 1s", { duration: 1000 }); + }, + }, + { + id: "play-pause-alt", + keys: ["k", "K"], + description: "Play/Pause (alternative)", + category: "Playback", + action: () => { + toggle(); + toast.info(isPlaying ? "Paused" : "Playing", { duration: 1000 }); + }, + }, + { + id: "fast-forward", + keys: ["l", "L"], + description: "Fast forward 1 second", + category: "Playback", + action: () => { + seek(Math.min(duration, currentTime + 1)); + toast.info("Forward 1s", { duration: 1000 }); + }, + }, + + // Navigation + { + id: "frame-backward", + keys: ["ArrowLeft"], + description: "Frame step backward", + category: "Navigation", + action: () => { + const projectFps = activeProject?.fps || 30; + seek(Math.max(0, currentTime - 1 / projectFps)); + }, + }, + { + id: "frame-forward", + keys: ["ArrowRight"], + description: "Frame step forward", + category: "Navigation", + action: () => { + const projectFps = activeProject?.fps || 30; + seek(Math.min(duration, currentTime + 1 / projectFps)); + }, + }, + { + id: "jump-backward", + keys: ["Shift+ArrowLeft"], + description: "Jump back 5 seconds", + category: "Navigation", + action: () => { + seek(Math.max(0, currentTime - 5)); + }, + }, + { + id: "jump-forward", + keys: ["Shift+ArrowRight"], + description: "Jump forward 5 seconds", + category: "Navigation", + action: () => { + seek(Math.min(duration, currentTime + 5)); + }, + }, + { + id: "goto-start", + keys: ["Home"], + description: "Go to timeline start", + category: "Navigation", + action: () => { + seek(0); + toast.info("Start of timeline", { duration: 1000 }); + }, + }, + { + id: "goto-end", + keys: ["End"], + description: "Go to timeline end", + category: "Navigation", + action: () => { + seek(duration); + toast.info("End of timeline", { duration: 1000 }); + }, + }, + + // Editing + { + id: "split-element", + keys: ["s", "S"], + description: "Split element at playhead", + category: "Editing", + requiresSelection: true, + action: () => { + if (selectedElements.length !== 1) { + toast.error("Select exactly one element to split"); + return; + } + + const { trackId, elementId } = selectedElements[0]; + const track = tracks.find((t: any) => t.id === trackId); + const element = track?.elements.find((el: any) => el.id === elementId); + + if (element) { + const effectiveStart = element.startTime; + const effectiveEnd = + element.startTime + + (element.duration - element.trimStart - element.trimEnd); + + if (currentTime > effectiveStart && currentTime < effectiveEnd) { + splitElement(trackId, elementId, currentTime); + toast.success("Element split at playhead"); + } else { + toast.error("Playhead must be within selected element"); + } + } + }, + }, + { + id: "delete-elements", + keys: ["Delete", "Backspace"], + description: "Delete selected elements", + category: "Editing", + requiresSelection: true, + action: () => { + if (selectedElements.length === 0) { + toast.error("No elements selected"); + return; + } + selectedElements.forEach( + ({ trackId, elementId }: { trackId: string; elementId: string }) => { + removeElementFromTrack(trackId, elementId); + } + ); + clearSelectedElements(); + }, + }, + { + id: "toggle-snapping", + keys: ["n", "N"], + description: "Toggle snapping", + category: "Editing", + action: () => { + toggleSnapping(); + toast.info(`Snapping ${snappingEnabled ? "disabled" : "enabled"}`, { + duration: 1000, + }); + }, + }, + + // Selection & Organization + { + id: "select-all", + keys: ["Cmd+a", "Ctrl+a"], + description: "Select all elements", + category: "Selection", + action: () => { + const allElements = tracks.flatMap((track: any) => + track.elements.map((element: any) => ({ + trackId: track.id, + elementId: element.id, + })) + ); + setSelectedElements(allElements); + toast.info(`Selected ${allElements.length} elements`, { + duration: 1000, + }); + }, + }, + { + id: "duplicate-element", + keys: ["Cmd+d", "Ctrl+d"], + description: "Duplicate selected element", + category: "Selection", + requiresSelection: true, + action: () => { + if (selectedElements.length !== 1) { + toast.error("Select exactly one element to duplicate"); + return; + } + + const { trackId, elementId } = selectedElements[0]; + const track = tracks.find((t: any) => t.id === trackId); + const element = track?.elements.find((el: any) => el.id === elementId); + + if (element) { + const newStartTime = + element.startTime + + (element.duration - element.trimStart - element.trimEnd) + + 0.1; + const { id, ...elementWithoutId } = element; + + addElementToTrack(trackId, { + ...elementWithoutId, + startTime: newStartTime, + }); + + toast.success("Element duplicated"); + } + }, + }, + + // History + { + id: "undo", + keys: ["Cmd+z", "Ctrl+z"], + description: "Undo", + category: "History", + action: () => { + undo(); + }, + }, + { + id: "redo", + keys: ["Cmd+Shift+z", "Ctrl+Shift+z", "Cmd+y", "Ctrl+y"], + description: "Redo", + category: "History", + action: () => { + redo(); + }, + }, + ]; + + // Parse keyboard event to match against shortcuts + const parseKeyboardEvent = useCallback((e: KeyboardEvent): string => { + const parts: string[] = []; + + if (e.metaKey || e.ctrlKey) parts.push(e.metaKey ? "Cmd" : "Ctrl"); + if (e.shiftKey) parts.push("Shift"); + if (e.altKey) parts.push("Alt"); + + parts.push(e.key); + + return parts.join("+"); + }, []); + + // Handle keyboard events + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (!enabled || isInputFocused()) return; + + const keyCombo = parseKeyboardEvent(e); + const shortcut = shortcuts.find((s) => + s.keys.some((key) => key === keyCombo || key === e.key) + ); + + if (shortcut) { + // Check if shortcut requires selection + if (shortcut.requiresSelection && selectedElements.length === 0) { + return; + } + + e.preventDefault(); + shortcut.action(); + } + }, + [enabled, shortcuts, selectedElements, parseKeyboardEvent, isInputFocused] + ); + + // Set up event listener + useEffect(() => { + if (!enabled) return; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [enabled, handleKeyDown]); + + // Return shortcuts for help component + return { + shortcuts: shortcuts.filter((s) => s.enabled !== false), + enabled, + }; +}; diff --git a/apps/web/src/hooks/use-playback-controls.ts b/apps/web/src/hooks/use-playback-controls.ts index 1191d4b4..ecbd1232 100644 --- a/apps/web/src/hooks/use-playback-controls.ts +++ b/apps/web/src/hooks/use-playback-controls.ts @@ -106,68 +106,4 @@ export const usePlaybackControls = () => { separateAudio(trackId, elementId); }, [selectedElements, tracks, separateAudio]); - - const handleKeyPress = useCallback( - (e: KeyboardEvent) => { - if ( - e.target instanceof HTMLInputElement || - e.target instanceof HTMLTextAreaElement - ) { - return; - } - - switch (e.key) { - case " ": - e.preventDefault(); - if (isPlaying) { - pause(); - } else { - play(); - } - break; - - case "s": - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - handleSplitSelectedElement(); - } - break; - - case "q": - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - handleSplitAndKeepLeftCallback(); - } - break; - - case "w": - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - handleSplitAndKeepRightCallback(); - } - break; - - case "d": - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - handleSeparateAudioCallback(); - } - break; - } - }, - [ - isPlaying, - play, - pause, - handleSplitSelectedElement, - handleSplitAndKeepLeftCallback, - handleSplitAndKeepRightCallback, - handleSeparateAudioCallback, - ] - ); - - useEffect(() => { - document.addEventListener("keydown", handleKeyPress); - return () => document.removeEventListener("keydown", handleKeyPress); - }, [handleKeyPress]); }; -- cgit v1.3.1