summaryrefslogtreecommitdiff
path: root/apps/web/src/stores/timeline-store.ts
diff options
context:
space:
mode:
authorHyteq <DerpsDer.es@gmail.com>2025-07-20 11:00:22 +0300
committerHyteq <DerpsDer.es@gmail.com>2025-07-20 11:00:22 +0300
commitbe1ae20d9089e81cb84a8294be76fe680a24b60f (patch)
tree48472c1122d35f45f802bbd1c3cf2c15f653f9cf /apps/web/src/stores/timeline-store.ts
parentede08dac416b47eac2db151969c164bdf0f06002 (diff)
downloadopencut-be1ae20d9089e81cb84a8294be76fe680a24b60f.tar.gz
format
Diffstat (limited to 'apps/web/src/stores/timeline-store.ts')
-rw-r--r--apps/web/src/stores/timeline-store.ts2438
1 files changed, 1220 insertions, 1218 deletions
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts
index 1fcfc60e..6e48f7cd 100644
--- a/apps/web/src/stores/timeline-store.ts
+++ b/apps/web/src/stores/timeline-store.ts
@@ -1,20 +1,20 @@
import { create } from "zustand";
import {
- TrackType,
- TimelineElement,
- CreateTimelineElement,
- TimelineTrack,
- TextElement,
- DragData,
- sortTracksByOrder,
- ensureMainTrack,
- validateElementTrackCompatibility,
+ TrackType,
+ TimelineElement,
+ CreateTimelineElement,
+ TimelineTrack,
+ TextElement,
+ DragData,
+ sortTracksByOrder,
+ ensureMainTrack,
+ validateElementTrackCompatibility,
} from "@/types/timeline";
import { useEditorStore } from "./editor-store";
import {
- useMediaStore,
- getMediaAspectRatio,
- type MediaItem,
+ useMediaStore,
+ getMediaAspectRatio,
+ type MediaItem,
} from "./media-store";
import { storageService } from "@/lib/storage/storage-service";
import { useProjectStore } from "./project-store";
@@ -25,1394 +25,1396 @@ import { checkElementOverlaps, resolveElementOverlaps } from "@/lib/timeline";
// Helper function to manage element naming with suffixes
const getElementNameWithSuffix = (
- originalName: string,
- suffix: string
+ originalName: string,
+ suffix: string,
): string => {
- // Remove existing suffixes to prevent accumulation
- const baseName = originalName
- .replace(/ \(left\)$/, "")
- .replace(/ \(right\)$/, "")
- .replace(/ \(audio\)$/, "")
- .replace(/ \(split \d+\)$/, "");
+ // Remove existing suffixes to prevent accumulation
+ const baseName = originalName
+ .replace(/ \(left\)$/, "")
+ .replace(/ \(right\)$/, "")
+ .replace(/ \(audio\)$/, "")
+ .replace(/ \(split \d+\)$/, "");
- return `${baseName} (${suffix})`;
+ return `${baseName} (${suffix})`;
};
interface TimelineStore {
- // Private track storage
- _tracks: TimelineTrack[];
- history: TimelineTrack[][];
- redoStack: TimelineTrack[][];
+ // Private track storage
+ _tracks: TimelineTrack[];
+ history: TimelineTrack[][];
+ redoStack: TimelineTrack[][];
- // Always returns properly ordered tracks with main track ensured
- tracks: TimelineTrack[];
+ // Always returns properly ordered tracks with main track ensured
+ tracks: TimelineTrack[];
- // Manual method if you need to force recomputation
- getSortedTracks: () => TimelineTrack[];
+ // Manual method if you need to force recomputation
+ getSortedTracks: () => TimelineTrack[];
- // Snapping settings
- snappingEnabled: boolean;
+ // Snapping settings
+ snappingEnabled: boolean;
- // Snapping actions
- toggleSnapping: () => void;
+ // Snapping actions
+ toggleSnapping: () => void;
- // Ripple editing mode
- rippleEditingEnabled: boolean;
- toggleRippleEditing: () => void;
+ // Ripple editing mode
+ rippleEditingEnabled: boolean;
+ toggleRippleEditing: () => void;
- // Multi-selection
- selectedElements: { trackId: string; elementId: string }[];
- selectElement: (trackId: string, elementId: string, multi?: boolean) => void;
- deselectElement: (trackId: string, elementId: string) => void;
- clearSelectedElements: () => void;
- setSelectedElements: (
- elements: { trackId: string; elementId: string }[]
- ) => void;
+ // Multi-selection
+ selectedElements: { trackId: string; elementId: string }[];
+ selectElement: (trackId: string, elementId: string, multi?: boolean) => void;
+ deselectElement: (trackId: string, elementId: string) => void;
+ clearSelectedElements: () => void;
+ setSelectedElements: (
+ elements: { trackId: string; elementId: string }[],
+ ) => void;
- // Drag state
- dragState: {
- isDragging: boolean;
- elementId: string | null;
- trackId: string | null;
- startMouseX: number;
- startElementTime: number;
- clickOffsetTime: number;
- currentTime: number;
- };
- setDragState: (dragState: Partial<TimelineStore["dragState"]>) => void;
- startDrag: (
- elementId: string,
- trackId: string,
- startMouseX: number,
- startElementTime: number,
- clickOffsetTime: number
- ) => void;
- updateDragTime: (currentTime: number) => void;
- endDrag: () => void;
+ // Drag state
+ dragState: {
+ isDragging: boolean;
+ elementId: string | null;
+ trackId: string | null;
+ startMouseX: number;
+ startElementTime: number;
+ clickOffsetTime: number;
+ currentTime: number;
+ };
+ setDragState: (dragState: Partial<TimelineStore["dragState"]>) => void;
+ startDrag: (
+ elementId: string,
+ trackId: string,
+ startMouseX: number,
+ startElementTime: number,
+ clickOffsetTime: number,
+ ) => void;
+ updateDragTime: (currentTime: number) => void;
+ endDrag: () => void;
- // Actions
- addTrack: (type: TrackType) => string;
- insertTrackAt: (type: TrackType, index: number) => string;
- removeTrack: (trackId: string) => void;
- removeTrackWithRipple: (trackId: string) => void;
- addElementToTrack: (trackId: string, element: CreateTimelineElement) => void;
- removeElementFromTrack: (trackId: string, elementId: string) => void;
- moveElementToTrack: (
- fromTrackId: string,
- toTrackId: string,
- elementId: string
- ) => void;
- updateElementTrim: (
- trackId: string,
- elementId: string,
- trimStart: number,
- trimEnd: number
- ) => void;
- updateElementDuration: (
- trackId: string,
- elementId: string,
- duration: number
- ) => void;
- updateElementStartTime: (
- trackId: string,
- elementId: string,
- startTime: number
- ) => void;
- toggleTrackMute: (trackId: string) => void;
+ // Actions
+ addTrack: (type: TrackType) => string;
+ insertTrackAt: (type: TrackType, index: number) => string;
+ removeTrack: (trackId: string) => void;
+ removeTrackWithRipple: (trackId: string) => void;
+ addElementToTrack: (trackId: string, element: CreateTimelineElement) => void;
+ removeElementFromTrack: (trackId: string, elementId: string) => void;
+ moveElementToTrack: (
+ fromTrackId: string,
+ toTrackId: string,
+ elementId: string,
+ ) => void;
+ updateElementTrim: (
+ trackId: string,
+ elementId: string,
+ trimStart: number,
+ trimEnd: number,
+ ) => void;
+ updateElementDuration: (
+ trackId: string,
+ elementId: string,
+ duration: number,
+ ) => void;
+ updateElementStartTime: (
+ trackId: string,
+ elementId: string,
+ startTime: number,
+ ) => void;
+ toggleTrackMute: (trackId: string) => void;
- // Split operations for elements
- splitElement: (
- trackId: string,
- elementId: string,
- splitTime: number
- ) => string | null;
- splitAndKeepLeft: (
- trackId: string,
- elementId: string,
- splitTime: number
- ) => void;
- splitAndKeepRight: (
- trackId: string,
- elementId: string,
- splitTime: number
- ) => void;
- separateAudio: (trackId: string, elementId: string) => string | null;
+ // Split operations for elements
+ splitElement: (
+ trackId: string,
+ elementId: string,
+ splitTime: number,
+ ) => string | null;
+ splitAndKeepLeft: (
+ trackId: string,
+ elementId: string,
+ splitTime: number,
+ ) => void;
+ splitAndKeepRight: (
+ trackId: string,
+ elementId: string,
+ splitTime: number,
+ ) => void;
+ separateAudio: (trackId: string, elementId: string) => string | null;
- // Replace media for an element
- replaceElementMedia: (
- trackId: string,
- elementId: string,
- newFile: File
- ) => Promise<boolean>;
+ // Replace media for an element
+ replaceElementMedia: (
+ trackId: string,
+ elementId: string,
+ newFile: File,
+ ) => Promise<boolean>;
- // Ripple editing functions
- updateElementStartTimeWithRipple: (
- trackId: string,
- elementId: string,
- newStartTime: number
- ) => void;
- removeElementFromTrackWithRipple: (
- trackId: string,
- elementId: string
- ) => void;
+ // Ripple editing functions
+ updateElementStartTimeWithRipple: (
+ trackId: string,
+ elementId: string,
+ newStartTime: number,
+ ) => void;
+ removeElementFromTrackWithRipple: (
+ trackId: string,
+ elementId: string,
+ ) => void;
- // Computed values
- getTotalDuration: () => number;
+ // Computed values
+ getTotalDuration: () => number;
- // History actions
- undo: () => void;
- redo: () => void;
- pushHistory: () => void;
+ // History actions
+ undo: () => void;
+ redo: () => void;
+ pushHistory: () => void;
- // Persistence actions
- loadProjectTimeline: (projectId: string) => Promise<void>;
- saveProjectTimeline: (projectId: string) => Promise<void>;
- clearTimeline: () => void;
- updateTextElement: (
- trackId: string,
- elementId: string,
- updates: Partial<
- Pick<
- TextElement,
- | "content"
- | "fontSize"
- | "fontFamily"
- | "color"
- | "backgroundColor"
- | "textAlign"
- | "fontWeight"
- | "fontStyle"
- | "textDecoration"
- | "x"
- | "y"
- | "rotation"
- | "opacity"
- >
- >
- ) => void;
- checkElementOverlap: (
- trackId: string,
- startTime: number,
- duration: number,
- excludeElementId?: string
- ) => boolean;
- findOrCreateTrack: (trackType: TrackType) => string;
- addMediaAtTime: (item: MediaItem, currentTime?: number) => boolean;
- addTextAtTime: (item: TextElement, currentTime?: number) => boolean;
- addMediaToNewTrack: (item: MediaItem) => boolean;
- addTextToNewTrack: (item: TextElement | DragData) => boolean;
+ // Persistence actions
+ loadProjectTimeline: (projectId: string) => Promise<void>;
+ saveProjectTimeline: (projectId: string) => Promise<void>;
+ clearTimeline: () => void;
+ updateTextElement: (
+ trackId: string,
+ elementId: string,
+ updates: Partial<
+ Pick<
+ TextElement,
+ | "content"
+ | "fontSize"
+ | "fontFamily"
+ | "color"
+ | "backgroundColor"
+ | "textAlign"
+ | "fontWeight"
+ | "fontStyle"
+ | "textDecoration"
+ | "x"
+ | "y"
+ | "rotation"
+ | "opacity"
+ >
+ >,
+ ) => void;
+ checkElementOverlap: (
+ trackId: string,
+ startTime: number,
+ duration: number,
+ excludeElementId?: string,
+ ) => boolean;
+ findOrCreateTrack: (trackType: TrackType) => string;
+ addMediaAtTime: (item: MediaItem, currentTime?: number) => boolean;
+ addTextAtTime: (item: TextElement, currentTime?: number) => boolean;
+ addMediaToNewTrack: (item: MediaItem) => boolean;
+ addTextToNewTrack: (item: TextElement | DragData) => boolean;
}
export const useTimelineStore = create<TimelineStore>((set, get) => {
- // Helper to update tracks and maintain ordering
- const updateTracks = (newTracks: TimelineTrack[]) => {
- const tracksWithMain = ensureMainTrack(newTracks);
- const sortedTracks = sortTracksByOrder(tracksWithMain);
- set({
- _tracks: tracksWithMain,
- tracks: sortedTracks,
- });
- };
+ // Helper to update tracks and maintain ordering
+ const updateTracks = (newTracks: TimelineTrack[]) => {
+ const tracksWithMain = ensureMainTrack(newTracks);
+ const sortedTracks = sortTracksByOrder(tracksWithMain);
+ set({
+ _tracks: tracksWithMain,
+ tracks: sortedTracks,
+ });
+ };
- // Helper to auto-save timeline changes
- const autoSaveTimeline = async () => {
- const activeProject = useProjectStore.getState().activeProject;
- if (activeProject) {
- try {
- await storageService.saveTimeline(activeProject.id, get()._tracks);
- } catch (error) {
- console.error("Failed to auto-save timeline:", error);
- }
- }
- };
+ // Helper to auto-save timeline changes
+ const autoSaveTimeline = async () => {
+ const activeProject = useProjectStore.getState().activeProject;
+ if (activeProject) {
+ try {
+ await storageService.saveTimeline(activeProject.id, get()._tracks);
+ } catch (error) {
+ console.error("Failed to auto-save timeline:", error);
+ }
+ }
+ };
- // Helper to update tracks and auto-save
- const updateTracksAndSave = (newTracks: TimelineTrack[]) => {
- updateTracks(newTracks);
- // Auto-save in background
- setTimeout(autoSaveTimeline, 100);
- };
+ // Helper to update tracks and auto-save
+ const updateTracksAndSave = (newTracks: TimelineTrack[]) => {
+ updateTracks(newTracks);
+ // Auto-save in background
+ setTimeout(autoSaveTimeline, 100);
+ };
- // Initialize with proper track ordering
- const initialTracks = ensureMainTrack([]);
- const sortedInitialTracks = sortTracksByOrder(initialTracks);
+ // Initialize with proper track ordering
+ const initialTracks = ensureMainTrack([]);
+ const sortedInitialTracks = sortTracksByOrder(initialTracks);
- return {
- _tracks: initialTracks,
- tracks: sortedInitialTracks,
- history: [],
- redoStack: [],
- selectedElements: [],
- rippleEditingEnabled: false,
+ return {
+ _tracks: initialTracks,
+ tracks: sortedInitialTracks,
+ history: [],
+ redoStack: [],
+ selectedElements: [],
+ rippleEditingEnabled: false,
- // Snapping settings defaults
- snappingEnabled: true,
+ // Snapping settings defaults
+ snappingEnabled: true,
- getSortedTracks: () => {
- const { _tracks } = get();
- const tracksWithMain = ensureMainTrack(_tracks);
- return sortTracksByOrder(tracksWithMain);
- },
+ getSortedTracks: () => {
+ const { _tracks } = get();
+ const tracksWithMain = ensureMainTrack(_tracks);
+ return sortTracksByOrder(tracksWithMain);
+ },
- pushHistory: () => {
- const { _tracks, history } = get();
- set({
- history: [...history, JSON.parse(JSON.stringify(_tracks))],
- redoStack: [],
- });
- },
+ pushHistory: () => {
+ const { _tracks, history } = get();
+ set({
+ history: [...history, JSON.parse(JSON.stringify(_tracks))],
+ redoStack: [],
+ });
+ },
- undo: () => {
- const { history, redoStack, _tracks } = get();
- if (history.length === 0) return;
- const prev = history[history.length - 1];
- updateTracksAndSave(prev);
- set({
- history: history.slice(0, -1),
- redoStack: [...redoStack, JSON.parse(JSON.stringify(_tracks))],
- });
- },
+ undo: () => {
+ const { history, redoStack, _tracks } = get();
+ if (history.length === 0) return;
+ const prev = history[history.length - 1];
+ updateTracksAndSave(prev);
+ set({
+ history: history.slice(0, -1),
+ redoStack: [...redoStack, JSON.parse(JSON.stringify(_tracks))],
+ });
+ },
- selectElement: (trackId, elementId, multi = false) => {
- set((state) => {
- const exists = state.selectedElements.some(
- (c) => c.trackId === trackId && c.elementId === elementId
- );
- if (multi) {
- return exists
- ? {
- selectedElements: state.selectedElements.filter(
- (c) => !(c.trackId === trackId && c.elementId === elementId)
- ),
- }
- : {
- selectedElements: [
- ...state.selectedElements,
- { trackId, elementId },
- ],
- };
- } else {
- return { selectedElements: [{ trackId, elementId }] };
- }
- });
- },
+ selectElement: (trackId, elementId, multi = false) => {
+ set((state) => {
+ const exists = state.selectedElements.some(
+ (c) => c.trackId === trackId && c.elementId === elementId,
+ );
+ if (multi) {
+ return exists
+ ? {
+ selectedElements: state.selectedElements.filter(
+ (c) => !(c.trackId === trackId && c.elementId === elementId),
+ ),
+ }
+ : {
+ selectedElements: [
+ ...state.selectedElements,
+ { trackId, elementId },
+ ],
+ };
+ } else {
+ return { selectedElements: [{ trackId, elementId }] };
+ }
+ });
+ },
- deselectElement: (trackId, elementId) => {
- set((state) => ({
- selectedElements: state.selectedElements.filter(
- (c) => !(c.trackId === trackId && c.elementId === elementId)
- ),
- }));
- },
+ deselectElement: (trackId, elementId) => {
+ set((state) => ({
+ selectedElements: state.selectedElements.filter(
+ (c) => !(c.trackId === trackId && c.elementId === elementId),
+ ),
+ }));
+ },
- clearSelectedElements: () => {
- set({ selectedElements: [] });
- },
+ clearSelectedElements: () => {
+ set({ selectedElements: [] });
+ },
- setSelectedElements: (elements) => set({ selectedElements: elements }),
+ setSelectedElements: (elements) => set({ selectedElements: elements }),
- addTrack: (type) => {
- get().pushHistory();
+ addTrack: (type) => {
+ get().pushHistory();
- // Generate proper track name based on type
- const trackName =
- type === "media"
- ? "Media Track"
- : type === "text"
- ? "Text Track"
- : type === "audio"
- ? "Audio Track"
- : "Track";
+ // Generate proper track name based on type
+ const trackName =
+ type === "media"
+ ? "Media Track"
+ : type === "text"
+ ? "Text Track"
+ : type === "audio"
+ ? "Audio Track"
+ : "Track";
- const newTrack: TimelineTrack = {
- id: generateUUID(),
- name: trackName,
- type,
- elements: [],
- muted: false,
- };
+ const newTrack: TimelineTrack = {
+ id: generateUUID(),
+ name: trackName,
+ type,
+ elements: [],
+ muted: false,
+ };
- updateTracksAndSave([...get()._tracks, newTrack]);
- return newTrack.id;
- },
+ updateTracksAndSave([...get()._tracks, newTrack]);
+ return newTrack.id;
+ },
- insertTrackAt: (type, index) => {
- get().pushHistory();
+ insertTrackAt: (type, index) => {
+ get().pushHistory();
- // Generate proper track name based on type
- const trackName =
- type === "media"
- ? "Media Track"
- : type === "text"
- ? "Text Track"
- : type === "audio"
- ? "Audio Track"
- : "Track";
+ // Generate proper track name based on type
+ const trackName =
+ type === "media"
+ ? "Media Track"
+ : type === "text"
+ ? "Text Track"
+ : type === "audio"
+ ? "Audio Track"
+ : "Track";
- const newTrack: TimelineTrack = {
- id: generateUUID(),
- name: trackName,
- type,
- elements: [],
- muted: false,
- };
+ const newTrack: TimelineTrack = {
+ id: generateUUID(),
+ name: trackName,
+ type,
+ elements: [],
+ muted: false,
+ };
- const newTracks = [...get()._tracks];
- newTracks.splice(index, 0, newTrack);
- updateTracksAndSave(newTracks);
- return newTrack.id;
- },
+ const newTracks = [...get()._tracks];
+ newTracks.splice(index, 0, newTrack);
+ updateTracksAndSave(newTracks);
+ return newTrack.id;
+ },
- removeTrack: (trackId) => {
- const { rippleEditingEnabled } = get();
+ removeTrack: (trackId) => {
+ const { rippleEditingEnabled } = get();
- if (rippleEditingEnabled) {
- get().removeTrackWithRipple(trackId);
- } else {
- get().pushHistory();
- updateTracksAndSave(
- get()._tracks.filter((track) => track.id !== trackId)
- );
- }
- },
+ if (rippleEditingEnabled) {
+ get().removeTrackWithRipple(trackId);
+ } else {
+ get().pushHistory();
+ updateTracksAndSave(
+ get()._tracks.filter((track) => track.id !== trackId),
+ );
+ }
+ },
- removeTrackWithRipple: (trackId) => {
- const { _tracks } = get();
- const trackToRemove = _tracks.find((t) => t.id === trackId);
+ removeTrackWithRipple: (trackId) => {
+ const { _tracks } = get();
+ const trackToRemove = _tracks.find((t) => t.id === trackId);
- if (!trackToRemove) return;
+ if (!trackToRemove) return;
- get().pushHistory();
+ get().pushHistory();
- // If track has no elements, just remove it normally
- if (trackToRemove.elements.length === 0) {
- updateTracksAndSave(_tracks.filter((track) => track.id !== trackId));
- return;
- }
+ // If track has no elements, just remove it normally
+ if (trackToRemove.elements.length === 0) {
+ updateTracksAndSave(_tracks.filter((track) => track.id !== trackId));
+ return;
+ }
- // Find all the time ranges occupied by elements in the track being removed
- const occupiedRanges = trackToRemove.elements.map((element) => ({
- startTime: element.startTime,
- endTime:
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd),
- }));
+ // Find all the time ranges occupied by elements in the track being removed
+ const occupiedRanges = trackToRemove.elements.map((element) => ({
+ startTime: element.startTime,
+ endTime:
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd),
+ }));
- // Sort ranges by start time
- occupiedRanges.sort((a, b) => a.startTime - b.startTime);
+ // Sort ranges by start time
+ occupiedRanges.sort((a, b) => a.startTime - b.startTime);
- // Merge overlapping ranges to get consolidated gaps
- const mergedRanges: Array<{
- startTime: number;
- endTime: number;
- duration: number;
- }> = [];
+ // Merge overlapping ranges to get consolidated gaps
+ const mergedRanges: Array<{
+ startTime: number;
+ endTime: number;
+ duration: number;
+ }> = [];
- for (const range of occupiedRanges) {
- if (mergedRanges.length === 0) {
- mergedRanges.push({
- startTime: range.startTime,
- endTime: range.endTime,
- duration: range.endTime - range.startTime,
- });
- } else {
- const lastRange = mergedRanges[mergedRanges.length - 1];
- if (range.startTime <= lastRange.endTime) {
- // Overlapping or adjacent ranges, merge them
- lastRange.endTime = Math.max(lastRange.endTime, range.endTime);
- lastRange.duration = lastRange.endTime - lastRange.startTime;
- } else {
- // Non-overlapping range, add as new
- mergedRanges.push({
- startTime: range.startTime,
- endTime: range.endTime,
- duration: range.endTime - range.startTime,
- });
- }
- }
- }
+ for (const range of occupiedRanges) {
+ if (mergedRanges.length === 0) {
+ mergedRanges.push({
+ startTime: range.startTime,
+ endTime: range.endTime,
+ duration: range.endTime - range.startTime,
+ });
+ } else {
+ const lastRange = mergedRanges[mergedRanges.length - 1];
+ if (range.startTime <= lastRange.endTime) {
+ // Overlapping or adjacent ranges, merge them
+ lastRange.endTime = Math.max(lastRange.endTime, range.endTime);
+ lastRange.duration = lastRange.endTime - lastRange.startTime;
+ } else {
+ // Non-overlapping range, add as new
+ mergedRanges.push({
+ startTime: range.startTime,
+ endTime: range.endTime,
+ duration: range.endTime - range.startTime,
+ });
+ }
+ }
+ }
- // Remove the track and apply ripple effects to remaining tracks
- const updatedTracks = _tracks
- .filter((track) => track.id !== trackId)
- .map((track) => {
- const updatedElements = track.elements.map((element) => {
- let newStartTime = element.startTime;
+ // Remove the track and apply ripple effects to remaining tracks
+ const updatedTracks = _tracks
+ .filter((track) => track.id !== trackId)
+ .map((track) => {
+ const updatedElements = track.elements.map((element) => {
+ let newStartTime = element.startTime;
- // Process gaps from right to left (latest to earliest) to avoid cumulative shifts
- for (let i = mergedRanges.length - 1; i >= 0; i--) {
- const gap = mergedRanges[i];
- // If this element starts after the gap, shift it left by the gap duration
- if (newStartTime >= gap.endTime) {
- newStartTime -= gap.duration;
- }
- }
+ // Process gaps from right to left (latest to earliest) to avoid cumulative shifts
+ for (let i = mergedRanges.length - 1; i >= 0; i--) {
+ const gap = mergedRanges[i];
+ // If this element starts after the gap, shift it left by the gap duration
+ if (newStartTime >= gap.endTime) {
+ newStartTime -= gap.duration;
+ }
+ }
- return {
- ...element,
- startTime: Math.max(0, newStartTime),
- };
- });
+ return {
+ ...element,
+ startTime: Math.max(0, newStartTime),
+ };
+ });
- // Check for overlaps and resolve them if necessary
- const hasOverlaps = checkElementOverlaps(updatedElements);
- if (hasOverlaps) {
- const resolvedElements = resolveElementOverlaps(updatedElements);
- return { ...track, elements: resolvedElements };
- }
+ // Check for overlaps and resolve them if necessary
+ const hasOverlaps = checkElementOverlaps(updatedElements);
+ if (hasOverlaps) {
+ const resolvedElements = resolveElementOverlaps(updatedElements);
+ return { ...track, elements: resolvedElements };
+ }
- return { ...track, elements: updatedElements };
- });
+ return { ...track, elements: updatedElements };
+ });
- updateTracksAndSave(updatedTracks);
- },
+ updateTracksAndSave(updatedTracks);
+ },
- addElementToTrack: (trackId, elementData) => {
- get().pushHistory();
+ addElementToTrack: (trackId, elementData) => {
+ get().pushHistory();
- // Validate element type matches track type
- const track = get()._tracks.find((t) => t.id === trackId);
- if (!track) {
- console.error("Track not found:", trackId);
- return;
- }
+ // Validate element type matches track type
+ const track = get()._tracks.find((t) => t.id === trackId);
+ if (!track) {
+ console.error("Track not found:", trackId);
+ return;
+ }
- // Use utility function for validation
- const validation = validateElementTrackCompatibility(elementData, track);
- if (!validation.isValid) {
- console.error(validation.errorMessage);
- return;
- }
+ // Use utility function for validation
+ const validation = validateElementTrackCompatibility(elementData, track);
+ if (!validation.isValid) {
+ console.error(validation.errorMessage);
+ return;
+ }
- // For media elements, validate mediaId exists
- if (elementData.type === "media" && !elementData.mediaId) {
- console.error("Media element must have mediaId");
- return;
- }
+ // For media elements, validate mediaId exists
+ if (elementData.type === "media" && !elementData.mediaId) {
+ console.error("Media element must have mediaId");
+ return;
+ }
- // For text elements, validate required text properties
- if (elementData.type === "text" && !elementData.content) {
- console.error("Text element must have content");
- return;
- }
+ // For text elements, validate required text properties
+ if (elementData.type === "text" && !elementData.content) {
+ console.error("Text element must have content");
+ return;
+ }
- // Check if this is the first element being added to the timeline
- const currentState = get();
- const totalElementsInTimeline = currentState._tracks.reduce(
- (total, track) => total + track.elements.length,
- 0
- );
- const isFirstElement = totalElementsInTimeline === 0;
+ // Check if this is the first element being added to the timeline
+ const currentState = get();
+ const totalElementsInTimeline = currentState._tracks.reduce(
+ (total, track) => total + track.elements.length,
+ 0,
+ );
+ const isFirstElement = totalElementsInTimeline === 0;
- const newElement: TimelineElement = {
- ...elementData,
- id: generateUUID(),
- startTime: elementData.startTime || 0,
- trimStart: 0,
- trimEnd: 0,
- } as TimelineElement; // Type assertion since we trust the caller passes valid data
+ const newElement: TimelineElement = {
+ ...elementData,
+ id: generateUUID(),
+ startTime: elementData.startTime || 0,
+ trimStart: 0,
+ trimEnd: 0,
+ } as TimelineElement; // Type assertion since we trust the caller passes valid data
- // If this is the first element and it's a media element, automatically set the project canvas size
- // to match the media's aspect ratio and FPS (for videos)
- if (isFirstElement && newElement.type === "media") {
- const mediaStore = useMediaStore.getState();
- const mediaItem = mediaStore.mediaItems.find(
- (item) => item.id === newElement.mediaId
- );
+ // If this is the first element and it's a media element, automatically set the project canvas size
+ // to match the media's aspect ratio and FPS (for videos)
+ if (isFirstElement && newElement.type === "media") {
+ const mediaStore = useMediaStore.getState();
+ const mediaItem = mediaStore.mediaItems.find(
+ (item) => item.id === newElement.mediaId,
+ );
- if (
- mediaItem &&
- (mediaItem.type === "image" || mediaItem.type === "video")
- ) {
- const editorStore = useEditorStore.getState();
- editorStore.setCanvasSizeFromAspectRatio(
- getMediaAspectRatio(mediaItem)
- );
- }
+ if (
+ mediaItem &&
+ (mediaItem.type === "image" || mediaItem.type === "video")
+ ) {
+ const editorStore = useEditorStore.getState();
+ editorStore.setCanvasSizeFromAspectRatio(
+ getMediaAspectRatio(mediaItem),
+ );
+ }
- // Set project FPS from the first video element
- if (mediaItem && mediaItem.type === "video" && mediaItem.fps) {
- const projectStore = useProjectStore.getState();
- if (projectStore.activeProject) {
- projectStore.updateProjectFps(mediaItem.fps);
- }
- }
- }
+ // Set project FPS from the first video element
+ if (mediaItem && mediaItem.type === "video" && mediaItem.fps) {
+ const projectStore = useProjectStore.getState();
+ if (projectStore.activeProject) {
+ projectStore.updateProjectFps(mediaItem.fps);
+ }
+ }
+ }
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? { ...track, elements: [...track.elements, newElement] }
- : track
- )
- );
- },
+ updateTracksAndSave(
+ get()._tracks.map((track) =>
+ track.id === trackId
+ ? { ...track, elements: [...track.elements, newElement] }
+ : track,
+ ),
+ );
+ },
- removeElementFromTrack: (trackId, elementId) => {
- const { rippleEditingEnabled } = get();
+ removeElementFromTrack: (trackId, elementId) => {
+ const { rippleEditingEnabled } = get();
- if (rippleEditingEnabled) {
- get().removeElementFromTrackWithRipple(trackId, elementId);
- } else {
- get().pushHistory();
- updateTracksAndSave(
- get()
- ._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.filter(
- (element) => element.id !== elementId
- ),
- }
- : track
- )
- .filter((track) => track.elements.length > 0)
- );
- }
- },
+ if (rippleEditingEnabled) {
+ get().removeElementFromTrackWithRipple(trackId, elementId);
+ } else {
+ get().pushHistory();
+ updateTracksAndSave(
+ get()
+ ._tracks.map((track) =>
+ track.id === trackId
+ ? {
+ ...track,
+ elements: track.elements.filter(
+ (element) => element.id !== elementId,
+ ),
+ }
+ : track,
+ )
+ .filter((track) => track.elements.length > 0),
+ );
+ }
+ },
- removeElementFromTrackWithRipple: (trackId, elementId) => {
- const { _tracks, rippleEditingEnabled } = get();
+ removeElementFromTrackWithRipple: (trackId, elementId) => {
+ const { _tracks, rippleEditingEnabled } = get();
- if (!rippleEditingEnabled) {
- // If ripple editing is disabled, use regular removal
- get().removeElementFromTrack(trackId, elementId);
- return;
- }
+ if (!rippleEditingEnabled) {
+ // If ripple editing is disabled, use regular removal
+ get().removeElementFromTrack(trackId, elementId);
+ return;
+ }
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
+ const track = _tracks.find((t) => t.id === trackId);
+ const element = track?.elements.find((e) => e.id === elementId);
- if (!element || !track) return;
+ if (!element || !track) return;
- get().pushHistory();
+ get().pushHistory();
- const elementStartTime = element.startTime;
- const elementDuration =
- element.duration - element.trimStart - element.trimEnd;
- const elementEndTime = elementStartTime + elementDuration;
+ const elementStartTime = element.startTime;
+ const elementDuration =
+ element.duration - element.trimStart - element.trimEnd;
+ const elementEndTime = elementStartTime + elementDuration;
- // Remove the element and shift all elements that come after it
- const updatedTracks = _tracks
- .map((currentTrack) => {
- // Only apply ripple effects to the same track unless multi-track ripple is enabled
- const shouldApplyRipple = currentTrack.id === trackId;
+ // Remove the element and shift all elements that come after it
+ const updatedTracks = _tracks
+ .map((currentTrack) => {
+ // Only apply ripple effects to the same track unless multi-track ripple is enabled
+ const shouldApplyRipple = currentTrack.id === trackId;
- const updatedElements = currentTrack.elements
- .filter((currentElement) => {
- // Remove the target element
- if (
- currentElement.id === elementId &&
- currentTrack.id === trackId
- ) {
- return false;
- }
- return true;
- })
- .map((currentElement) => {
- // Only apply ripple effects if we should process this track
- if (!shouldApplyRipple) {
- return currentElement;
- }
+ const updatedElements = currentTrack.elements
+ .filter((currentElement) => {
+ // Remove the target element
+ if (
+ currentElement.id === elementId &&
+ currentTrack.id === trackId
+ ) {
+ return false;
+ }
+ return true;
+ })
+ .map((currentElement) => {
+ // Only apply ripple effects if we should process this track
+ if (!shouldApplyRipple) {
+ return currentElement;
+ }
- // Shift elements that start after the removed element
- if (currentElement.startTime >= elementEndTime) {
- return {
- ...currentElement,
- startTime: Math.max(
- 0,
- currentElement.startTime - elementDuration
- ),
- };
- }
- return currentElement;
- });
+ // Shift elements that start after the removed element
+ if (currentElement.startTime >= elementEndTime) {
+ return {
+ ...currentElement,
+ startTime: Math.max(
+ 0,
+ currentElement.startTime - elementDuration,
+ ),
+ };
+ }
+ return currentElement;
+ });
- // Check for overlaps and resolve them if necessary
- const hasOverlaps = checkElementOverlaps(updatedElements);
- if (hasOverlaps) {
- // Resolve overlaps by adjusting element positions
- const resolvedElements = resolveElementOverlaps(updatedElements);
- return { ...currentTrack, elements: resolvedElements };
- }
+ // Check for overlaps and resolve them if necessary
+ const hasOverlaps = checkElementOverlaps(updatedElements);
+ if (hasOverlaps) {
+ // Resolve overlaps by adjusting element positions
+ const resolvedElements = resolveElementOverlaps(updatedElements);
+ return { ...currentTrack, elements: resolvedElements };
+ }
- return { ...currentTrack, elements: updatedElements };
- })
- .filter((track) => track.elements.length > 0 || track.isMain);
+ return { ...currentTrack, elements: updatedElements };
+ })
+ .filter((track) => track.elements.length > 0 || track.isMain);
- updateTracksAndSave(updatedTracks);
- },
+ updateTracksAndSave(updatedTracks);
+ },
- moveElementToTrack: (fromTrackId, toTrackId, elementId) => {
- get().pushHistory();
+ moveElementToTrack: (fromTrackId, toTrackId, elementId) => {
+ get().pushHistory();
- const fromTrack = get()._tracks.find((track) => track.id === fromTrackId);
- const toTrack = get()._tracks.find((track) => track.id === toTrackId);
- const elementToMove = fromTrack?.elements.find(
- (element) => element.id === elementId
- );
+ const fromTrack = get()._tracks.find((track) => track.id === fromTrackId);
+ const toTrack = get()._tracks.find((track) => track.id === toTrackId);
+ const elementToMove = fromTrack?.elements.find(
+ (element) => element.id === elementId,
+ );
- if (!elementToMove || !toTrack) return;
+ if (!elementToMove || !toTrack) return;
- // Validate element type compatibility with target track
- const validation = validateElementTrackCompatibility(
- elementToMove,
- toTrack
- );
- if (!validation.isValid) {
- console.error(validation.errorMessage);
- return;
- }
+ // Validate element type compatibility with target track
+ const validation = validateElementTrackCompatibility(
+ elementToMove,
+ toTrack,
+ );
+ if (!validation.isValid) {
+ console.error(validation.errorMessage);
+ return;
+ }
- const newTracks = get()
- ._tracks.map((track) => {
- if (track.id === fromTrackId) {
- return {
- ...track,
- elements: track.elements.filter(
- (element) => element.id !== elementId
- ),
- };
- } else if (track.id === toTrackId) {
- return {
- ...track,
- elements: [...track.elements, elementToMove],
- };
- }
- return track;
- })
- .filter((track) => track.elements.length > 0);
+ const newTracks = get()
+ ._tracks.map((track) => {
+ if (track.id === fromTrackId) {
+ return {
+ ...track,
+ elements: track.elements.filter(
+ (element) => element.id !== elementId,
+ ),
+ };
+ } else if (track.id === toTrackId) {
+ return {
+ ...track,
+ elements: [...track.elements, elementToMove],
+ };
+ }
+ return track;
+ })
+ .filter((track) => track.elements.length > 0);
- updateTracksAndSave(newTracks);
- },
+ updateTracksAndSave(newTracks);
+ },
- updateElementTrim: (trackId, elementId, trimStart, trimEnd) => {
- get().pushHistory();
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((element) =>
- element.id === elementId
- ? { ...element, trimStart, trimEnd }
- : element
- ),
- }
- : track
- )
- );
- },
+ updateElementTrim: (trackId, elementId, trimStart, trimEnd) => {
+ get().pushHistory();
+ updateTracksAndSave(
+ get()._tracks.map((track) =>
+ track.id === trackId
+ ? {
+ ...track,
+ elements: track.elements.map((element) =>
+ element.id === elementId
+ ? { ...element, trimStart, trimEnd }
+ : element,
+ ),
+ }
+ : track,
+ ),
+ );
+ },
- updateElementDuration: (trackId, elementId, duration) => {
- get().pushHistory();
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((element) =>
- element.id === elementId ? { ...element, duration } : element
- ),
- }
- : track
- )
- );
- },
+ updateElementDuration: (trackId, elementId, duration) => {
+ get().pushHistory();
+ updateTracksAndSave(
+ get()._tracks.map((track) =>
+ track.id === trackId
+ ? {
+ ...track,
+ elements: track.elements.map((element) =>
+ element.id === elementId ? { ...element, duration } : element,
+ ),
+ }
+ : track,
+ ),
+ );
+ },
- updateElementStartTime: (trackId, elementId, startTime) => {
- get().pushHistory();
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((element) =>
- element.id === elementId ? { ...element, startTime } : element
- ),
- }
- : track
- )
- );
- },
+ updateElementStartTime: (trackId, elementId, startTime) => {
+ get().pushHistory();
+ updateTracksAndSave(
+ get()._tracks.map((track) =>
+ track.id === trackId
+ ? {
+ ...track,
+ elements: track.elements.map((element) =>
+ element.id === elementId
+ ? { ...element, startTime }
+ : element,
+ ),
+ }
+ : track,
+ ),
+ );
+ },
- updateElementStartTimeWithRipple: (trackId, elementId, newStartTime) => {
- const { _tracks, rippleEditingEnabled } = get();
+ updateElementStartTimeWithRipple: (trackId, elementId, newStartTime) => {
+ const { _tracks, rippleEditingEnabled } = get();
- if (!rippleEditingEnabled) {
- // If ripple editing is disabled, use regular update
- get().updateElementStartTime(trackId, elementId, newStartTime);
- return;
- }
+ if (!rippleEditingEnabled) {
+ // If ripple editing is disabled, use regular update
+ get().updateElementStartTime(trackId, elementId, newStartTime);
+ return;
+ }
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
+ const track = _tracks.find((t) => t.id === trackId);
+ const element = track?.elements.find((e) => e.id === elementId);
- if (!element || !track) return;
+ if (!element || !track) return;
- get().pushHistory();
+ get().pushHistory();
- const oldStartTime = element.startTime;
- const oldEndTime =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
- const newEndTime =
- newStartTime + (element.duration - element.trimStart - element.trimEnd);
- const timeDelta = newStartTime - oldStartTime;
+ const oldStartTime = element.startTime;
+ const oldEndTime =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd);
+ const newEndTime =
+ newStartTime + (element.duration - element.trimStart - element.trimEnd);
+ const timeDelta = newStartTime - oldStartTime;
- // Update tracks based on multi-track ripple setting
- const updatedTracks = _tracks.map((currentTrack) => {
- // Only apply ripple effects to the same track unless multi-track ripple is enabled
- const shouldApplyRipple = currentTrack.id === trackId;
+ // Update tracks based on multi-track ripple setting
+ const updatedTracks = _tracks.map((currentTrack) => {
+ // Only apply ripple effects to the same track unless multi-track ripple is enabled
+ const shouldApplyRipple = currentTrack.id === trackId;
- const updatedElements = currentTrack.elements.map((currentElement) => {
- if (currentElement.id === elementId && currentTrack.id === trackId) {
- // Update the moved element
- return { ...currentElement, startTime: newStartTime };
- }
+ const updatedElements = currentTrack.elements.map((currentElement) => {
+ if (currentElement.id === elementId && currentTrack.id === trackId) {
+ // Update the moved element
+ return { ...currentElement, startTime: newStartTime };
+ }
- // Only apply ripple effects if we should process this track
- if (!shouldApplyRipple) {
- return currentElement;
- }
+ // Only apply ripple effects if we should process this track
+ if (!shouldApplyRipple) {
+ return currentElement;
+ }
- // For ripple editing, we need to move elements that come after the moved element
- const currentElementStart = currentElement.startTime;
- const currentElementEnd =
- currentElement.startTime +
- (currentElement.duration -
- currentElement.trimStart -
- currentElement.trimEnd);
+ // For ripple editing, we need to move elements that come after the moved element
+ const currentElementStart = currentElement.startTime;
+ const currentElementEnd =
+ currentElement.startTime +
+ (currentElement.duration -
+ currentElement.trimStart -
+ currentElement.trimEnd);
- // If moving element to the right (positive delta)
- if (timeDelta > 0) {
- // Move elements that start after the original position of the moved element
- if (currentElementStart >= oldEndTime) {
- return {
- ...currentElement,
- startTime: currentElementStart + timeDelta,
- };
- }
- }
- // If moving element to the left (negative delta)
- else if (timeDelta < 0) {
- // Move elements that start after the new position of the moved element
- if (
- currentElementStart >= newEndTime &&
- currentElementStart >= oldStartTime
- ) {
- return {
- ...currentElement,
- startTime: Math.max(0, currentElementStart + timeDelta),
- };
- }
- }
+ // If moving element to the right (positive delta)
+ if (timeDelta > 0) {
+ // Move elements that start after the original position of the moved element
+ if (currentElementStart >= oldEndTime) {
+ return {
+ ...currentElement,
+ startTime: currentElementStart + timeDelta,
+ };
+ }
+ }
+ // If moving element to the left (negative delta)
+ else if (timeDelta < 0) {
+ // Move elements that start after the new position of the moved element
+ if (
+ currentElementStart >= newEndTime &&
+ currentElementStart >= oldStartTime
+ ) {
+ return {
+ ...currentElement,
+ startTime: Math.max(0, currentElementStart + timeDelta),
+ };
+ }
+ }
- return currentElement;
- });
+ return currentElement;
+ });
- // Check for overlaps and resolve them if necessary
- const hasOverlaps = checkElementOverlaps(updatedElements);
- if (hasOverlaps) {
- // Resolve overlaps by adjusting element positions
- const resolvedElements = resolveElementOverlaps(updatedElements);
- return { ...currentTrack, elements: resolvedElements };
- }
+ // Check for overlaps and resolve them if necessary
+ const hasOverlaps = checkElementOverlaps(updatedElements);
+ if (hasOverlaps) {
+ // Resolve overlaps by adjusting element positions
+ const resolvedElements = resolveElementOverlaps(updatedElements);
+ return { ...currentTrack, elements: resolvedElements };
+ }
- return { ...currentTrack, elements: updatedElements };
- });
+ return { ...currentTrack, elements: updatedElements };
+ });
- updateTracksAndSave(updatedTracks);
- },
+ updateTracksAndSave(updatedTracks);
+ },
- toggleTrackMute: (trackId) => {
- get().pushHistory();
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId ? { ...track, muted: !track.muted } : track
- )
- );
- },
+ toggleTrackMute: (trackId) => {
+ get().pushHistory();
+ updateTracksAndSave(
+ get()._tracks.map((track) =>
+ track.id === trackId ? { ...track, muted: !track.muted } : track,
+ ),
+ );
+ },
- updateTextElement: (trackId, elementId, updates) => {
- get().pushHistory();
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((element) =>
- element.id === elementId && element.type === "text"
- ? { ...element, ...updates }
- : element
- ),
- }
- : track
- )
- );
- },
+ updateTextElement: (trackId, elementId, updates) => {
+ get().pushHistory();
+ updateTracksAndSave(
+ get()._tracks.map((track) =>
+ track.id === trackId
+ ? {
+ ...track,
+ elements: track.elements.map((element) =>
+ element.id === elementId && element.type === "text"
+ ? { ...element, ...updates }
+ : element,
+ ),
+ }
+ : track,
+ ),
+ );
+ },
- splitElement: (trackId, elementId, splitTime) => {
- const { _tracks } = get();
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
+ splitElement: (trackId, elementId, splitTime) => {
+ const { _tracks } = get();
+ const track = _tracks.find((t) => t.id === trackId);
+ const element = track?.elements.find((c) => c.id === elementId);
- if (!element) return null;
+ if (!element) return null;
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
+ const effectiveStart = element.startTime;
+ const effectiveEnd =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd);
- if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return null;
+ if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return null;
- get().pushHistory();
+ get().pushHistory();
- const relativeTime = splitTime - element.startTime;
- const firstDuration = relativeTime;
- const secondDuration =
- element.duration - element.trimStart - element.trimEnd - relativeTime;
+ const relativeTime = splitTime - element.startTime;
+ const firstDuration = relativeTime;
+ const secondDuration =
+ element.duration - element.trimStart - element.trimEnd - relativeTime;
- const secondElementId = generateUUID();
+ const secondElementId = generateUUID();
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.flatMap((c) =>
- c.id === elementId
- ? [
- {
- ...c,
- trimEnd: c.trimEnd + secondDuration,
- name: getElementNameWithSuffix(c.name, "left"),
- },
- {
- ...c,
- id: secondElementId,
- startTime: splitTime,
- trimStart: c.trimStart + firstDuration,
- name: getElementNameWithSuffix(c.name, "right"),
- },
- ]
- : [c]
- ),
- }
- : track
- )
- );
+ updateTracksAndSave(
+ get()._tracks.map((track) =>
+ track.id === trackId
+ ? {
+ ...track,
+ elements: track.elements.flatMap((c) =>
+ c.id === elementId
+ ? [
+ {
+ ...c,
+ trimEnd: c.trimEnd + secondDuration,
+ name: getElementNameWithSuffix(c.name, "left"),
+ },
+ {
+ ...c,
+ id: secondElementId,
+ startTime: splitTime,
+ trimStart: c.trimStart + firstDuration,
+ name: getElementNameWithSuffix(c.name, "right"),
+ },
+ ]
+ : [c],
+ ),
+ }
+ : track,
+ ),
+ );
- return secondElementId;
- },
+ return secondElementId;
+ },
- // Split element and keep only the left portion
- splitAndKeepLeft: (trackId, elementId, splitTime) => {
- const { _tracks } = get();
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
+ // Split element and keep only the left portion
+ splitAndKeepLeft: (trackId, elementId, splitTime) => {
+ const { _tracks } = get();
+ const track = _tracks.find((t) => t.id === trackId);
+ const element = track?.elements.find((c) => c.id === elementId);
- if (!element) return;
+ if (!element) return;
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
+ const effectiveStart = element.startTime;
+ const effectiveEnd =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd);
- if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return;
+ if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return;
- get().pushHistory();
+ get().pushHistory();
- const relativeTime = splitTime - element.startTime;
- const durationToRemove =
- element.duration - element.trimStart - element.trimEnd - relativeTime;
+ const relativeTime = splitTime - element.startTime;
+ const durationToRemove =
+ element.duration - element.trimStart - element.trimEnd - relativeTime;
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((c) =>
- c.id === elementId
- ? {
- ...c,
- trimEnd: c.trimEnd + durationToRemove,
- name: getElementNameWithSuffix(c.name, "left"),
- }
- : c
- ),
- }
- : track
- )
- );
- },
+ updateTracksAndSave(
+ get()._tracks.map((track) =>
+ track.id === trackId
+ ? {
+ ...track,
+ elements: track.elements.map((c) =>
+ c.id === elementId
+ ? {
+ ...c,
+ trimEnd: c.trimEnd + durationToRemove,
+ name: getElementNameWithSuffix(c.name, "left"),
+ }
+ : c,
+ ),
+ }
+ : track,
+ ),
+ );
+ },
- // Split element and keep only the right portion
- splitAndKeepRight: (trackId, elementId, splitTime) => {
- const { _tracks } = get();
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
+ // Split element and keep only the right portion
+ splitAndKeepRight: (trackId, elementId, splitTime) => {
+ const { _tracks } = get();
+ const track = _tracks.find((t) => t.id === trackId);
+ const element = track?.elements.find((c) => c.id === elementId);
- if (!element) return;
+ if (!element) return;
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
+ const effectiveStart = element.startTime;
+ const effectiveEnd =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd);
- if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return;
+ if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return;
- get().pushHistory();
+ get().pushHistory();
- const relativeTime = splitTime - element.startTime;
+ const relativeTime = splitTime - element.startTime;
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((c) =>
- c.id === elementId
- ? {
- ...c,
- startTime: splitTime,
- trimStart: c.trimStart + relativeTime,
- name: getElementNameWithSuffix(c.name, "right"),
- }
- : c
- ),
- }
- : track
- )
- );
- },
+ updateTracksAndSave(
+ get()._tracks.map((track) =>
+ track.id === trackId
+ ? {
+ ...track,
+ elements: track.elements.map((c) =>
+ c.id === elementId
+ ? {
+ ...c,
+ startTime: splitTime,
+ trimStart: c.trimStart + relativeTime,
+ name: getElementNameWithSuffix(c.name, "right"),
+ }
+ : c,
+ ),
+ }
+ : track,
+ ),
+ );
+ },
- // Extract audio from video element to an audio track
- separateAudio: (trackId, elementId) => {
- const { _tracks } = get();
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
+ // Extract audio from video element to an audio track
+ separateAudio: (trackId, elementId) => {
+ const { _tracks } = get();
+ const track = _tracks.find((t) => t.id === trackId);
+ const element = track?.elements.find((c) => c.id === elementId);
- if (!element || track?.type !== "media") return null;
+ if (!element || track?.type !== "media") return null;
- get().pushHistory();
+ get().pushHistory();
- // Find existing audio track or prepare to create one
- const existingAudioTrack = _tracks.find((t) => t.type === "audio");
- const audioElementId = generateUUID();
+ // Find existing audio track or prepare to create one
+ const existingAudioTrack = _tracks.find((t) => t.type === "audio");
+ const audioElementId = generateUUID();
- if (existingAudioTrack) {
- // Add audio element to existing audio track
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === existingAudioTrack.id
- ? {
- ...track,
- elements: [
- ...track.elements,
- {
- ...element,
- id: audioElementId,
- name: getElementNameWithSuffix(element.name, "audio"),
- },
- ],
- }
- : track
- )
- );
- } else {
- // Create new audio track with the audio element in a single atomic update
- const newAudioTrack: TimelineTrack = {
- id: generateUUID(),
- name: "Audio Track",
- type: "audio",
- elements: [
- {
- ...element,
- id: audioElementId,
- name: getElementNameWithSuffix(element.name, "audio"),
- },
- ],
- muted: false,
- };
+ if (existingAudioTrack) {
+ // Add audio element to existing audio track
+ updateTracksAndSave(
+ get()._tracks.map((track) =>
+ track.id === existingAudioTrack.id
+ ? {
+ ...track,
+ elements: [
+ ...track.elements,
+ {
+ ...element,
+ id: audioElementId,
+ name: getElementNameWithSuffix(element.name, "audio"),
+ },
+ ],
+ }
+ : track,
+ ),
+ );
+ } else {
+ // Create new audio track with the audio element in a single atomic update
+ const newAudioTrack: TimelineTrack = {
+ id: generateUUID(),
+ name: "Audio Track",
+ type: "audio",
+ elements: [
+ {
+ ...element,
+ id: audioElementId,
+ name: getElementNameWithSuffix(element.name, "audio"),
+ },
+ ],
+ muted: false,
+ };
- updateTracksAndSave([...get()._tracks, newAudioTrack]);
- }
+ updateTracksAndSave([...get()._tracks, newAudioTrack]);
+ }
- return audioElementId;
- },
+ return audioElementId;
+ },
- // Replace media for an element
- replaceElementMedia: async (trackId, elementId, newFile) => {
- const { _tracks } = get();
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
+ // Replace media for an element
+ replaceElementMedia: async (trackId, elementId, newFile) => {
+ const { _tracks } = get();
+ const track = _tracks.find((t) => t.id === trackId);
+ const element = track?.elements.find((c) => c.id === elementId);
- if (!element || element.type !== "media") return false;
+ if (!element || element.type !== "media") return false;
- try {
- const mediaStore = useMediaStore.getState();
- const projectStore = useProjectStore.getState();
+ try {
+ const mediaStore = useMediaStore.getState();
+ const projectStore = useProjectStore.getState();
- if (!projectStore.activeProject) return false;
+ if (!projectStore.activeProject) return false;
- // Import required media processing functions
- const {
- getFileType,
- getImageDimensions,
- generateVideoThumbnail,
- getMediaDuration,
- } = await import("./media-store");
+ // Import required media processing functions
+ const {
+ getFileType,
+ getImageDimensions,
+ generateVideoThumbnail,
+ getMediaDuration,
+ } = await import("./media-store");
- const fileType = getFileType(newFile);
- if (!fileType) return false;
+ const fileType = getFileType(newFile);
+ if (!fileType) return false;
- // Process the new media file
- let mediaData: any = {
- name: newFile.name,
- type: fileType,
- file: newFile,
- url: URL.createObjectURL(newFile),
- };
+ // Process the new media file
+ let mediaData: any = {
+ name: newFile.name,
+ type: fileType,
+ file: newFile,
+ url: URL.createObjectURL(newFile),
+ };
- // Get media-specific metadata
- if (fileType === "image") {
- const { width, height } = await getImageDimensions(newFile);
- mediaData.width = width;
- mediaData.height = height;
- } else if (fileType === "video") {
- const [duration, { thumbnailUrl, width, height }] = await Promise.all(
- [getMediaDuration(newFile), generateVideoThumbnail(newFile)]
- );
- mediaData.duration = duration;
- mediaData.thumbnailUrl = thumbnailUrl;
- mediaData.width = width;
- mediaData.height = height;
- } else if (fileType === "audio") {
- mediaData.duration = await getMediaDuration(newFile);
- }
+ // Get media-specific metadata
+ if (fileType === "image") {
+ const { width, height } = await getImageDimensions(newFile);
+ mediaData.width = width;
+ mediaData.height = height;
+ } else if (fileType === "video") {
+ const [duration, { thumbnailUrl, width, height }] = await Promise.all(
+ [getMediaDuration(newFile), generateVideoThumbnail(newFile)],
+ );
+ mediaData.duration = duration;
+ mediaData.thumbnailUrl = thumbnailUrl;
+ mediaData.width = width;
+ mediaData.height = height;
+ } else if (fileType === "audio") {
+ mediaData.duration = await getMediaDuration(newFile);
+ }
- // Add new media item to store
- await mediaStore.addMediaItem(projectStore.activeProject.id, mediaData);
+ // Add new media item to store
+ await mediaStore.addMediaItem(projectStore.activeProject.id, mediaData);
- // Find the newly created media item
- const newMediaItem = mediaStore.mediaItems.find(
- (item) => item.file === newFile
- );
+ // Find the newly created media item
+ const newMediaItem = mediaStore.mediaItems.find(
+ (item) => item.file === newFile,
+ );
- if (!newMediaItem) return false;
+ if (!newMediaItem) return false;
- get().pushHistory();
+ get().pushHistory();
- // Update the timeline element to reference the new media
- updateTracksAndSave(
- _tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((c) =>
- c.id === elementId
- ? {
- ...c,
- mediaId: newMediaItem.id,
- name: newMediaItem.name,
- // Update duration if the new media has a different duration
- duration: newMediaItem.duration || c.duration,
- }
- : c
- ),
- }
- : track
- )
- );
+ // Update the timeline element to reference the new media
+ updateTracksAndSave(
+ _tracks.map((track) =>
+ track.id === trackId
+ ? {
+ ...track,
+ elements: track.elements.map((c) =>
+ c.id === elementId
+ ? {
+ ...c,
+ mediaId: newMediaItem.id,
+ name: newMediaItem.name,
+ // Update duration if the new media has a different duration
+ duration: newMediaItem.duration || c.duration,
+ }
+ : c,
+ ),
+ }
+ : track,
+ ),
+ );
- return true;
- } catch (error) {
- console.log(
- JSON.stringify({
- error: "Failed to replace element media",
- details: error,
- })
- );
- return false;
- }
- },
+ return true;
+ } catch (error) {
+ console.log(
+ JSON.stringify({
+ error: "Failed to replace element media",
+ details: error,
+ }),
+ );
+ return false;
+ }
+ },
- getTotalDuration: () => {
- const { _tracks } = get();
- if (_tracks.length === 0) return 0;
+ getTotalDuration: () => {
+ const { _tracks } = get();
+ if (_tracks.length === 0) return 0;
- const trackEndTimes = _tracks.map((track) =>
- track.elements.reduce((maxEnd, element) => {
- const elementEnd =
- element.startTime +
- element.duration -
- element.trimStart -
- element.trimEnd;
- return Math.max(maxEnd, elementEnd);
- }, 0)
- );
+ const trackEndTimes = _tracks.map((track) =>
+ track.elements.reduce((maxEnd, element) => {
+ const elementEnd =
+ element.startTime +
+ element.duration -
+ element.trimStart -
+ element.trimEnd;
+ return Math.max(maxEnd, elementEnd);
+ }, 0),
+ );
- return Math.max(...trackEndTimes, 0);
- },
+ return Math.max(...trackEndTimes, 0);
+ },
- redo: () => {
- const { redoStack } = get();
- if (redoStack.length === 0) return;
- const next = redoStack[redoStack.length - 1];
- updateTracksAndSave(next);
- set({ redoStack: redoStack.slice(0, -1) });
- },
+ redo: () => {
+ const { redoStack } = get();
+ if (redoStack.length === 0) return;
+ const next = redoStack[redoStack.length - 1];
+ updateTracksAndSave(next);
+ set({ redoStack: redoStack.slice(0, -1) });
+ },
- dragState: {
- isDragging: false,
- elementId: null,
- trackId: null,
- startMouseX: 0,
- startElementTime: 0,
- clickOffsetTime: 0,
- currentTime: 0,
- },
+ dragState: {
+ isDragging: false,
+ elementId: null,
+ trackId: null,
+ startMouseX: 0,
+ startElementTime: 0,
+ clickOffsetTime: 0,
+ currentTime: 0,
+ },
- setDragState: (dragState) =>
- set((state) => ({
- dragState: { ...state.dragState, ...dragState },
- })),
+ setDragState: (dragState) =>
+ set((state) => ({
+ dragState: { ...state.dragState, ...dragState },
+ })),
- startDrag: (
- elementId,
- trackId,
- startMouseX,
- startElementTime,
- clickOffsetTime
- ) => {
- set({
- dragState: {
- isDragging: true,
- elementId,
- trackId,
- startMouseX,
- startElementTime,
- clickOffsetTime,
- currentTime: startElementTime,
- },
- });
- },
+ startDrag: (
+ elementId,
+ trackId,
+ startMouseX,
+ startElementTime,
+ clickOffsetTime,
+ ) => {
+ set({
+ dragState: {
+ isDragging: true,
+ elementId,
+ trackId,
+ startMouseX,
+ startElementTime,
+ clickOffsetTime,
+ currentTime: startElementTime,
+ },
+ });
+ },
- updateDragTime: (currentTime) => {
- set((state) => ({
- dragState: {
- ...state.dragState,
- currentTime,
- },
- }));
- },
+ updateDragTime: (currentTime) => {
+ set((state) => ({
+ dragState: {
+ ...state.dragState,
+ currentTime,
+ },
+ }));
+ },
- endDrag: () => {
- set({
- dragState: {
- isDragging: false,
- elementId: null,
- trackId: null,
- startMouseX: 0,
- startElementTime: 0,
- clickOffsetTime: 0,
- currentTime: 0,
- },
- });
- },
+ endDrag: () => {
+ set({
+ dragState: {
+ isDragging: false,
+ elementId: null,
+ trackId: null,
+ startMouseX: 0,
+ startElementTime: 0,
+ clickOffsetTime: 0,
+ currentTime: 0,
+ },
+ });
+ },
- // Persistence methods
- loadProjectTimeline: async (projectId) => {
- try {
- const tracks = await storageService.loadTimeline(projectId);
- if (tracks) {
- updateTracks(tracks);
- } else {
- // No timeline saved yet, initialize with default
- const defaultTracks = ensureMainTrack([]);
- updateTracks(defaultTracks);
- }
- // Clear history when loading a project
- set({ history: [], redoStack: [] });
- } catch (error) {
- console.error("Failed to load timeline:", error);
- // Initialize with default on error
- const defaultTracks = ensureMainTrack([]);
- updateTracks(defaultTracks);
- set({ history: [], redoStack: [] });
- }
- },
+ // Persistence methods
+ loadProjectTimeline: async (projectId) => {
+ try {
+ const tracks = await storageService.loadTimeline(projectId);
+ if (tracks) {
+ updateTracks(tracks);
+ } else {
+ // No timeline saved yet, initialize with default
+ const defaultTracks = ensureMainTrack([]);
+ updateTracks(defaultTracks);
+ }
+ // Clear history when loading a project
+ set({ history: [], redoStack: [] });
+ } catch (error) {
+ console.error("Failed to load timeline:", error);
+ // Initialize with default on error
+ const defaultTracks = ensureMainTrack([]);
+ updateTracks(defaultTracks);
+ set({ history: [], redoStack: [] });
+ }
+ },
- saveProjectTimeline: async (projectId) => {
- try {
- await storageService.saveTimeline(projectId, get()._tracks);
- } catch (error) {
- console.error("Failed to save timeline:", error);
- }
- },
+ saveProjectTimeline: async (projectId) => {
+ try {
+ await storageService.saveTimeline(projectId, get()._tracks);
+ } catch (error) {
+ console.error("Failed to save timeline:", error);
+ }
+ },
- clearTimeline: () => {
- const defaultTracks = ensureMainTrack([]);
- updateTracks(defaultTracks);
- set({ history: [], redoStack: [], selectedElements: [] });
- },
+ clearTimeline: () => {
+ const defaultTracks = ensureMainTrack([]);
+ updateTracks(defaultTracks);
+ set({ history: [], redoStack: [], selectedElements: [] });
+ },
- // Snapping actions
- toggleSnapping: () => {
- set((state) => ({ snappingEnabled: !state.snappingEnabled }));
- },
+ // Snapping actions
+ toggleSnapping: () => {
+ set((state) => ({ snappingEnabled: !state.snappingEnabled }));
+ },
- // Ripple editing functions
- toggleRippleEditing: () => {
- set((state) => ({
- rippleEditingEnabled: !state.rippleEditingEnabled,
- }));
- },
+ // Ripple editing functions
+ toggleRippleEditing: () => {
+ set((state) => ({
+ rippleEditingEnabled: !state.rippleEditingEnabled,
+ }));
+ },
- checkElementOverlap: (trackId, startTime, duration, excludeElementId) => {
- const track = get()._tracks.find((t) => t.id === trackId);
- if (!track) return false;
+ checkElementOverlap: (trackId, startTime, duration, excludeElementId) => {
+ const track = get()._tracks.find((t) => t.id === trackId);
+ if (!track) return false;
- const overlap = track.elements.some((element) => {
- const elementEnd =
- element.startTime +
- element.duration -
- element.trimStart -
- element.trimEnd;
+ const overlap = track.elements.some((element) => {
+ const elementEnd =
+ element.startTime +
+ element.duration -
+ element.trimStart -
+ element.trimEnd;
- if (element.id === excludeElementId) {
- return false;
- }
+ if (element.id === excludeElementId) {
+ return false;
+ }
- return (
- (startTime >= element.startTime && startTime < elementEnd) ||
- (startTime + duration > element.startTime &&
- startTime + duration <= elementEnd) ||
- (startTime < element.startTime && startTime + duration > elementEnd)
- );
- });
- return overlap;
- },
+ return (
+ (startTime >= element.startTime && startTime < elementEnd) ||
+ (startTime + duration > element.startTime &&
+ startTime + duration <= elementEnd) ||
+ (startTime < element.startTime && startTime + duration > elementEnd)
+ );
+ });
+ return overlap;
+ },
- findOrCreateTrack: (trackType) => {
- // Always create new text track to allow multiple text elements
- if (trackType === "text") {
- return get().addTrack(trackType);
- }
+ findOrCreateTrack: (trackType) => {
+ // Always create new text track to allow multiple text elements
+ if (trackType === "text") {
+ return get().addTrack(trackType);
+ }
- const existingTrack = get()._tracks.find((t) => t.type === trackType);
- if (existingTrack) {
- return existingTrack.id;
- }
+ const existingTrack = get()._tracks.find((t) => t.type === trackType);
+ if (existingTrack) {
+ return existingTrack.id;
+ }
- return get().addTrack(trackType);
- },
+ return get().addTrack(trackType);
+ },
- addMediaAtTime: (item, currentTime = 0) => {
- const trackType = item.type === "audio" ? "audio" : "media";
- const targetTrackId = get().findOrCreateTrack(trackType);
+ addMediaAtTime: (item, currentTime = 0) => {
+ const trackType = item.type === "audio" ? "audio" : "media";
+ const targetTrackId = get().findOrCreateTrack(trackType);
- const duration =
- item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION;
+ const duration =
+ item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION;
- if (get().checkElementOverlap(targetTrackId, currentTime, duration)) {
- toast.error(
- "Cannot place element here - it would overlap with existing elements"
- );
- return false;
- }
+ if (get().checkElementOverlap(targetTrackId, currentTime, duration)) {
+ toast.error(
+ "Cannot place element here - it would overlap with existing elements",
+ );
+ return false;
+ }
- get().addElementToTrack(targetTrackId, {
- type: "media",
- mediaId: item.id,
- name: item.name,
- duration,
- startTime: currentTime,
- trimStart: 0,
- trimEnd: 0,
- });
- return true;
- },
+ get().addElementToTrack(targetTrackId, {
+ type: "media",
+ mediaId: item.id,
+ name: item.name,
+ duration,
+ startTime: currentTime,
+ trimStart: 0,
+ trimEnd: 0,
+ });
+ return true;
+ },
- addTextAtTime: (item, currentTime = 0) => {
- const targetTrackId = get().addTrack("text"); // Always create new text track to allow multiple text elements
+ addTextAtTime: (item, currentTime = 0) => {
+ const targetTrackId = get().addTrack("text"); // Always create new text track to allow multiple text elements
- get().addElementToTrack(targetTrackId, {
- type: "text",
- name: item.name || "Text",
- content: item.content || "Default Text",
- duration: item.duration || TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
- startTime: currentTime,
- trimStart: 0,
- trimEnd: 0,
- fontSize: item.fontSize || 48,
- fontFamily: item.fontFamily || "Arial",
- color: item.color || "#ffffff",
- backgroundColor: item.backgroundColor || "transparent",
- textAlign: item.textAlign || "center",
- fontWeight: item.fontWeight || "normal",
- fontStyle: item.fontStyle || "normal",
- textDecoration: item.textDecoration || "none",
- x: item.x || 0,
- y: item.y || 0,
- rotation: item.rotation || 0,
- opacity: item.opacity !== undefined ? item.opacity : 1,
- });
- return true;
- },
+ get().addElementToTrack(targetTrackId, {
+ type: "text",
+ name: item.name || "Text",
+ content: item.content || "Default Text",
+ duration: item.duration || TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
+ startTime: currentTime,
+ trimStart: 0,
+ trimEnd: 0,
+ fontSize: item.fontSize || 48,
+ fontFamily: item.fontFamily || "Arial",
+ color: item.color || "#ffffff",
+ backgroundColor: item.backgroundColor || "transparent",
+ textAlign: item.textAlign || "center",
+ fontWeight: item.fontWeight || "normal",
+ fontStyle: item.fontStyle || "normal",
+ textDecoration: item.textDecoration || "none",
+ x: item.x || 0,
+ y: item.y || 0,
+ rotation: item.rotation || 0,
+ opacity: item.opacity !== undefined ? item.opacity : 1,
+ });
+ return true;
+ },
- addMediaToNewTrack: (item) => {
- const trackType = item.type === "audio" ? "audio" : "media";
- const targetTrackId = get().findOrCreateTrack(trackType);
+ addMediaToNewTrack: (item) => {
+ const trackType = item.type === "audio" ? "audio" : "media";
+ const targetTrackId = get().findOrCreateTrack(trackType);
- get().addElementToTrack(targetTrackId, {
- type: "media",
- mediaId: item.id,
- name: item.name,
- duration: item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION,
- startTime: 0,
- trimStart: 0,
- trimEnd: 0,
- });
- return true;
- },
+ get().addElementToTrack(targetTrackId, {
+ type: "media",
+ mediaId: item.id,
+ name: item.name,
+ duration: item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+ });
+ return true;
+ },
- addTextToNewTrack: (item) => {
- const targetTrackId = get().addTrack("text"); // Always create new text track to allow multiple text elements
+ addTextToNewTrack: (item) => {
+ const targetTrackId = get().addTrack("text"); // Always create new text track to allow multiple text elements
- get().addElementToTrack(targetTrackId, {
- type: "text",
- name: item.name || "Text",
- content:
- ("content" in item ? item.content : "Default Text") || "Default Text",
- duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
- startTime: 0,
- trimStart: 0,
- trimEnd: 0,
- fontSize: ("fontSize" in item ? item.fontSize : 48) || 48,
- fontFamily:
- ("fontFamily" in item ? item.fontFamily : "Arial") || "Arial",
- color: ("color" in item ? item.color : "#ffffff") || "#ffffff",
- backgroundColor:
- ("backgroundColor" in item ? item.backgroundColor : "transparent") ||
- "transparent",
- textAlign:
- ("textAlign" in item ? item.textAlign : "center") || "center",
- fontWeight:
- ("fontWeight" in item ? item.fontWeight : "normal") || "normal",
- fontStyle:
- ("fontStyle" in item ? item.fontStyle : "normal") || "normal",
- textDecoration:
- ("textDecoration" in item ? item.textDecoration : "none") || "none",
- x: ("x" in item ? item.x : 0) || 0,
- y: ("y" in item ? item.y : 0) || 0,
- rotation: ("rotation" in item ? item.rotation : 0) || 0,
- opacity:
- "opacity" in item && item.opacity !== undefined ? item.opacity : 1,
- });
- return true;
- },
- };
+ get().addElementToTrack(targetTrackId, {
+ type: "text",
+ name: item.name || "Text",
+ content:
+ ("content" in item ? item.content : "Default Text") || "Default Text",
+ duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+ fontSize: ("fontSize" in item ? item.fontSize : 48) || 48,
+ fontFamily:
+ ("fontFamily" in item ? item.fontFamily : "Arial") || "Arial",
+ color: ("color" in item ? item.color : "#ffffff") || "#ffffff",
+ backgroundColor:
+ ("backgroundColor" in item ? item.backgroundColor : "transparent") ||
+ "transparent",
+ textAlign:
+ ("textAlign" in item ? item.textAlign : "center") || "center",
+ fontWeight:
+ ("fontWeight" in item ? item.fontWeight : "normal") || "normal",
+ fontStyle:
+ ("fontStyle" in item ? item.fontStyle : "normal") || "normal",
+ textDecoration:
+ ("textDecoration" in item ? item.textDecoration : "none") || "none",
+ x: ("x" in item ? item.x : 0) || 0,
+ y: ("y" in item ? item.y : 0) || 0,
+ rotation: ("rotation" in item ? item.rotation : 0) || 0,
+ opacity:
+ "opacity" in item && item.opacity !== undefined ? item.opacity : 1,
+ });
+ return true;
+ },
+ };
});