From 8bc26539ebe44d6ff0d74d86ef316934c7f18faf Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 14:37:08 +0200 Subject: [PATCH] cleanup (this doesn't work) --- apps/web/package.json | 2 +- apps/web/src/app/(auth)/login/page.tsx | 12 +- apps/web/src/app/(auth)/signup/page.tsx | 6 +- apps/web/src/components/editor-provider.tsx | 6 + apps/web/src/components/editor/timeline.tsx | 266 +------------ .../components/keyboard-shortcuts-help.tsx | 137 +------ apps/web/src/hooks/use-keyboard-shortcuts.ts | 349 ++++++++++++++++++ apps/web/src/hooks/use-playback-controls.ts | 64 ---- apps/web/src/types/global.d.ts | 19 - apps/web/src/types/modules.d.ts | 100 ----- bun.lock | 4 +- 11 files changed, 388 insertions(+), 577 deletions(-) create mode 100644 apps/web/src/hooks/use-keyboard-shortcuts.ts delete mode 100644 apps/web/src/types/global.d.ts delete mode 100644 apps/web/src/types/modules.d.ts diff --git a/apps/web/package.json b/apps/web/package.json index 98232fc5..193eb244 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -37,7 +37,7 @@ "embla-carousel-react": "^8.5.1", "framer-motion": "^11.13.1", "input-otp": "^1.4.1", - "lucide-react": "^0.525.0", + "lucide-react": "^0.468.0", "motion": "^12.18.1", "next": "^15.4.1", "next-themes": "^0.4.4", diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx index 6b1f8c83..f28cd191 100644 --- a/apps/web/src/app/(auth)/login/page.tsx +++ b/apps/web/src/app/(auth)/login/page.tsx @@ -15,7 +15,7 @@ import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import Link from "next/link"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { ChevronLeft, Loader2 } from "lucide-react"; +import { ArrowLeft, Loader2 } from "lucide-react"; import { GoogleIcon } from "@/components/icons"; import { useLogin } from "@/hooks/auth/useLogin"; @@ -41,7 +41,7 @@ const LoginPage = () => { onClick={() => router.back()} className="absolute top-6 left-6" > - Back + Back @@ -119,7 +119,11 @@ const LoginPage = () => { className="w-full h-11" size="lg" > - {isEmailLoading ? : "Sign in"} + {isEmailLoading ? ( + + ) : ( + "Sign in" + )} @@ -137,6 +141,6 @@ const LoginPage = () => { ); -} +}; export default memo(LoginPage); diff --git a/apps/web/src/app/(auth)/signup/page.tsx b/apps/web/src/app/(auth)/signup/page.tsx index a5bc3174..c3a0f899 100644 --- a/apps/web/src/app/(auth)/signup/page.tsx +++ b/apps/web/src/app/(auth)/signup/page.tsx @@ -15,7 +15,7 @@ import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import Link from "next/link"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { ChevronLeft, Loader2 } from "lucide-react"; +import { ArrowLeft, Loader2 } from "lucide-react"; import { GoogleIcon } from "@/components/icons"; import { useSignUp } from "@/hooks/auth/useSignUp"; @@ -43,7 +43,7 @@ const SignUpPage = () => { onClick={() => router.back()} className="absolute top-6 left-6" > - Back + Back @@ -157,6 +157,6 @@ const SignUpPage = () => { ); -} +}; export default memo(SignUpPage); diff --git a/apps/web/src/components/editor-provider.tsx b/apps/web/src/components/editor-provider.tsx index 38255956..d970f86d 100644 --- a/apps/web/src/components/editor-provider.tsx +++ b/apps/web/src/components/editor-provider.tsx @@ -3,6 +3,7 @@ import { useEffect } from "react"; import { Loader2 } from "lucide-react"; import { useEditorStore } from "@/stores/editor-store"; +import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts"; interface EditorProviderProps { children: React.ReactNode; @@ -11,6 +12,11 @@ interface EditorProviderProps { export function EditorProvider({ children }: EditorProviderProps) { const { isInitializing, isPanelsReady, initializeApp } = useEditorStore(); + useKeyboardShortcuts({ + enabled: !isInitializing && isPanelsReady, + context: "editor", + }); + useEffect(() => { initializeApp(); }, [initializeApp]); diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 6775e845..539846b3 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -1,6 +1,5 @@ "use client"; -// @ts-ignore - IDE TypeScript configuration issue (React modules compile fine in Docker) import { ScrollArea } from "../ui/scroll-area"; import { Button } from "../ui/button"; import { @@ -16,7 +15,6 @@ import { Video, Music, TypeIcon, - Magnet, Lock, LockOpen, } from "lucide-react"; @@ -39,9 +37,6 @@ import { useProjectStore } from "@/stores/project-store"; import { useTimelineZoom } from "@/hooks/use-timeline-zoom"; import { processMediaFiles } from "@/lib/media-processing"; import { toast } from "sonner"; -// @ts-ignore - React type definitions -import * as React from "react"; -// @ts-ignore - React type definitions import { useState, useRef, useEffect, useCallback } from "react"; import { TimelineTrackContent } from "./timeline-track"; import { @@ -65,9 +60,7 @@ export function Timeline() { // Timeline shows all tracks (video, audio, effects) and their elements. // You can drag media here to add it to your project. // elements can be trimmed, deleted, and moved. - - // Note: Some parameter types are inferred as 'any' due to store interface definitions - // This doesn't affect runtime safety as the stores provide proper type checking + const { tracks, addTrack, @@ -265,259 +258,6 @@ export function Timeline() { setDuration(Math.max(totalDuration, 10)); // Minimum 10 seconds for empty timeline }, [tracks, setDuration, getTotalDuration]); - // Keyboard event for deleting selected elements - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Don't trigger when typing in input fields or textareas - if ( - e.target instanceof HTMLInputElement || - e.target instanceof HTMLTextAreaElement - ) { - return; - } - - // Only trigger when timeline is focused or mouse is over timeline - if ( - !isInTimeline && - !timelineRef.current?.contains(document.activeElement) - ) { - return; - } - - if ( - (e.key === "Delete" || e.key === "Backspace") && - selectedElements.length > 0 - ) { - selectedElements.forEach(({ trackId, elementId }: { trackId: string; elementId: string }) => { - removeElementFromTrack(trackId, elementId); - }); - clearSelectedElements(); - } - }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [ - selectedElements, - removeElementFromTrack, - clearSelectedElements, - isInTimeline, - ]); - - // Keyboard event for undo (Cmd+Z) - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === "z" && !e.shiftKey) { - e.preventDefault(); - undo(); - } - }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [undo]); - - // Keyboard event for redo (Cmd+Shift+Z or Cmd+Y) - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === "z" && e.shiftKey) { - e.preventDefault(); - redo(); - } else if ((e.metaKey || e.ctrlKey) && e.key === "y") { - e.preventDefault(); - redo(); - } - }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [redo]); - - // Core video editor keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Don't interfere with typing in inputs - const activeElement = document.activeElement as HTMLElement; - const isInputFocused = activeElement && ( - activeElement.tagName === 'INPUT' || - activeElement.tagName === 'TEXTAREA' || - activeElement.contentEditable === 'true' - ); - - if (isInputFocused) return; - - const { key, metaKey, ctrlKey, shiftKey } = e; - const isModified = metaKey || ctrlKey || shiftKey; - - switch (key) { - case ' ': // Spacebar - Play/Pause - if (!isModified) { - e.preventDefault(); - toggle(); - toast.info(isPlaying ? 'Paused' : 'Playing', { duration: 1000 }); - } - break; - - case 'j': - case 'J': - if (!isModified) { - e.preventDefault(); - // J - Rewind 1 second - seek(Math.max(0, currentTime - 1)); - toast.info('Rewind 1s', { duration: 1000 }); - } - break; - - case 'k': - case 'K': - if (!isModified) { - e.preventDefault(); - // K - Pause/Play toggle - toggle(); - toast.info(isPlaying ? 'Paused' : 'Playing', { duration: 1000 }); - } - break; - - case 'l': - case 'L': - if (!isModified) { - e.preventDefault(); - // L - Fast forward 1 second - seek(Math.min(duration, currentTime + 1)); - toast.info('Forward 1s', { duration: 1000 }); - } - break; - - case 'ArrowLeft': - if (shiftKey) { - e.preventDefault(); - // Shift+Left - Jump back 5 seconds - seek(Math.max(0, currentTime - 5)); - } else if (!isModified) { - e.preventDefault(); - // Left - Frame step backward (1 frame) - const projectFps = activeProject?.fps || 30; - seek(Math.max(0, currentTime - (1/projectFps))); - } - break; - - case 'ArrowRight': - if (shiftKey) { - e.preventDefault(); - // Shift+Right - Jump forward 5 seconds - seek(Math.min(duration, currentTime + 5)); - } else if (!isModified) { - e.preventDefault(); - // Right - Frame step forward (1 frame) - const projectFps = activeProject?.fps || 30; - seek(Math.min(duration, currentTime + (1/projectFps))); - } - break; - - case 'Home': - if (!isModified) { - e.preventDefault(); - seek(0); - toast.info('Start of timeline', { duration: 1000 }); - } - break; - - case 'End': - if (!isModified) { - e.preventDefault(); - seek(duration); - toast.info('End of timeline', { duration: 1000 }); - } - break; - - case 's': - case 'S': - if (!isModified && selectedElements.length === 1) { - e.preventDefault(); - // S - Split element at playhead - 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'); - } - } - } - break; - - case 'n': - case 'N': - if (!isModified) { - e.preventDefault(); - toggleSnapping(); - toast.info(`Snapping ${snappingEnabled ? 'disabled' : 'enabled'}`, { duration: 1000 }); - } - break; - } - - // Multi-key shortcuts - if ((metaKey || ctrlKey) && !shiftKey) { - switch (key.toLowerCase()) { - case 'a': - e.preventDefault(); - // Cmd/Ctrl+A - Select all elements - 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 }); - break; - - case 'd': - if (selectedElements.length === 1) { - e.preventDefault(); - // Cmd/Ctrl+D - Duplicate selected element - 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'); - } - } - break; - } - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [ - isPlaying, - currentTime, - duration, - selectedElements, - tracks, - toggle, - seek, - splitElement, - snappingEnabled, - toggleSnapping, - setSelectedElements, - addElementToTrack - ]); - // Old marquee system removed - using new SelectionBox component instead const handleDragEnter = (e: React.DragEvent) => { @@ -575,7 +315,9 @@ export function Timeline() { useTimelineStore.getState().addTextToNewTrack(dragData); } else { // Handle media items - const mediaItem = mediaItems.find((item: any) => item.id === dragData.id); + const mediaItem = mediaItems.find( + (item: any) => item.id === dragData.id + ); if (!mediaItem) { toast.error("Media item not found"); return; diff --git a/apps/web/src/components/keyboard-shortcuts-help.tsx b/apps/web/src/components/keyboard-shortcuts-help.tsx index 129cb438..9f0d1e57 100644 --- a/apps/web/src/components/keyboard-shortcuts-help.tsx +++ b/apps/web/src/components/keyboard-shortcuts-help.tsx @@ -11,120 +11,8 @@ import { DialogTrigger, } from "./ui/dialog"; import { Badge } from "./ui/badge"; -import { Keyboard, Play, Pause, SkipBack, SkipForward } from "lucide-react"; - -interface Shortcut { - keys: string[]; - description: string; - category: string; - icon?: React.ReactNode; -} - -const shortcuts: Shortcut[] = [ - // Playback Controls - { - keys: ["Space"], - description: "Play/Pause", - category: "Playback", - icon: - }, - { - keys: ["J"], - description: "Rewind 1 second", - category: "Playback", - icon: - }, - { - keys: ["K"], - description: "Play/Pause (alternative)", - category: "Playback", - icon: - }, - { - keys: ["L"], - description: "Fast forward 1 second", - category: "Playback", - icon: - }, - - // Navigation - { - keys: ["←"], - description: "Frame step backward", - category: "Navigation" - }, - { - keys: ["→"], - description: "Frame step forward", - category: "Navigation" - }, - { - keys: ["Shift", "←"], - description: "Jump back 5 seconds", - category: "Navigation" - }, - { - keys: ["Shift", "→"], - description: "Jump forward 5 seconds", - category: "Navigation" - }, - { - keys: ["Home"], - description: "Go to timeline start", - category: "Navigation" - }, - { - keys: ["End"], - description: "Go to timeline end", - category: "Navigation" - }, - - // Editing - { - keys: ["S"], - description: "Split element at playhead", - category: "Editing" - }, - { - keys: ["Delete", "Backspace"], - description: "Delete selected elements", - category: "Editing" - }, - { - keys: ["N"], - description: "Toggle snapping", - category: "Editing" - }, - - // Selection & Organization - { - keys: ["Cmd", "A"], - description: "Select all elements", - category: "Selection" - }, - { - keys: ["Cmd", "D"], - description: "Duplicate selected element", - category: "Selection" - }, - - // History - { - keys: ["Cmd", "Z"], - description: "Undo", - category: "History" - }, - { - keys: ["Cmd", "Shift", "Z"], - description: "Redo", - category: "History" - }, - { - keys: ["Cmd", "Y"], - description: "Redo (alternative)", - category: "History" - } -]; +import { Keyboard } from "lucide-react"; +import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts"; const KeyBadge = ({ keyName }: { keyName: string }) => { // Replace common key names with symbols @@ -142,7 +30,7 @@ const KeyBadge = ({ keyName }: { keyName: string }) => { ); }; -const ShortcutItem = ({ shortcut }: { shortcut: Shortcut }) => ( +const ShortcutItem = ({ shortcut }: { shortcut: any }) => (
{shortcut.icon && ( @@ -151,7 +39,7 @@ const ShortcutItem = ({ shortcut }: { shortcut: Shortcut }) => ( {shortcut.description}
- {shortcut.keys.map((key, index) => ( + {shortcut.keys.map((key: string, index: number) => (
{index < shortcut.keys.length - 1 && ( @@ -166,7 +54,10 @@ const ShortcutItem = ({ shortcut }: { shortcut: Shortcut }) => ( export const KeyboardShortcutsHelp = () => { const [open, setOpen] = useState(false); - const categories = Array.from(new Set(shortcuts.map(s => s.category))); + // Get shortcuts from centralized hook (disabled so it doesn't add event listeners) + const { shortcuts } = useKeyboardShortcuts({ enabled: false }); + + const categories = Array.from(new Set(shortcuts.map((s) => s.category))); return ( @@ -187,16 +78,16 @@ export const KeyboardShortcutsHelp = () => { Most shortcuts work when the timeline is focused. - +
- {categories.map(category => ( + {categories.map((category) => (

{category}

{shortcuts - .filter(shortcut => shortcut.category === category) + .filter((shortcut) => shortcut.category === category) .map((shortcut, index) => ( ))} @@ -208,7 +99,9 @@ export const KeyboardShortcutsHelp = () => {

Tips:

    -
  • • Shortcuts work when the editor is focused (not typing in inputs)
  • +
  • + • Shortcuts work when the editor is focused (not typing in inputs) +
  • • J/K/L are industry-standard video editing shortcuts
  • • Use arrow keys for frame-perfect positioning
  • • Hold Shift with arrow keys for larger jumps
  • @@ -217,4 +110,4 @@ export const KeyboardShortcutsHelp = () => {
); -}; \ No newline at end of file +}; 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]); }; diff --git a/apps/web/src/types/global.d.ts b/apps/web/src/types/global.d.ts deleted file mode 100644 index f99e4cc3..00000000 --- a/apps/web/src/types/global.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/// -/// - -// Global React types -declare global { - namespace JSX { - interface IntrinsicElements { - [elemName: string]: any; - } - } -} - -// React module declarations -declare module 'react' { - export * from '@types/react'; -} - -// Ensure this file is treated as a module -export {}; \ No newline at end of file diff --git a/apps/web/src/types/modules.d.ts b/apps/web/src/types/modules.d.ts deleted file mode 100644 index 7cb7adfd..00000000 --- a/apps/web/src/types/modules.d.ts +++ /dev/null @@ -1,100 +0,0 @@ -// Type declarations for external modules without @types packages - -declare module 'lucide-react' { - import { FC, SVGProps } from 'react'; - - export interface IconProps extends SVGProps { - size?: string | number; - strokeWidth?: string | number; - } - - export const Scissors: FC; - export const ArrowLeftToLine: FC; - export const ArrowRightToLine: FC; - export const Trash2: FC; - export const Snowflake: FC; - export const Copy: FC; - export const SplitSquareHorizontal: FC; - export const Pause: FC; - export const Play: FC; - export const Video: FC; - export const Music: FC; - export const TypeIcon: FC; - export const Magnet: FC; - export const Lock: FC; - export const ChevronLeft: FC; - export const ChevronRight: FC; - export const ChevronDown: FC; - export const ChevronUp: FC; - export const Download: FC; - export const Keyboard: FC; - export const SkipBack: FC; - export const SkipForward: FC; - export const Loader2: FC; - export const ExternalLink: FC; - export const ArrowRight: FC; - export const ArrowLeft: FC; - export const Expand: FC; - export const Plus: FC; - export const Upload: FC; - export const Image: FC; - export const MoreVertical: FC; - export const MoreHorizontal: FC; - export const Eye: FC; - export const EyeOff: FC; - export const Check: FC; - export const Circle: FC; - export const Search: FC; - export const X: FC; - export const PanelLeft: FC; - export const ChevronsUpDown: FC; - export const CheckIcon: FC; - export const Minus: FC; - export const RefreshCw: FC; - export const PipetteIcon: FC; - export const Type: FC; - export const Calendar: FC; - export const CaptionsIcon: FC; - export const ArrowLeftRightIcon: FC; - export const SparklesIcon: FC; - export const StickerIcon: FC; - export const MusicIcon: FC; - export const VideoIcon: FC; - export const BlendIcon: FC; - export const SlidersHorizontalIcon: FC; - export type LucideIcon = FC; - // Add other icons as needed -} - -declare module 'sonner' { - export interface ToastOptions { - duration?: number; - position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center'; - style?: React.CSSProperties; - className?: string; - description?: string; - action?: { - label: string; - onClick: () => void; - }; - cancel?: { - label: string; - onClick?: () => void; - }; - id?: string | number; - onDismiss?: (toast: any) => void; - onAutoClose?: (toast: any) => void; - } - - export interface Toast { - success: (message: string, options?: ToastOptions) => void; - error: (message: string, options?: ToastOptions) => void; - info: (message: string, options?: ToastOptions) => void; - warning: (message: string, options?: ToastOptions) => void; - loading: (message: string, options?: ToastOptions) => void; - custom: (jsx: React.ReactNode, options?: ToastOptions) => void; - } - - export const toast: Toast; - export const Toaster: React.FC; -} \ No newline at end of file diff --git a/bun.lock b/bun.lock index 915c3d0e..7102ca46 100644 --- a/bun.lock +++ b/bun.lock @@ -38,7 +38,7 @@ "embla-carousel-react": "^8.5.1", "framer-motion": "^11.13.1", "input-otp": "^1.4.1", - "lucide-react": "^0.525.0", + "lucide-react": "^0.468.0", "motion": "^12.18.1", "next": "^15.4.1", "next-themes": "^0.4.4", @@ -734,7 +734,7 @@ "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "lucide-react": ["lucide-react@0.525.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ=="], + "lucide-react": ["lucide-react@0.468.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA=="], "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],