feat: add clip splitting and audio separation functionality to timeline store
This commit is contained in:
@ -74,10 +74,28 @@ interface TimelineStore {
|
|||||||
) => void;
|
) => void;
|
||||||
toggleTrackMute: (trackId: string) => 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
|
// Computed values
|
||||||
getTotalDuration: () => number;
|
getTotalDuration: () => number;
|
||||||
|
|
||||||
// New actions
|
// History actions
|
||||||
undo: () => void;
|
undo: () => void;
|
||||||
redo: () => void;
|
redo: () => void;
|
||||||
pushHistory: () => void;
|
pushHistory: () => void;
|
||||||
@ -91,10 +109,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => ({
|
|||||||
|
|
||||||
pushHistory: () => {
|
pushHistory: () => {
|
||||||
const { tracks, history, redoStack } = get();
|
const { tracks, history, redoStack } = get();
|
||||||
// Deep copy tracks
|
|
||||||
set({
|
set({
|
||||||
history: [...history, JSON.parse(JSON.stringify(tracks))],
|
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<TimelineStore>((set, get) => ({
|
|||||||
set({
|
set({
|
||||||
tracks: prev,
|
tracks: prev,
|
||||||
history: history.slice(0, -1),
|
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<TimelineStore>((set, get) => ({
|
|||||||
(c) => c.trackId === trackId && c.clipId === clipId
|
(c) => c.trackId === trackId && c.clipId === clipId
|
||||||
);
|
);
|
||||||
if (multi) {
|
if (multi) {
|
||||||
// Toggle selection
|
|
||||||
return exists
|
return exists
|
||||||
? {
|
? {
|
||||||
selectedClips: state.selectedClips.filter(
|
selectedClips: state.selectedClips.filter(
|
||||||
@ -128,6 +144,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
deselectClip: (trackId, clipId) => {
|
deselectClip: (trackId, clipId) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
selectedClips: state.selectedClips.filter(
|
selectedClips: state.selectedClips.filter(
|
||||||
@ -135,6 +152,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
clearSelectedClips: () => {
|
clearSelectedClips: () => {
|
||||||
set({ selectedClips: [] });
|
set({ selectedClips: [] });
|
||||||
},
|
},
|
||||||
@ -194,7 +212,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => ({
|
|||||||
}
|
}
|
||||||
: track
|
: track
|
||||||
)
|
)
|
||||||
// Remove track if it becomes empty
|
|
||||||
.filter((track) => track.clips.length > 0),
|
.filter((track) => track.clips.length > 0),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
@ -223,7 +240,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => ({
|
|||||||
}
|
}
|
||||||
return track;
|
return track;
|
||||||
})
|
})
|
||||||
// Remove track if it becomes empty
|
|
||||||
.filter((track) => track.clips.length > 0),
|
.filter((track) => track.clips.length > 0),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -270,6 +286,185 @@ export const useTimelineStore = create<TimelineStore>((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: () => {
|
getTotalDuration: () => {
|
||||||
const { tracks } = get();
|
const { tracks } = get();
|
||||||
if (tracks.length === 0) return 0;
|
if (tracks.length === 0) return 0;
|
||||||
|
Reference in New Issue
Block a user