From 12d26c574b4517f358d97d130b0838f4868a8b85 Mon Sep 17 00:00:00 2001 From: aashishparuvada Date: Mon, 23 Jun 2025 20:05:49 +0530 Subject: enabled-clip-selection --- apps/web/src/stores/timeline-store.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'apps/web/src/stores/timeline-store.ts') diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index c569ae11..ea6fc35e 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -21,6 +21,11 @@ export interface TimelineTrack { interface TimelineStore { tracks: TimelineTrack[]; + // Selection + selectedClip: { trackId: string; clipId: string } | null; + selectClip: (trackId: string, clipId: string) => void; + clearSelectedClip: () => void; + // Actions addTrack: (type: "video" | "audio" | "effects") => string; removeTrack: (trackId: string) => void; @@ -50,6 +55,14 @@ interface TimelineStore { export const useTimelineStore = create((set, get) => ({ tracks: [], + selectedClip: null, + + selectClip: (trackId, clipId) => { + set({ selectedClip: { trackId, clipId } }); + }, + clearSelectedClip: () => { + set({ selectedClip: null }); + }, addTrack: (type) => { const newTrack: TimelineTrack = { -- cgit v1.3.1 From 630250c23b1cd8ec608c95334ae2babf8e7a00af Mon Sep 17 00:00:00 2001 From: aashishparuvada Date: Mon, 23 Jun 2025 20:51:36 +0530 Subject: enabled-multiclip-selection --- apps/web/src/components/editor/timeline.tsx | 165 ++++++++++++++++++++++++---- apps/web/src/stores/timeline-store.ts | 36 ++++-- 2 files changed, 168 insertions(+), 33 deletions(-) (limited to 'apps/web/src/stores/timeline-store.ts') diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index a16284ab..1120fbe0 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -33,7 +33,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, selectedClip, clearSelectedClip } = + const { tracks, addTrack, addClipToTrack, removeTrack, toggleTrackMute, removeClipFromTrack, moveClipToTrack, getTotalDuration, selectedClips, selectClip, deselectClip, clearSelectedClips } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); const { currentTime, duration, seek, setDuration, isPlaying, play, pause, toggle } = usePlaybackStore(); @@ -52,6 +52,16 @@ export function Timeline() { y: number; } | null>(null); + // Marquee selection state + const [marquee, setMarquee] = useState<{ + startX: number; + startY: number; + endX: number; + endY: number; + active: boolean; + additive: boolean; + } | null>(null); + // Update timeline duration when tracks change useEffect(() => { const totalDuration = getTotalDuration(); @@ -67,17 +77,99 @@ export function Timeline() { } }, [contextMenu]); - // Keyboard event for deleting selected clip + // Keyboard event for deleting selected clips useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if ((e.key === "Delete" || e.key === "Backspace") && selectedClip) { - removeClipFromTrack(selectedClip.trackId, selectedClip.clipId); - clearSelectedClip(); + if ((e.key === "Delete" || e.key === "Backspace") && selectedClips.length > 0) { + selectedClips.forEach(({ trackId, clipId }) => { + removeClipFromTrack(trackId, clipId); + }); + clearSelectedClips(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [selectedClip, removeClipFromTrack, clearSelectedClip]); + }, [selectedClips, removeClipFromTrack, clearSelectedClips]); + + // Mouse down on timeline background to start marquee + const handleTimelineMouseDown = (e: React.MouseEvent) => { + if (e.target === e.currentTarget && e.button === 0) { + setMarquee({ + startX: e.clientX, + startY: e.clientY, + endX: e.clientX, + endY: e.clientY, + active: true, + additive: e.metaKey || e.ctrlKey || e.shiftKey, + }); + } + }; + + // Mouse move to update marquee + useEffect(() => { + if (!marquee || !marquee.active) return; + const handleMouseMove = (e: MouseEvent) => { + setMarquee((prev) => prev && { ...prev, endX: e.clientX, endY: e.clientY }); + }; + const handleMouseUp = (e: MouseEvent) => { + setMarquee((prev) => prev && { ...prev, endX: e.clientX, endY: e.clientY, active: false }); + }; + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [marquee]); + + // On marquee end, select clips in box + useEffect(() => { + if (!marquee || marquee.active) return; + // Calculate selection box in timeline coordinates + const timeline = timelineRef.current; + if (!timeline) return; + const rect = timeline.getBoundingClientRect(); + const x1 = Math.min(marquee.startX, marquee.endX) - rect.left; + const x2 = Math.max(marquee.startX, marquee.endX) - rect.left; + const y1 = Math.min(marquee.startY, marquee.endY) - rect.top; + const y2 = Math.max(marquee.startY, marquee.endY) - rect.top; + // Find all clips that intersect the box + let newSelection: { trackId: string; clipId: string }[] = []; + tracks.forEach((track, trackIdx) => { + track.clips.forEach((clip) => { + const effectiveDuration = clip.duration - clip.trimStart - clip.trimEnd; + const clipWidth = Math.max(80, effectiveDuration * 50 * zoomLevel); + const clipLeft = clip.startTime * 50 * zoomLevel; + const clipTop = trackIdx * 60; + const clipBottom = clipTop + 60; + const clipRight = clipLeft + clipWidth; + // Check intersection + if ( + x1 < clipRight && + x2 > clipLeft && + y1 < clipBottom && + y2 > clipTop + ) { + newSelection.push({ trackId: track.id, clipId: clip.id }); + } + }); + }); + if (newSelection.length > 0) { + if (marquee.additive) { + // Add to current selection + const current = new Set(selectedClips.map((c) => c.trackId + ":" + c.clipId)); + newSelection = [ + ...selectedClips, + ...newSelection.filter((c) => !current.has(c.trackId + ":" + c.clipId)), + ]; + } + clearSelectedClips(); + newSelection.forEach((c) => selectClip(c.trackId, c.clipId, true)); + } else if (!marquee.additive) { + clearSelectedClips(); + } + setMarquee(null); + }, [marquee, tracks, zoomLevel, selectedClips, selectClip, clearSelectedClips]); const handleDragEnter = (e: React.DragEvent) => { // When something is dragged over the timeline, show overlay @@ -185,17 +277,12 @@ export function Timeline() { } }; - const handleTimelineClick = (e: React.MouseEvent) => { - const timeline = timelineRef.current; - if (!timeline || duration === 0) return; - - const rect = timeline.getBoundingClientRect(); - const x = e.clientX - rect.left; - const timelineWidth = rect.width; - const visibleDuration = duration / zoomLevel; - const clickedTime = (x / timelineWidth) * visibleDuration; - - seek(Math.max(0, Math.min(duration, clickedTime))); + // Deselect all clips when clicking empty timeline area + const handleTimelineAreaClick = (e: React.MouseEvent) => { + // Only clear selection if the click target is the timeline background (not a child/clip) + if (e.target === e.currentTarget) { + clearSelectedClips(); + } }; const handleWheel = (e: React.WheelEvent) => { @@ -500,9 +587,33 @@ export function Timeline() { minHeight: tracks.length > 0 ? `${tracks.length * 60}px` : "200px", }} - onClick={handleTimelineClick} + onClick={handleTimelineAreaClick} + onMouseDown={handleTimelineMouseDown} onWheel={handleWheel} > + {/* Overlay for deselect/marquee */} +
+ {/* Marquee selection rectangle */} + {marquee && marquee.active && timelineRef.current && ( +
+ )} {tracks.length === 0 ? (
@@ -711,8 +822,9 @@ function TimelineTrackContent({ addClipToTrack, removeClipFromTrack, toggleTrackMute, - selectedClip, + selectedClips, selectClip, + deselectClip, } = useTimelineStore(); const { currentTime } = usePlaybackStore(); const [isDropping, setIsDropping] = useState(false); @@ -1274,10 +1386,9 @@ function TimelineTrackContent({ effectiveDuration * 50 * zoomLevel ); const clipLeft = clip.startTime * 50 * zoomLevel; - - // Correctly declare isSelected inside the map - const isSelected = selectedClip && selectedClip.trackId === track.id && selectedClip.clipId === clip.id; - + const isSelected = selectedClips.some( + (c) => c.trackId === track.id && c.clipId === clip.id + ); return (
{ e.stopPropagation(); - selectClip(track.id, clip.id); + if (e.metaKey || e.ctrlKey || e.shiftKey) { + selectClip(track.id, clip.id, true); + } else if (isSelected) { + deselectClip(track.id, clip.id); + } else { + selectClip(track.id, clip.id, false); + } }} tabIndex={0} onContextMenu={(e) => { diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index ea6fc35e..c6ecaea5 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -21,10 +21,11 @@ export interface TimelineTrack { interface TimelineStore { tracks: TimelineTrack[]; - // Selection - selectedClip: { trackId: string; clipId: string } | null; - selectClip: (trackId: string, clipId: string) => void; - clearSelectedClip: () => void; + // Multi-selection + selectedClips: { trackId: string; clipId: string }[]; + selectClip: (trackId: string, clipId: string, multi?: boolean) => void; + deselectClip: (trackId: string, clipId: string) => void; + clearSelectedClips: () => void; // Actions addTrack: (type: "video" | "audio" | "effects") => string; @@ -55,13 +56,30 @@ interface TimelineStore { export const useTimelineStore = create((set, get) => ({ tracks: [], - selectedClip: null, + selectedClips: [], - selectClip: (trackId, clipId) => { - set({ selectedClip: { trackId, clipId } }); + selectClip: (trackId, clipId, multi = false) => { + set((state) => { + const exists = state.selectedClips.some( + (c) => c.trackId === trackId && c.clipId === clipId + ); + if (multi) { + // Toggle selection + return exists + ? { selectedClips: state.selectedClips.filter((c) => !(c.trackId === trackId && c.clipId === clipId)) } + : { selectedClips: [...state.selectedClips, { trackId, clipId }] }; + } else { + return { selectedClips: [{ trackId, clipId }] }; + } + }); + }, + deselectClip: (trackId, clipId) => { + set((state) => ({ + selectedClips: state.selectedClips.filter((c) => !(c.trackId === trackId && c.clipId === clipId)), + })); }, - clearSelectedClip: () => { - set({ selectedClip: null }); + clearSelectedClips: () => { + set({ selectedClips: [] }); }, addTrack: (type) => { -- cgit v1.3.1 From f44f0acf7024febdbbecbbf10772b9b9d3863b4f Mon Sep 17 00:00:00 2001 From: aashishparuvada Date: Mon, 23 Jun 2025 21:16:21 +0530 Subject: hotfix-improved-selection-standards --- apps/web/src/components/editor/timeline.tsx | 10 +++------- apps/web/src/stores/timeline-store.ts | 3 +++ 2 files changed, 6 insertions(+), 7 deletions(-) (limited to 'apps/web/src/stores/timeline-store.ts') diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 1120fbe0..e824d42e 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -33,7 +33,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 } = + const { tracks, addTrack, addClipToTrack, removeTrack, toggleTrackMute, removeClipFromTrack, moveClipToTrack, getTotalDuration, selectedClips, selectClip, deselectClip, clearSelectedClips, setSelectedClips } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); const { currentTime, duration, seek, setDuration, isPlaying, play, pause, toggle } = usePlaybackStore(); @@ -156,20 +156,18 @@ export function Timeline() { }); if (newSelection.length > 0) { if (marquee.additive) { - // Add to current selection const current = new Set(selectedClips.map((c) => c.trackId + ":" + c.clipId)); newSelection = [ ...selectedClips, ...newSelection.filter((c) => !current.has(c.trackId + ":" + c.clipId)), ]; } - clearSelectedClips(); - newSelection.forEach((c) => selectClip(c.trackId, c.clipId, true)); + setSelectedClips(newSelection); } else if (!marquee.additive) { clearSelectedClips(); } setMarquee(null); - }, [marquee, tracks, zoomLevel, selectedClips, selectClip, clearSelectedClips]); + }, [marquee, tracks, zoomLevel, selectedClips, selectClip, clearSelectedClips, setSelectedClips]); const handleDragEnter = (e: React.DragEvent) => { // When something is dragged over the timeline, show overlay @@ -1398,8 +1396,6 @@ function TimelineTrackContent({ e.stopPropagation(); if (e.metaKey || e.ctrlKey || e.shiftKey) { selectClip(track.id, clip.id, true); - } else if (isSelected) { - deselectClip(track.id, clip.id); } else { selectClip(track.id, clip.id, false); } diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index c6ecaea5..4b55a53d 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -26,6 +26,7 @@ interface TimelineStore { selectClip: (trackId: string, clipId: string, multi?: boolean) => void; deselectClip: (trackId: string, clipId: string) => void; clearSelectedClips: () => void; + setSelectedClips: (clips: { trackId: string; clipId: string }[]) => void; // Actions addTrack: (type: "video" | "audio" | "effects") => string; @@ -82,6 +83,8 @@ export const useTimelineStore = create((set, get) => ({ set({ selectedClips: [] }); }, + setSelectedClips: (clips) => set({ selectedClips: clips }), + addTrack: (type) => { const newTrack: TimelineTrack = { id: crypto.randomUUID(), -- cgit v1.3.1 From 5eb29bb01d5366756cf73c852e4707cdc235e80c Mon Sep 17 00:00:00 2001 From: aashishparuvada Date: Tue, 24 Jun 2025 09:26:31 +0530 Subject: feat:implemented-undo-feature-for-timeline --- apps/web/src/components/editor/timeline.tsx | 14 +++++- apps/web/src/stores/timeline-store.ts | 74 ++++++++++++++++++++--------- 2 files changed, 65 insertions(+), 23 deletions(-) (limited to 'apps/web/src/stores/timeline-store.ts') diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index fb94b374..484e1a6b 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 } = + const { tracks, addTrack, addClipToTrack, removeTrack, toggleTrackMute, removeClipFromTrack, moveClipToTrack, getTotalDuration, selectedClips, selectClip, deselectClip, clearSelectedClips, setSelectedClips, updateClipTrim, undo } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); const { currentTime, duration, seek, setDuration, isPlaying, play, pause, toggle, setSpeed, speed } = usePlaybackStore(); @@ -98,6 +98,18 @@ export function Timeline() { return () => window.removeEventListener("keydown", handleKeyDown); }, [selectedClips, removeClipFromTrack, clearSelectedClips]); + // 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]); + // 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 4b55a53d..a369d574 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -20,6 +20,7 @@ export interface TimelineTrack { interface TimelineStore { tracks: TimelineTrack[]; + history: TimelineTrack[][]; // Multi-selection selectedClips: { trackId: string; clipId: string }[]; @@ -53,12 +54,30 @@ interface TimelineStore { // Computed values getTotalDuration: () => number; + + // New actions + undo: () => void; + pushHistory: () => void; } export const useTimelineStore = create((set, get) => ({ tracks: [], + history: [], selectedClips: [], + pushHistory: () => { + const { tracks, history } = get(); + // Deep copy tracks + set({ history: [...history, JSON.parse(JSON.stringify(tracks))] }); + }, + + undo: () => { + const { history } = get(); + if (history.length === 0) return; + const prev = history[history.length - 1]; + set({ tracks: prev, history: history.slice(0, -1) }); + }, + selectClip: (trackId, clipId, multi = false) => { set((state) => { const exists = state.selectedClips.some( @@ -86,6 +105,7 @@ export const useTimelineStore = create((set, get) => ({ setSelectedClips: (clips) => set({ selectedClips: clips }), addTrack: (type) => { + get().pushHistory(); const newTrack: TimelineTrack = { id: crypto.randomUUID(), name: `${type.charAt(0).toUpperCase() + type.slice(1)} Track`, @@ -100,12 +120,14 @@ export const useTimelineStore = create((set, get) => ({ }, removeTrack: (trackId) => { + get().pushHistory(); set((state) => ({ tracks: state.tracks.filter((track) => track.id !== trackId), })); }, addClipToTrack: (trackId, clipData) => { + get().pushHistory(); const newClip: TimelineClip = { ...clipData, id: crypto.randomUUID(), @@ -124,19 +146,21 @@ export const useTimelineStore = create((set, get) => ({ }, removeClipFromTrack: (trackId, clipId) => { + get().pushHistory(); set((state) => ({ - tracks: state.tracks.map((track) => - track.id === trackId - ? { - ...track, - clips: track.clips.filter((clip) => clip.id !== clipId), - } - : track - ), + tracks: state.tracks + .map((track) => + track.id === trackId + ? { ...track, clips: track.clips.filter((clip) => clip.id !== clipId) } + : track + ) + // Remove track if it becomes empty + .filter((track) => track.clips.length > 0), })); }, moveClipToTrack: (fromTrackId, toTrackId, clipId) => { + get().pushHistory(); set((state) => { const fromTrack = state.tracks.find((track) => track.id === fromTrackId); const clipToMove = fromTrack?.clips.find((clip) => clip.id === clipId); @@ -144,25 +168,29 @@ export const useTimelineStore = create((set, get) => ({ if (!clipToMove) return state; return { - tracks: state.tracks.map((track) => { - if (track.id === fromTrackId) { - return { - ...track, - clips: track.clips.filter((clip) => clip.id !== clipId), - }; - } else if (track.id === toTrackId) { - return { - ...track, - clips: [...track.clips, clipToMove], - }; - } - return track; - }), + tracks: state.tracks + .map((track) => { + if (track.id === fromTrackId) { + return { + ...track, + clips: track.clips.filter((clip) => clip.id !== clipId), + }; + } else if (track.id === toTrackId) { + return { + ...track, + clips: [...track.clips, clipToMove], + }; + } + return track; + }) + // Remove track if it becomes empty + .filter((track) => track.clips.length > 0), }; }); }, updateClipTrim: (trackId, clipId, trimStart, trimEnd) => { + get().pushHistory(); set((state) => ({ tracks: state.tracks.map((track) => track.id === trackId @@ -178,6 +206,7 @@ export const useTimelineStore = create((set, get) => ({ }, updateClipStartTime: (trackId, clipId, startTime) => { + get().pushHistory(); set((state) => ({ tracks: state.tracks.map((track) => track.id === trackId @@ -193,6 +222,7 @@ export const useTimelineStore = create((set, get) => ({ }, toggleTrackMute: (trackId) => { + get().pushHistory(); set((state) => ({ tracks: state.tracks.map((track) => track.id === trackId ? { ...track, muted: !track.muted } : track -- cgit v1.3.1 From 073f0d89bd8ee6ccd81a3a4b3948f88816812bdf Mon Sep 17 00:00:00 2001 From: aashishparuvada Date: Tue, 24 Jun 2025 09:31:53 +0530 Subject: feat:enabled-redo-functionality --- apps/web/src/components/editor/timeline.tsx | 17 ++++++++++++++++- apps/web/src/stores/timeline-store.ts | 25 +++++++++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) (limited to 'apps/web/src/stores/timeline-store.ts') diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 484e1a6b..064d0dd2 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 a369d574..e1474bae 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) }); + }, })); -- cgit v1.3.1