From 3224dd974aa0a4906cae5642e8479a3618c49c03 Mon Sep 17 00:00:00 2001 From: DevloperAmanSingh Date: Thu, 26 Jun 2025 23:40:50 +0530 Subject: 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(-) (limited to 'apps/web/src/stores') diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index 3b068d7c..a6da4e25 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; -- cgit v1.3.1 From a816cc503f692f43b966b6f166691a18fc3fe76f Mon Sep 17 00:00:00 2001 From: DevloperAmanSingh Date: Thu, 26 Jun 2025 23:46:43 +0530 Subject: feat: add detailed comments for clip splitting and audio separation methods in timeline store --- apps/web/src/stores/timeline-store.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'apps/web/src/stores') diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index a6da4e25..f789548f 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -74,7 +74,7 @@ interface TimelineStore { ) => void; toggleTrackMute: (trackId: string) => void; - // Split operations + // Split operations for clips splitClip: ( trackId: string, clipId: string, @@ -339,6 +339,7 @@ export const useTimelineStore = create((set, get) => ({ return secondClipId; }, + // Split clip and keep only the left portion splitAndKeepLeft: (trackId, clipId, splitTime) => { const { tracks } = get(); const track = tracks.find((t) => t.id === trackId); @@ -378,6 +379,7 @@ export const useTimelineStore = create((set, get) => ({ })); }, + // Split clip and keep only the right portion splitAndKeepRight: (trackId, clipId, splitTime) => { const { tracks } = get(); const track = tracks.find((t) => t.id === trackId); @@ -416,6 +418,7 @@ export const useTimelineStore = create((set, get) => ({ })); }, + // Extract audio from video clip to an audio track separateAudio: (trackId, clipId) => { const { tracks } = get(); const track = tracks.find((t) => t.id === trackId); @@ -425,6 +428,7 @@ export const useTimelineStore = create((set, get) => ({ get().pushHistory(); + // Find or create an audio track let audioTrackId = tracks.find((t) => t.type === "audio")?.id; if (!audioTrackId) { -- cgit v1.3.1 From 6262f2b3797623cc1b62c82a8e810ad3aaf1d2b1 Mon Sep 17 00:00:00 2001 From: DevloperAmanSingh Date: Fri, 27 Jun 2025 00:07:47 +0530 Subject: refactor: introduce helper function for consistent clip naming with suffixes in timeline store --- apps/web/src/stores/timeline-store.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) (limited to 'apps/web/src/stores') diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index f789548f..dd97879f 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -1,5 +1,20 @@ import { create } from "zustand"; +// Helper function to manage clip naming with suffixes +const getClipNameWithSuffix = ( + originalName: string, + suffix: string +): string => { + // Remove existing suffixes to prevent accumulation + const baseName = originalName + .replace(/ \(left\)$/, "") + .replace(/ \(right\)$/, "") + .replace(/ \(audio\)$/, "") + .replace(/ \(split \d+\)$/, ""); + + return `${baseName} (${suffix})`; +}; + export interface TimelineClip { id: string; mediaId: string; @@ -319,14 +334,14 @@ export const useTimelineStore = create((set, get) => ({ { ...c, trimEnd: c.trimEnd + secondDuration, - name: c.name + " (left)", + name: getClipNameWithSuffix(c.name, "left"), }, { ...c, id: secondClipId, startTime: splitTime, trimStart: c.trimStart + firstDuration, - name: c.name + " (right)", + name: getClipNameWithSuffix(c.name, "right"), }, ] : [c] @@ -369,7 +384,7 @@ export const useTimelineStore = create((set, get) => ({ ? { ...c, trimEnd: c.trimEnd + durationToRemove, - name: c.name + " (left)", + name: getClipNameWithSuffix(c.name, "left"), } : c ), @@ -408,7 +423,7 @@ export const useTimelineStore = create((set, get) => ({ ...c, startTime: splitTime, trimStart: c.trimStart + relativeTime, - name: c.name + " (right)", + name: getClipNameWithSuffix(c.name, "right"), } : c ), @@ -458,7 +473,7 @@ export const useTimelineStore = create((set, get) => ({ { ...clip, id: audioClipId, - name: clip.name + " (audio)", + name: getClipNameWithSuffix(clip.name, "audio"), }, ], } -- cgit v1.3.1 From 0bdbd7e2b3feb698ba35b03ec834dd924d4e21d3 Mon Sep 17 00:00:00 2001 From: DevloperAmanSingh Date: Fri, 27 Jun 2025 00:14:29 +0530 Subject: refactor: streamline audio track handling by consolidating clip addition logic in timeline store --- apps/web/src/stores/timeline-store.ts | 58 +++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 26 deletions(-) (limited to 'apps/web/src/stores') diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index dd97879f..04b2f6fb 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -443,16 +443,42 @@ export const useTimelineStore = create((set, get) => ({ get().pushHistory(); - // Find or create an audio track - let audioTrackId = tracks.find((t) => t.type === "audio")?.id; + // Find existing audio track or prepare to create one + const existingAudioTrack = tracks.find((t) => t.type === "audio"); + const audioClipId = crypto.randomUUID(); - if (!audioTrackId) { - audioTrackId = crypto.randomUUID(); + if (existingAudioTrack) { + // Add audio clip to existing audio track + set((state) => ({ + tracks: state.tracks.map((track) => + track.id === existingAudioTrack.id + ? { + ...track, + clips: [ + ...track.clips, + { + ...clip, + id: audioClipId, + name: getClipNameWithSuffix(clip.name, "audio"), + }, + ], + } + : track + ), + })); + } else { + // Create new audio track with the audio clip in a single atomic update const newAudioTrack: TimelineTrack = { - id: audioTrackId, + id: crypto.randomUUID(), name: "Audio Track", type: "audio", - clips: [], + clips: [ + { + ...clip, + id: audioClipId, + name: getClipNameWithSuffix(clip.name, "audio"), + }, + ], muted: false, }; @@ -461,26 +487,6 @@ export const useTimelineStore = create((set, get) => ({ })); } - const audioClipId = crypto.randomUUID(); - - set((state) => ({ - tracks: state.tracks.map((track) => - track.id === audioTrackId - ? { - ...track, - clips: [ - ...track.clips, - { - ...clip, - id: audioClipId, - name: getClipNameWithSuffix(clip.name, "audio"), - }, - ], - } - : track - ), - })); - return audioClipId; }, -- cgit v1.3.1