cleanup (this doesn't work)
This commit is contained in:
parent
527d588897
commit
8bc26539eb
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" /> Back
|
||||
<ArrowLeft className="h-5 w-5" /> Back
|
||||
</Button>
|
||||
<Card className="w-[400px] shadow-lg border-0">
|
||||
<CardHeader className="text-center pb-4">
|
||||
|
|
@ -119,7 +119,11 @@ const LoginPage = () => {
|
|||
className="w-full h-11"
|
||||
size="lg"
|
||||
>
|
||||
{isEmailLoading ? <Loader2 className="animate-spin" /> : "Sign in"}
|
||||
{isEmailLoading ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
"Sign in"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -137,6 +141,6 @@ const LoginPage = () => {
|
|||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(LoginPage);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" /> Back
|
||||
<ArrowLeft className="h-5 w-5" /> Back
|
||||
</Button>
|
||||
<Card className="w-[400px] shadow-lg border-0">
|
||||
<CardHeader className="text-center pb-4">
|
||||
|
|
@ -157,6 +157,6 @@ const SignUpPage = () => {
|
|||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(SignUpPage);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: <Play className="w-3 h-3" />
|
||||
},
|
||||
{
|
||||
keys: ["J"],
|
||||
description: "Rewind 1 second",
|
||||
category: "Playback",
|
||||
icon: <SkipBack className="w-3 h-3" />
|
||||
},
|
||||
{
|
||||
keys: ["K"],
|
||||
description: "Play/Pause (alternative)",
|
||||
category: "Playback",
|
||||
icon: <Pause className="w-3 h-3" />
|
||||
},
|
||||
{
|
||||
keys: ["L"],
|
||||
description: "Fast forward 1 second",
|
||||
category: "Playback",
|
||||
icon: <SkipForward className="w-3 h-3" />
|
||||
},
|
||||
|
||||
// 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 }) => (
|
||||
<div className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
{shortcut.icon && (
|
||||
|
|
@ -151,7 +39,7 @@ const ShortcutItem = ({ shortcut }: { shortcut: Shortcut }) => (
|
|||
<span className="text-sm">{shortcut.description}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{shortcut.keys.map((key, index) => (
|
||||
{shortcut.keys.map((key: string, index: number) => (
|
||||
<div key={index} className="flex items-center gap-1">
|
||||
<KeyBadge keyName={key} />
|
||||
{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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
|
|
@ -187,16 +78,16 @@ export const KeyboardShortcutsHelp = () => {
|
|||
Most shortcuts work when the timeline is focused.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<div className="space-y-6">
|
||||
{categories.map(category => (
|
||||
{categories.map((category) => (
|
||||
<div key={category}>
|
||||
<h3 className="font-semibold text-sm text-muted-foreground uppercase tracking-wide mb-3">
|
||||
{category}
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{shortcuts
|
||||
.filter(shortcut => shortcut.category === category)
|
||||
.filter((shortcut) => shortcut.category === category)
|
||||
.map((shortcut, index) => (
|
||||
<ShortcutItem key={index} shortcut={shortcut} />
|
||||
))}
|
||||
|
|
@ -208,7 +99,9 @@ export const KeyboardShortcutsHelp = () => {
|
|||
<div className="mt-6 p-4 bg-muted/30 rounded-lg">
|
||||
<h4 className="font-medium text-sm mb-2">Tips:</h4>
|
||||
<ul className="text-sm text-muted-foreground space-y-1">
|
||||
<li>• Shortcuts work when the editor is focused (not typing in inputs)</li>
|
||||
<li>
|
||||
• Shortcuts work when the editor is focused (not typing in inputs)
|
||||
</li>
|
||||
<li>• J/K/L are industry-standard video editing shortcuts</li>
|
||||
<li>• Use arrow keys for frame-perfect positioning</li>
|
||||
<li>• Hold Shift with arrow keys for larger jumps</li>
|
||||
|
|
@ -217,4 +110,4 @@ export const KeyboardShortcutsHelp = () => {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
/// <reference types="react" />
|
||||
/// <reference types="react-dom" />
|
||||
|
||||
// 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 {};
|
||||
|
|
@ -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<SVGSVGElement> {
|
||||
size?: string | number;
|
||||
strokeWidth?: string | number;
|
||||
}
|
||||
|
||||
export const Scissors: FC<IconProps>;
|
||||
export const ArrowLeftToLine: FC<IconProps>;
|
||||
export const ArrowRightToLine: FC<IconProps>;
|
||||
export const Trash2: FC<IconProps>;
|
||||
export const Snowflake: FC<IconProps>;
|
||||
export const Copy: FC<IconProps>;
|
||||
export const SplitSquareHorizontal: FC<IconProps>;
|
||||
export const Pause: FC<IconProps>;
|
||||
export const Play: FC<IconProps>;
|
||||
export const Video: FC<IconProps>;
|
||||
export const Music: FC<IconProps>;
|
||||
export const TypeIcon: FC<IconProps>;
|
||||
export const Magnet: FC<IconProps>;
|
||||
export const Lock: FC<IconProps>;
|
||||
export const ChevronLeft: FC<IconProps>;
|
||||
export const ChevronRight: FC<IconProps>;
|
||||
export const ChevronDown: FC<IconProps>;
|
||||
export const ChevronUp: FC<IconProps>;
|
||||
export const Download: FC<IconProps>;
|
||||
export const Keyboard: FC<IconProps>;
|
||||
export const SkipBack: FC<IconProps>;
|
||||
export const SkipForward: FC<IconProps>;
|
||||
export const Loader2: FC<IconProps>;
|
||||
export const ExternalLink: FC<IconProps>;
|
||||
export const ArrowRight: FC<IconProps>;
|
||||
export const ArrowLeft: FC<IconProps>;
|
||||
export const Expand: FC<IconProps>;
|
||||
export const Plus: FC<IconProps>;
|
||||
export const Upload: FC<IconProps>;
|
||||
export const Image: FC<IconProps>;
|
||||
export const MoreVertical: FC<IconProps>;
|
||||
export const MoreHorizontal: FC<IconProps>;
|
||||
export const Eye: FC<IconProps>;
|
||||
export const EyeOff: FC<IconProps>;
|
||||
export const Check: FC<IconProps>;
|
||||
export const Circle: FC<IconProps>;
|
||||
export const Search: FC<IconProps>;
|
||||
export const X: FC<IconProps>;
|
||||
export const PanelLeft: FC<IconProps>;
|
||||
export const ChevronsUpDown: FC<IconProps>;
|
||||
export const CheckIcon: FC<IconProps>;
|
||||
export const Minus: FC<IconProps>;
|
||||
export const RefreshCw: FC<IconProps>;
|
||||
export const PipetteIcon: FC<IconProps>;
|
||||
export const Type: FC<IconProps>;
|
||||
export const Calendar: FC<IconProps>;
|
||||
export const CaptionsIcon: FC<IconProps>;
|
||||
export const ArrowLeftRightIcon: FC<IconProps>;
|
||||
export const SparklesIcon: FC<IconProps>;
|
||||
export const StickerIcon: FC<IconProps>;
|
||||
export const MusicIcon: FC<IconProps>;
|
||||
export const VideoIcon: FC<IconProps>;
|
||||
export const BlendIcon: FC<IconProps>;
|
||||
export const SlidersHorizontalIcon: FC<IconProps>;
|
||||
export type LucideIcon = FC<IconProps>;
|
||||
// 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<any>;
|
||||
}
|
||||
4
bun.lock
4
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=="],
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue