diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 484e1a6..064d0dd 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -40,7 +40,7 @@ export function Timeline() { // Timeline shows all tracks (video, audio, effects) and their clips. // You can drag media here to add it to your project. // Clips can be trimmed, deleted, and moved. - const { tracks, addTrack, addClipToTrack, removeTrack, toggleTrackMute, removeClipFromTrack, moveClipToTrack, getTotalDuration, selectedClips, selectClip, deselectClip, clearSelectedClips, setSelectedClips, updateClipTrim, undo } = + const { tracks, addTrack, addClipToTrack, removeTrack, toggleTrackMute, removeClipFromTrack, moveClipToTrack, getTotalDuration, selectedClips, selectClip, deselectClip, clearSelectedClips, setSelectedClips, updateClipTrim, undo, redo } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); const { currentTime, duration, seek, setDuration, isPlaying, play, pause, toggle, setSpeed, speed } = usePlaybackStore(); @@ -110,6 +110,21 @@ export function Timeline() { 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]); + // Mouse down on timeline background to start marquee const handleTimelineMouseDown = (e: React.MouseEvent) => { if (e.target === e.currentTarget && e.button === 0) { diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index a369d57..e1474ba 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -21,6 +21,7 @@ export interface TimelineTrack { interface TimelineStore { tracks: TimelineTrack[]; history: TimelineTrack[][]; + redoStack: TimelineTrack[][]; // Multi-selection selectedClips: { trackId: string; clipId: string }[]; @@ -57,25 +58,34 @@ interface TimelineStore { // New actions undo: () => void; + redo: () => void; pushHistory: () => void; } export const useTimelineStore = create((set, get) => ({ tracks: [], history: [], + redoStack: [], selectedClips: [], pushHistory: () => { - const { tracks, history } = get(); + const { tracks, history, redoStack } = get(); // Deep copy tracks - set({ history: [...history, JSON.parse(JSON.stringify(tracks))] }); + set({ + history: [...history, JSON.parse(JSON.stringify(tracks))], + redoStack: [] // Clear redo stack when new action is performed + }); }, undo: () => { - const { history } = get(); + const { history, redoStack, tracks } = get(); if (history.length === 0) return; const prev = history[history.length - 1]; - set({ tracks: prev, history: history.slice(0, -1) }); + set({ + tracks: prev, + history: history.slice(0, -1), + redoStack: [...redoStack, JSON.parse(JSON.stringify(tracks))] // Add current state to redo stack + }); }, selectClip: (trackId, clipId, multi = false) => { @@ -244,4 +254,11 @@ export const useTimelineStore = create((set, get) => ({ return Math.max(...trackEndTimes, 0); }, + + redo: () => { + const { redoStack } = get(); + if (redoStack.length === 0) return; + const next = redoStack[redoStack.length - 1]; + set({ tracks: next, redoStack: redoStack.slice(0, -1) }); + }, }));