From 3224dd974aa0a4906cae5642e8479a3618c49c03 Mon Sep 17 00:00:00 2001 From: DevloperAmanSingh Date: Thu, 26 Jun 2025 23:40:50 +0530 Subject: [PATCH] feat: add clip splitting and audio separation functionality to timeline store --- apps/web/src/stores/timeline-store.ts | 209 +++++++++++++++++++++++++- 1 file changed, 202 insertions(+), 7 deletions(-) diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index 3b068d7..a6da4e2 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -74,10 +74,28 @@ interface TimelineStore { ) => void; toggleTrackMute: (trackId: string) => void; + // Split operations + splitClip: ( + trackId: string, + clipId: string, + splitTime: number + ) => string | null; + splitAndKeepLeft: ( + trackId: string, + clipId: string, + splitTime: number + ) => void; + splitAndKeepRight: ( + trackId: string, + clipId: string, + splitTime: number + ) => void; + separateAudio: (trackId: string, clipId: string) => string | null; + // Computed values getTotalDuration: () => number; - // New actions + // History actions undo: () => void; redo: () => void; pushHistory: () => void; @@ -91,10 +109,9 @@ export const useTimelineStore = create((set, get) => ({ pushHistory: () => { const { tracks, history, redoStack } = get(); - // Deep copy tracks set({ history: [...history, JSON.parse(JSON.stringify(tracks))], - redoStack: [], // Clear redo stack when new action is performed + redoStack: [], }); }, @@ -105,7 +122,7 @@ export const useTimelineStore = create((set, get) => ({ set({ tracks: prev, history: history.slice(0, -1), - redoStack: [...redoStack, JSON.parse(JSON.stringify(tracks))], // Add current state to redo stack + redoStack: [...redoStack, JSON.parse(JSON.stringify(tracks))], }); }, @@ -115,7 +132,6 @@ export const useTimelineStore = create((set, get) => ({ (c) => c.trackId === trackId && c.clipId === clipId ); if (multi) { - // Toggle selection return exists ? { selectedClips: state.selectedClips.filter( @@ -128,6 +144,7 @@ export const useTimelineStore = create((set, get) => ({ } }); }, + deselectClip: (trackId, clipId) => { set((state) => ({ selectedClips: state.selectedClips.filter( @@ -135,6 +152,7 @@ export const useTimelineStore = create((set, get) => ({ ), })); }, + clearSelectedClips: () => { set({ selectedClips: [] }); }, @@ -194,7 +212,6 @@ export const useTimelineStore = create((set, get) => ({ } : track ) - // Remove track if it becomes empty .filter((track) => track.clips.length > 0), })); }, @@ -223,7 +240,6 @@ export const useTimelineStore = create((set, get) => ({ } return track; }) - // Remove track if it becomes empty .filter((track) => track.clips.length > 0), }; }); @@ -270,6 +286,185 @@ export const useTimelineStore = create((set, get) => ({ })); }, + splitClip: (trackId, clipId, splitTime) => { + const { tracks } = get(); + const track = tracks.find((t) => t.id === trackId); + const clip = track?.clips.find((c) => c.id === clipId); + + if (!clip) return null; + + const effectiveStart = clip.startTime; + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + + if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return null; + + get().pushHistory(); + + const relativeTime = splitTime - clip.startTime; + const firstDuration = relativeTime; + const secondDuration = + clip.duration - clip.trimStart - clip.trimEnd - relativeTime; + + const secondClipId = crypto.randomUUID(); + + set((state) => ({ + tracks: state.tracks.map((track) => + track.id === trackId + ? { + ...track, + clips: track.clips.flatMap((c) => + c.id === clipId + ? [ + { + ...c, + trimEnd: c.trimEnd + secondDuration, + name: c.name + " (left)", + }, + { + ...c, + id: secondClipId, + startTime: splitTime, + trimStart: c.trimStart + firstDuration, + name: c.name + " (right)", + }, + ] + : [c] + ), + } + : track + ), + })); + + return secondClipId; + }, + + splitAndKeepLeft: (trackId, clipId, splitTime) => { + const { tracks } = get(); + const track = tracks.find((t) => t.id === trackId); + const clip = track?.clips.find((c) => c.id === clipId); + + if (!clip) return; + + const effectiveStart = clip.startTime; + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + + if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return; + + get().pushHistory(); + + const relativeTime = splitTime - clip.startTime; + const durationToRemove = + clip.duration - clip.trimStart - clip.trimEnd - relativeTime; + + set((state) => ({ + tracks: state.tracks.map((track) => + track.id === trackId + ? { + ...track, + clips: track.clips.map((c) => + c.id === clipId + ? { + ...c, + trimEnd: c.trimEnd + durationToRemove, + name: c.name + " (left)", + } + : c + ), + } + : track + ), + })); + }, + + splitAndKeepRight: (trackId, clipId, splitTime) => { + const { tracks } = get(); + const track = tracks.find((t) => t.id === trackId); + const clip = track?.clips.find((c) => c.id === clipId); + + if (!clip) return; + + const effectiveStart = clip.startTime; + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + + if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return; + + get().pushHistory(); + + const relativeTime = splitTime - clip.startTime; + + set((state) => ({ + tracks: state.tracks.map((track) => + track.id === trackId + ? { + ...track, + clips: track.clips.map((c) => + c.id === clipId + ? { + ...c, + startTime: splitTime, + trimStart: c.trimStart + relativeTime, + name: c.name + " (right)", + } + : c + ), + } + : track + ), + })); + }, + + separateAudio: (trackId, clipId) => { + const { tracks } = get(); + const track = tracks.find((t) => t.id === trackId); + const clip = track?.clips.find((c) => c.id === clipId); + + if (!clip || track?.type !== "video") return null; + + get().pushHistory(); + + let audioTrackId = tracks.find((t) => t.type === "audio")?.id; + + if (!audioTrackId) { + audioTrackId = crypto.randomUUID(); + const newAudioTrack: TimelineTrack = { + id: audioTrackId, + name: "Audio Track", + type: "audio", + clips: [], + muted: false, + }; + + set((state) => ({ + tracks: [...state.tracks, newAudioTrack], + })); + } + + const audioClipId = crypto.randomUUID(); + + set((state) => ({ + tracks: state.tracks.map((track) => + track.id === audioTrackId + ? { + ...track, + clips: [ + ...track.clips, + { + ...clip, + id: audioClipId, + name: clip.name + " (audio)", + }, + ], + } + : track + ), + })); + + return audioClipId; + }, + getTotalDuration: () => { const { tracks } = get(); if (tracks.length === 0) return 0;