diff options
| author | Hyteq <DerpsDer.es@gmail.com> | 2025-07-20 10:50:17 +0300 |
|---|---|---|
| committer | Hyteq <DerpsDer.es@gmail.com> | 2025-07-20 10:50:17 +0300 |
| commit | fe020be6cea00691d6ce66e21843915defa5e944 (patch) | |
| tree | 65718c8aee56a61b6c42032f8ebd96915cd13dc6 /apps/web/src/stores | |
| parent | eab8ec3ba81b86fdd4ade920d999c28fb34785b1 (diff) | |
| download | opencut-fe020be6cea00691d6ce66e21843915defa5e944.tar.gz | |
Revert "format, cleanup, deps"
This reverts commit eab8ec3ba81b86fdd4ade920d999c28fb34785b1.
Diffstat (limited to 'apps/web/src/stores')
| -rw-r--r-- | apps/web/src/stores/editor-store.ts | 210 | ||||
| -rw-r--r-- | apps/web/src/stores/media-store.ts | 438 | ||||
| -rw-r--r-- | apps/web/src/stores/panel-store.ts | 90 | ||||
| -rw-r--r-- | apps/web/src/stores/playback-store.ts | 206 | ||||
| -rw-r--r-- | apps/web/src/stores/project-store.ts | 722 | ||||
| -rw-r--r-- | apps/web/src/stores/timeline-store.ts | 1954 |
6 files changed, 1807 insertions, 1813 deletions
diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts index 2b7e6181..c24929a2 100644 --- a/apps/web/src/stores/editor-store.ts +++ b/apps/web/src/stores/editor-store.ts @@ -1,105 +1,105 @@ -import { create } from "zustand"; -import { CanvasSize, CanvasPreset } from "@/types/editor"; - -type CanvasMode = "preset" | "original" | "custom"; - -interface EditorState { - // Loading states - isInitializing: boolean; - isPanelsReady: boolean; - - // Canvas/Project settings - canvasSize: CanvasSize; - canvasMode: CanvasMode; - canvasPresets: CanvasPreset[]; - - // Actions - setInitializing: (loading: boolean) => void; - setPanelsReady: (ready: boolean) => void; - initializeApp: () => Promise<void>; - setCanvasSize: (size: CanvasSize) => void; - setCanvasSizeToOriginal: (aspectRatio: number) => void; - setCanvasSizeFromAspectRatio: (aspectRatio: number) => void; -} - -const DEFAULT_CANVAS_PRESETS: CanvasPreset[] = [ - { name: "16:9", width: 1920, height: 1080 }, - { name: "9:16", width: 1080, height: 1920 }, - { name: "1:1", width: 1080, height: 1080 }, - { name: "4:3", width: 1440, height: 1080 }, -]; - -// Helper function to find the best matching canvas preset for an aspect ratio -const findBestCanvasPreset = (aspectRatio: number): CanvasSize => { - // Calculate aspect ratio for each preset and find the closest match - let bestMatch = DEFAULT_CANVAS_PRESETS[0]; // Default to 16:9 HD - let smallestDifference = Math.abs( - aspectRatio - bestMatch.width / bestMatch.height, - ); - - for (const preset of DEFAULT_CANVAS_PRESETS) { - const presetAspectRatio = preset.width / preset.height; - const difference = Math.abs(aspectRatio - presetAspectRatio); - - if (difference < smallestDifference) { - smallestDifference = difference; - bestMatch = preset; - } - } - - // If the difference is still significant (> 0.1), create a custom size - // based on the media aspect ratio with a reasonable resolution - const bestAspectRatio = bestMatch.width / bestMatch.height; - if (Math.abs(aspectRatio - bestAspectRatio) > 0.1) { - // Create custom dimensions based on the aspect ratio - if (aspectRatio > 1) { - // Landscape - use 1920 width - return { width: 1920, height: Math.round(1920 / aspectRatio) }; - } else { - // Portrait or square - use 1080 height - return { width: Math.round(1080 * aspectRatio), height: 1080 }; - } - } - - return { width: bestMatch.width, height: bestMatch.height }; -}; - -export const useEditorStore = create<EditorState>((set, get) => ({ - // Initial states - isInitializing: true, - isPanelsReady: false, - canvasSize: { width: 1920, height: 1080 }, // Default 16:9 HD - canvasMode: "preset" as CanvasMode, - canvasPresets: DEFAULT_CANVAS_PRESETS, - - // Actions - setInitializing: (loading) => { - set({ isInitializing: loading }); - }, - - setPanelsReady: (ready) => { - set({ isPanelsReady: ready }); - }, - - initializeApp: async () => { - console.log("Initializing video editor..."); - set({ isInitializing: true, isPanelsReady: false }); - - set({ isPanelsReady: true, isInitializing: false }); - console.log("Video editor ready"); - }, - - setCanvasSize: (size) => { - set({ canvasSize: size, canvasMode: "preset" }); - }, - - setCanvasSizeToOriginal: (aspectRatio) => { - const newCanvasSize = findBestCanvasPreset(aspectRatio); - set({ canvasSize: newCanvasSize, canvasMode: "original" }); - }, - - setCanvasSizeFromAspectRatio: (aspectRatio) => { - const newCanvasSize = findBestCanvasPreset(aspectRatio); - set({ canvasSize: newCanvasSize, canvasMode: "custom" }); - }, -})); +import { create } from "zustand";
+import { CanvasSize, CanvasPreset } from "@/types/editor";
+
+type CanvasMode = "preset" | "original" | "custom";
+
+interface EditorState {
+ // Loading states
+ isInitializing: boolean;
+ isPanelsReady: boolean;
+
+ // Canvas/Project settings
+ canvasSize: CanvasSize;
+ canvasMode: CanvasMode;
+ canvasPresets: CanvasPreset[];
+
+ // Actions
+ setInitializing: (loading: boolean) => void;
+ setPanelsReady: (ready: boolean) => void;
+ initializeApp: () => Promise<void>;
+ setCanvasSize: (size: CanvasSize) => void;
+ setCanvasSizeToOriginal: (aspectRatio: number) => void;
+ setCanvasSizeFromAspectRatio: (aspectRatio: number) => void;
+}
+
+const DEFAULT_CANVAS_PRESETS: CanvasPreset[] = [
+ { name: "16:9", width: 1920, height: 1080 },
+ { name: "9:16", width: 1080, height: 1920 },
+ { name: "1:1", width: 1080, height: 1080 },
+ { name: "4:3", width: 1440, height: 1080 },
+];
+
+// Helper function to find the best matching canvas preset for an aspect ratio
+const findBestCanvasPreset = (aspectRatio: number): CanvasSize => {
+ // Calculate aspect ratio for each preset and find the closest match
+ let bestMatch = DEFAULT_CANVAS_PRESETS[0]; // Default to 16:9 HD
+ let smallestDifference = Math.abs(
+ aspectRatio - bestMatch.width / bestMatch.height
+ );
+
+ for (const preset of DEFAULT_CANVAS_PRESETS) {
+ const presetAspectRatio = preset.width / preset.height;
+ const difference = Math.abs(aspectRatio - presetAspectRatio);
+
+ if (difference < smallestDifference) {
+ smallestDifference = difference;
+ bestMatch = preset;
+ }
+ }
+
+ // If the difference is still significant (> 0.1), create a custom size
+ // based on the media aspect ratio with a reasonable resolution
+ const bestAspectRatio = bestMatch.width / bestMatch.height;
+ if (Math.abs(aspectRatio - bestAspectRatio) > 0.1) {
+ // Create custom dimensions based on the aspect ratio
+ if (aspectRatio > 1) {
+ // Landscape - use 1920 width
+ return { width: 1920, height: Math.round(1920 / aspectRatio) };
+ } else {
+ // Portrait or square - use 1080 height
+ return { width: Math.round(1080 * aspectRatio), height: 1080 };
+ }
+ }
+
+ return { width: bestMatch.width, height: bestMatch.height };
+};
+
+export const useEditorStore = create<EditorState>((set, get) => ({
+ // Initial states
+ isInitializing: true,
+ isPanelsReady: false,
+ canvasSize: { width: 1920, height: 1080 }, // Default 16:9 HD
+ canvasMode: "preset" as CanvasMode,
+ canvasPresets: DEFAULT_CANVAS_PRESETS,
+
+ // Actions
+ setInitializing: (loading) => {
+ set({ isInitializing: loading });
+ },
+
+ setPanelsReady: (ready) => {
+ set({ isPanelsReady: ready });
+ },
+
+ initializeApp: async () => {
+ console.log("Initializing video editor...");
+ set({ isInitializing: true, isPanelsReady: false });
+
+ set({ isPanelsReady: true, isInitializing: false });
+ console.log("Video editor ready");
+ },
+
+ setCanvasSize: (size) => {
+ set({ canvasSize: size, canvasMode: "preset" });
+ },
+
+ setCanvasSizeToOriginal: (aspectRatio) => {
+ const newCanvasSize = findBestCanvasPreset(aspectRatio);
+ set({ canvasSize: newCanvasSize, canvasMode: "original" });
+ },
+
+ setCanvasSizeFromAspectRatio: (aspectRatio) => {
+ const newCanvasSize = findBestCanvasPreset(aspectRatio);
+ set({ canvasSize: newCanvasSize, canvasMode: "custom" });
+ },
+}));
diff --git a/apps/web/src/stores/media-store.ts b/apps/web/src/stores/media-store.ts index 305316c8..c4ee5f3e 100644 --- a/apps/web/src/stores/media-store.ts +++ b/apps/web/src/stores/media-store.ts @@ -6,288 +6,284 @@ import { generateUUID } from "@/lib/utils"; export type MediaType = "image" | "video" | "audio"; export interface MediaItem { - id: string; - name: string; - type: MediaType; - file: File; - url?: string; // Object URL for preview - thumbnailUrl?: string; // For video thumbnails - duration?: number; // For video/audio duration - width?: number; // For video/image width - height?: number; // For video/image height - fps?: number; // For video frame rate - // Text-specific properties - content?: string; // Text content - fontSize?: number; // Font size - fontFamily?: string; // Font family - color?: string; // Text color - backgroundColor?: string; // Background color - textAlign?: "left" | "center" | "right"; // Text alignment + id: string; + name: string; + type: MediaType; + file: File; + url?: string; // Object URL for preview + thumbnailUrl?: string; // For video thumbnails + duration?: number; // For video/audio duration + width?: number; // For video/image width + height?: number; // For video/image height + fps?: number; // For video frame rate + // Text-specific properties + content?: string; // Text content + fontSize?: number; // Font size + fontFamily?: string; // Font family + color?: string; // Text color + backgroundColor?: string; // Background color + textAlign?: "left" | "center" | "right"; // Text alignment } interface MediaStore { - mediaItems: MediaItem[]; - isLoading: boolean; + mediaItems: MediaItem[]; + isLoading: boolean; - // Actions - now require projectId - addMediaItem: ( - projectId: string, - item: Omit<MediaItem, "id">, - ) => Promise<void>; - removeMediaItem: (projectId: string, id: string) => Promise<void>; - loadProjectMedia: (projectId: string) => Promise<void>; - clearProjectMedia: (projectId: string) => Promise<void>; - clearAllMedia: () => void; // Clear local state only + // Actions - now require projectId + addMediaItem: ( + projectId: string, + item: Omit<MediaItem, "id"> + ) => Promise<void>; + removeMediaItem: (projectId: string, id: string) => Promise<void>; + loadProjectMedia: (projectId: string) => Promise<void>; + clearProjectMedia: (projectId: string) => Promise<void>; + clearAllMedia: () => void; // Clear local state only } // Helper function to determine file type export const getFileType = (file: File): MediaType | null => { - const { type } = file; + const { type } = file; - if (type.startsWith("image/")) { - return "image"; - } - if (type.startsWith("video/")) { - return "video"; - } - if (type.startsWith("audio/")) { - return "audio"; - } + if (type.startsWith("image/")) { + return "image"; + } + if (type.startsWith("video/")) { + return "video"; + } + if (type.startsWith("audio/")) { + return "audio"; + } - return null; + return null; }; // Helper function to get image dimensions export const getImageDimensions = ( - file: File, + file: File ): Promise<{ width: number; height: number }> => { - return new Promise((resolve, reject) => { - const img = new window.Image(); + return new Promise((resolve, reject) => { + const img = new window.Image(); - img.addEventListener("load", () => { - const width = img.naturalWidth; - const height = img.naturalHeight; - resolve({ width, height }); - img.remove(); - }); + img.addEventListener("load", () => { + const width = img.naturalWidth; + const height = img.naturalHeight; + resolve({ width, height }); + img.remove(); + }); - img.addEventListener("error", () => { - reject(new Error("Could not load image")); - img.remove(); - }); + img.addEventListener("error", () => { + reject(new Error("Could not load image")); + img.remove(); + }); - img.src = URL.createObjectURL(file); - }); + img.src = URL.createObjectURL(file); + }); }; // Helper function to generate video thumbnail and get dimensions export const generateVideoThumbnail = ( - file: File, + file: File ): Promise<{ thumbnailUrl: string; width: number; height: number }> => { - return new Promise((resolve, reject) => { - const video = document.createElement("video") as HTMLVideoElement; - const canvas = document.createElement("canvas") as HTMLCanvasElement; - const ctx = canvas.getContext("2d"); + return new Promise((resolve, reject) => { + const video = document.createElement("video") as HTMLVideoElement; + const canvas = document.createElement("canvas") as HTMLCanvasElement; + const ctx = canvas.getContext("2d"); - if (!ctx) { - reject(new Error("Could not get canvas context")); - return; - } + if (!ctx) { + reject(new Error("Could not get canvas context")); + return; + } - video.addEventListener("loadedmetadata", () => { - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; + video.addEventListener("loadedmetadata", () => { + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; - // Seek to 1 second or 10% of duration, whichever is smaller - video.currentTime = Math.min(1, video.duration * 0.1); - }); + // Seek to 1 second or 10% of duration, whichever is smaller + video.currentTime = Math.min(1, video.duration * 0.1); + }); - video.addEventListener("seeked", () => { - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - const thumbnailUrl = canvas.toDataURL("image/jpeg", 0.8); - const width = video.videoWidth; - const height = video.videoHeight; + video.addEventListener("seeked", () => { + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + const thumbnailUrl = canvas.toDataURL("image/jpeg", 0.8); + const width = video.videoWidth; + const height = video.videoHeight; - resolve({ thumbnailUrl, width, height }); + resolve({ thumbnailUrl, width, height }); - // Cleanup - video.remove(); - canvas.remove(); - }); + // Cleanup + video.remove(); + canvas.remove(); + }); - video.addEventListener("error", () => { - reject(new Error("Could not load video")); - video.remove(); - canvas.remove(); - }); + video.addEventListener("error", () => { + reject(new Error("Could not load video")); + video.remove(); + canvas.remove(); + }); - video.src = URL.createObjectURL(file); - video.load(); - }); + video.src = URL.createObjectURL(file); + video.load(); + }); }; // Helper function to get media duration export const getMediaDuration = (file: File): Promise<number> => { - return new Promise((resolve, reject) => { - const element = document.createElement( - file.type.startsWith("video/") ? "video" : "audio", - ) as HTMLVideoElement; + return new Promise((resolve, reject) => { + const element = document.createElement( + file.type.startsWith("video/") ? "video" : "audio" + ) as HTMLVideoElement; - element.addEventListener("loadedmetadata", () => { - resolve(element.duration); - element.remove(); - }); + element.addEventListener("loadedmetadata", () => { + resolve(element.duration); + element.remove(); + }); - element.addEventListener("error", () => { - reject(new Error("Could not load media")); - element.remove(); - }); + element.addEventListener("error", () => { + reject(new Error("Could not load media")); + element.remove(); + }); - element.src = URL.createObjectURL(file); - element.load(); - }); + element.src = URL.createObjectURL(file); + element.load(); + }); }; // Helper to get aspect ratio from MediaItem export const getMediaAspectRatio = (item: MediaItem): number => { - if (item.width && item.height) { - return item.width / item.height; - } - return 16 / 9; // Default aspect ratio + if (item.width && item.height) { + return item.width / item.height; + } + return 16 / 9; // Default aspect ratio }; export const useMediaStore = create<MediaStore>((set, get) => ({ - mediaItems: [], - isLoading: false, + mediaItems: [], + isLoading: false, - addMediaItem: async (projectId, item) => { - const newItem: MediaItem = { - ...item, - id: generateUUID(), - }; + addMediaItem: async (projectId, item) => { + const newItem: MediaItem = { + ...item, + id: generateUUID(), + }; - // Add to local state immediately for UI responsiveness - set((state) => ({ - mediaItems: [...state.mediaItems, newItem], - })); + // Add to local state immediately for UI responsiveness + set((state) => ({ + mediaItems: [...state.mediaItems, newItem], + })); - // Save to persistent storage in background - try { - await storageService.saveMediaItem(projectId, newItem); - } catch (error) { - console.error("Failed to save media item:", error); - // Remove from local state if save failed - set((state) => ({ - mediaItems: state.mediaItems.filter((media) => media.id !== newItem.id), - })); - } - }, + // Save to persistent storage in background + try { + await storageService.saveMediaItem(projectId, newItem); + } catch (error) { + console.error("Failed to save media item:", error); + // Remove from local state if save failed + set((state) => ({ + mediaItems: state.mediaItems.filter((media) => media.id !== newItem.id), + })); + } + }, - removeMediaItem: async (projectId, id: string) => { - const state = get(); - const item = state.mediaItems.find((media) => media.id === id); + removeMediaItem: async (projectId, id: string) => { + const state = get(); + const item = state.mediaItems.find((media) => media.id === id); - // Cleanup object URLs to prevent memory leaks - if (item && item.url) { - URL.revokeObjectURL(item.url); - if (item.thumbnailUrl) { - URL.revokeObjectURL(item.thumbnailUrl); - } - } + // Cleanup object URLs to prevent memory leaks + if (item && item.url) { + URL.revokeObjectURL(item.url); + if (item.thumbnailUrl) { + URL.revokeObjectURL(item.thumbnailUrl); + } + } - // Remove from local state immediately - set((state) => ({ - mediaItems: state.mediaItems.filter((media) => media.id !== id), - })); + // Remove from local state immediately + set((state) => ({ + mediaItems: state.mediaItems.filter((media) => media.id !== id), + })); - // Remove from persistent storage - try { - await storageService.deleteMediaItem(projectId, id); - } catch (error) { - console.error("Failed to delete media item:", error); - } - }, + // Remove from persistent storage + try { + await storageService.deleteMediaItem(projectId, id); + } catch (error) { + console.error("Failed to delete media item:", error); + } + }, - loadProjectMedia: async (projectId) => { - set({ isLoading: true }); + loadProjectMedia: async (projectId) => { + set({ isLoading: true }); - try { - const mediaItems = await storageService.loadAllMediaItems(projectId); + try { + const mediaItems = await storageService.loadAllMediaItems(projectId); - // Regenerate thumbnails for video items - const updatedMediaItems = await Promise.all( - mediaItems.map(async (item) => { - if (item.type === "video" && item.file) { - try { - const { thumbnailUrl, width, height } = - await generateVideoThumbnail(item.file); - return { - ...item, - thumbnailUrl, - width: width || item.width, - height: height || item.height, - }; - } catch (error) { - console.error( - `Failed to regenerate thumbnail for video ${item.id}:`, - error, - ); - return item; - } - } - return item; - }), - ); + // Regenerate thumbnails for video items + const updatedMediaItems = await Promise.all( + mediaItems.map(async (item) => { + if (item.type === "video" && item.file) { + try { + const { thumbnailUrl, width, height } = await generateVideoThumbnail(item.file); + return { + ...item, + thumbnailUrl, + width: width || item.width, + height: height || item.height + }; + } catch (error) { + console.error(`Failed to regenerate thumbnail for video ${item.id}:`, error); + return item; + } + } + return item; + }) + ); - set({ mediaItems: updatedMediaItems }); - } catch (error) { - console.error("Failed to load media items:", error); - } finally { - set({ isLoading: false }); - } - }, + set({ mediaItems: updatedMediaItems }); + } catch (error) { + console.error("Failed to load media items:", error); + } finally { + set({ isLoading: false }); + } + }, - clearProjectMedia: async (projectId) => { - const state = get(); + clearProjectMedia: async (projectId) => { + const state = get(); - // Cleanup all object URLs - state.mediaItems.forEach((item) => { - if (item.url) { - URL.revokeObjectURL(item.url); - } - if (item.thumbnailUrl) { - URL.revokeObjectURL(item.thumbnailUrl); - } - }); + // Cleanup all object URLs + state.mediaItems.forEach((item) => { + if (item.url) { + URL.revokeObjectURL(item.url); + } + if (item.thumbnailUrl) { + URL.revokeObjectURL(item.thumbnailUrl); + } + }); - // Clear local state - set({ mediaItems: [] }); + // Clear local state + set({ mediaItems: [] }); - // Clear persistent storage - try { - const mediaIds = state.mediaItems.map((item) => item.id); - await Promise.all( - mediaIds.map((id) => storageService.deleteMediaItem(projectId, id)), - ); - } catch (error) { - console.error("Failed to clear media items from storage:", error); - } - }, + // Clear persistent storage + try { + const mediaIds = state.mediaItems.map((item) => item.id); + await Promise.all( + mediaIds.map((id) => storageService.deleteMediaItem(projectId, id)) + ); + } catch (error) { + console.error("Failed to clear media items from storage:", error); + } + }, - clearAllMedia: () => { - const state = get(); + clearAllMedia: () => { + const state = get(); - // Cleanup all object URLs - state.mediaItems.forEach((item) => { - if (item.url) { - URL.revokeObjectURL(item.url); - } - if (item.thumbnailUrl) { - URL.revokeObjectURL(item.thumbnailUrl); - } - }); + // Cleanup all object URLs + state.mediaItems.forEach((item) => { + if (item.url) { + URL.revokeObjectURL(item.url); + } + if (item.thumbnailUrl) { + URL.revokeObjectURL(item.thumbnailUrl); + } + }); - // Clear local state - set({ mediaItems: [] }); - }, + // Clear local state + set({ mediaItems: [] }); + }, })); diff --git a/apps/web/src/stores/panel-store.ts b/apps/web/src/stores/panel-store.ts index 8360ad57..450b6a6e 100644 --- a/apps/web/src/stores/panel-store.ts +++ b/apps/web/src/stores/panel-store.ts @@ -1,45 +1,45 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; - -const DEFAULT_PANEL_SIZES = { - toolsPanel: 45, - previewPanel: 75, - propertiesPanel: 20, - mainContent: 70, - timeline: 30, -} as const; - -interface PanelState { - // Panel sizes as percentages - toolsPanel: number; - previewPanel: number; - propertiesPanel: number; - mainContent: number; - timeline: number; - - // Actions - setToolsPanel: (size: number) => void; - setPreviewPanel: (size: number) => void; - setPropertiesPanel: (size: number) => void; - setMainContent: (size: number) => void; - setTimeline: (size: number) => void; -} - -export const usePanelStore = create<PanelState>()( - persist( - (set) => ({ - // Default sizes - optimized for responsiveness - ...DEFAULT_PANEL_SIZES, - - // Actions - setToolsPanel: (size) => set({ toolsPanel: size }), - setPreviewPanel: (size) => set({ previewPanel: size }), - setPropertiesPanel: (size) => set({ propertiesPanel: size }), - setMainContent: (size) => set({ mainContent: size }), - setTimeline: (size) => set({ timeline: size }), - }), - { - name: "panel-sizes", - }, - ), -); +import { create } from "zustand";
+import { persist } from "zustand/middleware";
+
+const DEFAULT_PANEL_SIZES = {
+ toolsPanel: 45,
+ previewPanel: 75,
+ propertiesPanel: 20,
+ mainContent: 70,
+ timeline: 30,
+} as const;
+
+interface PanelState {
+ // Panel sizes as percentages
+ toolsPanel: number;
+ previewPanel: number;
+ propertiesPanel: number;
+ mainContent: number;
+ timeline: number;
+
+ // Actions
+ setToolsPanel: (size: number) => void;
+ setPreviewPanel: (size: number) => void;
+ setPropertiesPanel: (size: number) => void;
+ setMainContent: (size: number) => void;
+ setTimeline: (size: number) => void;
+}
+
+export const usePanelStore = create<PanelState>()(
+ persist(
+ (set) => ({
+ // Default sizes - optimized for responsiveness
+ ...DEFAULT_PANEL_SIZES,
+
+ // Actions
+ setToolsPanel: (size) => set({ toolsPanel: size }),
+ setPreviewPanel: (size) => set({ previewPanel: size }),
+ setPropertiesPanel: (size) => set({ propertiesPanel: size }),
+ setMainContent: (size) => set({ mainContent: size }),
+ setTimeline: (size) => set({ timeline: size }),
+ }),
+ {
+ name: "panel-sizes",
+ }
+ )
+);
diff --git a/apps/web/src/stores/playback-store.ts b/apps/web/src/stores/playback-store.ts index 3ef1d95c..543865f9 100644 --- a/apps/web/src/stores/playback-store.ts +++ b/apps/web/src/stores/playback-store.ts @@ -2,133 +2,133 @@ import { create } from "zustand"; import type { PlaybackState, PlaybackControls } from "@/types/playback"; interface PlaybackStore extends PlaybackState, PlaybackControls { - setDuration: (duration: number) => void; - setCurrentTime: (time: number) => void; + setDuration: (duration: number) => void; + setCurrentTime: (time: number) => void; } let playbackTimer: number | null = null; const startTimer = (store: () => PlaybackStore) => { - if (playbackTimer) cancelAnimationFrame(playbackTimer); + if (playbackTimer) cancelAnimationFrame(playbackTimer); - // Use requestAnimationFrame for smoother updates - const updateTime = () => { - const state = store(); - if (state.isPlaying && state.currentTime < state.duration) { - const now = performance.now(); - const delta = (now - lastUpdate) / 1000; // Convert to seconds - lastUpdate = now; + // Use requestAnimationFrame for smoother updates + const updateTime = () => { + const state = store(); + if (state.isPlaying && state.currentTime < state.duration) { + const now = performance.now(); + const delta = (now - lastUpdate) / 1000; // Convert to seconds + lastUpdate = now; - const newTime = state.currentTime + delta * state.speed; - if (newTime >= state.duration) { - // When video completes, pause and reset playhead to start - state.pause(); - state.setCurrentTime(0); - // Notify video elements to sync with reset - window.dispatchEvent( - new CustomEvent("playback-seek", { detail: { time: 0 } }), - ); - } else { - state.setCurrentTime(newTime); - // Notify video elements to sync - window.dispatchEvent( - new CustomEvent("playback-update", { detail: { time: newTime } }), - ); - } - } - playbackTimer = requestAnimationFrame(updateTime); - }; + const newTime = state.currentTime + delta * state.speed; + if (newTime >= state.duration) { + // When video completes, pause and reset playhead to start + state.pause(); + state.setCurrentTime(0); + // Notify video elements to sync with reset + window.dispatchEvent( + new CustomEvent("playback-seek", { detail: { time: 0 } }) + ); + } else { + state.setCurrentTime(newTime); + // Notify video elements to sync + window.dispatchEvent( + new CustomEvent("playback-update", { detail: { time: newTime } }) + ); + } + } + playbackTimer = requestAnimationFrame(updateTime); + }; - let lastUpdate = performance.now(); - playbackTimer = requestAnimationFrame(updateTime); + let lastUpdate = performance.now(); + playbackTimer = requestAnimationFrame(updateTime); }; const stopTimer = () => { - if (playbackTimer) { - cancelAnimationFrame(playbackTimer); - playbackTimer = null; - } + if (playbackTimer) { + cancelAnimationFrame(playbackTimer); + playbackTimer = null; + } }; export const usePlaybackStore = create<PlaybackStore>((set, get) => ({ - isPlaying: false, - currentTime: 0, - duration: 0, - volume: 1, - muted: false, - previousVolume: 1, - speed: 1.0, + isPlaying: false, + currentTime: 0, + duration: 0, + volume: 1, + muted: false, + previousVolume: 1, + speed: 1.0, - play: () => { - set({ isPlaying: true }); - startTimer(get); - }, + play: () => { + set({ isPlaying: true }); + startTimer(get); + }, - pause: () => { - set({ isPlaying: false }); - stopTimer(); - }, + pause: () => { + set({ isPlaying: false }); + stopTimer(); + }, - toggle: () => { - const { isPlaying } = get(); - if (isPlaying) { - get().pause(); - } else { - get().play(); - } - }, + toggle: () => { + const { isPlaying } = get(); + if (isPlaying) { + get().pause(); + } else { + get().play(); + } + }, - seek: (time: number) => { - const { duration } = get(); - const clampedTime = Math.max(0, Math.min(duration, time)); - set({ currentTime: clampedTime }); + seek: (time: number) => { + const { duration } = get(); + const clampedTime = Math.max(0, Math.min(duration, time)); + set({ currentTime: clampedTime }); - const event = new CustomEvent("playback-seek", { - detail: { time: clampedTime }, - }); - window.dispatchEvent(event); - }, + const event = new CustomEvent("playback-seek", { + detail: { time: clampedTime }, + }); + window.dispatchEvent(event); + }, - setVolume: (volume: number) => - set((state) => ({ - volume: Math.max(0, Math.min(1, volume)), - muted: volume === 0, - previousVolume: volume > 0 ? volume : state.previousVolume, - })), + setVolume: (volume: number) => + set((state) => ({ + volume: Math.max(0, Math.min(1, volume)), + muted: volume === 0, + previousVolume: volume > 0 ? volume : state.previousVolume, + })), - setSpeed: (speed: number) => { - const newSpeed = Math.max(0.1, Math.min(2.0, speed)); - set({ speed: newSpeed }); + setSpeed: (speed: number) => { + const newSpeed = Math.max(0.1, Math.min(2.0, speed)); + set({ speed: newSpeed }); - const event = new CustomEvent("playback-speed", { - detail: { speed: newSpeed }, - }); - window.dispatchEvent(event); - }, + const event = new CustomEvent("playback-speed", { + detail: { speed: newSpeed }, + }); + window.dispatchEvent(event); + }, - setDuration: (duration: number) => set({ duration }), - setCurrentTime: (time: number) => set({ currentTime: time }), + setDuration: (duration: number) => set({ duration }), + setCurrentTime: (time: number) => set({ currentTime: time }), - mute: () => { - const { volume, previousVolume } = get(); - set({ - muted: true, - previousVolume: volume > 0 ? volume : previousVolume, - volume: 0, - }); - }, + mute: () => { + const { volume, previousVolume } = get(); + set({ + muted: true, + previousVolume: volume > 0 ? volume : previousVolume, + volume: 0, + }); + }, - unmute: () => { - const { previousVolume } = get(); - set({ muted: false, volume: previousVolume ?? 1 }); - }, + unmute: () => { + const { previousVolume } = get(); + set({ muted: false, volume: previousVolume ?? 1 }); + }, - toggleMute: () => { - const { muted } = get(); - if (muted) { - get().unmute(); - } else { - get().mute(); - } - }, + toggleMute: () => { + const { muted } = get(); + if (muted) { + get().unmute(); + } else { + get().mute(); + } + }, })); diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index 36ab3503..a8833982 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -1,361 +1,361 @@ -import { TProject } from "@/types/project"; -import { create } from "zustand"; -import { storageService } from "@/lib/storage/storage-service"; -import { toast } from "sonner"; -import { useMediaStore } from "./media-store"; -import { useTimelineStore } from "./timeline-store"; -import { generateUUID } from "@/lib/utils"; - -interface ProjectStore { - activeProject: TProject | null; - savedProjects: TProject[]; - isLoading: boolean; - isInitialized: boolean; - - // Actions - createNewProject: (name: string) => Promise<string>; - loadProject: (id: string) => Promise<void>; - saveCurrentProject: () => Promise<void>; - loadAllProjects: () => Promise<void>; - deleteProject: (id: string) => Promise<void>; - closeProject: () => void; - renameProject: (projectId: string, name: string) => Promise<void>; - duplicateProject: (projectId: string) => Promise<string>; - updateProjectBackground: (backgroundColor: string) => Promise<void>; - updateBackgroundType: ( - type: "color" | "blur", - options?: { backgroundColor?: string; blurIntensity?: number }, - ) => Promise<void>; - updateProjectFps: (fps: number) => Promise<void>; - - getFilteredAndSortedProjects: ( - searchQuery: string, - sortOption: string, - ) => TProject[]; -} - -export const useProjectStore = create<ProjectStore>((set, get) => ({ - activeProject: null, - savedProjects: [], - isLoading: true, - isInitialized: false, - - createNewProject: async (name: string) => { - const newProject: TProject = { - id: generateUUID(), - name, - thumbnail: "", - createdAt: new Date(), - updatedAt: new Date(), - backgroundColor: "#000000", - backgroundType: "color", - blurIntensity: 8, - }; - - set({ activeProject: newProject }); - - try { - await storageService.saveProject(newProject); - // Reload all projects to update the list - await get().loadAllProjects(); - return newProject.id; - } catch (error) { - toast.error("Failed to save new project"); - throw error; - } - }, - - loadProject: async (id: string) => { - if (!get().isInitialized) { - set({ isLoading: true }); - } - - // Clear media and timeline immediately to prevent flickering when switching projects - const mediaStore = useMediaStore.getState(); - const timelineStore = useTimelineStore.getState(); - mediaStore.clearAllMedia(); - timelineStore.clearTimeline(); - - try { - const project = await storageService.loadProject(id); - if (project) { - set({ activeProject: project }); - - // Load project-specific data in parallel - await Promise.all([ - mediaStore.loadProjectMedia(id), - timelineStore.loadProjectTimeline(id), - ]); - } else { - throw new Error(`Project with id ${id} not found`); - } - } catch (error) { - console.error("Failed to load project:", error); - throw error; // Re-throw so the editor page can handle it - } finally { - set({ isLoading: false }); - } - }, - - saveCurrentProject: async () => { - const { activeProject } = get(); - if (!activeProject) return; - - try { - // Save project metadata and timeline data in parallel - const timelineStore = useTimelineStore.getState(); - await Promise.all([ - storageService.saveProject(activeProject), - timelineStore.saveProjectTimeline(activeProject.id), - ]); - await get().loadAllProjects(); // Refresh the list - } catch (error) { - console.error("Failed to save project:", error); - } - }, - - loadAllProjects: async () => { - if (!get().isInitialized) { - set({ isLoading: true }); - } - - try { - const projects = await storageService.loadAllProjects(); - set({ savedProjects: projects }); - } catch (error) { - console.error("Failed to load projects:", error); - } finally { - set({ isLoading: false, isInitialized: true }); - } - }, - - deleteProject: async (id: string) => { - try { - // Delete project data in parallel - await Promise.all([ - storageService.deleteProjectMedia(id), - storageService.deleteProjectTimeline(id), - storageService.deleteProject(id), - ]); - await get().loadAllProjects(); // Refresh the list - - // If we deleted the active project, close it and clear data - const { activeProject } = get(); - if (activeProject?.id === id) { - set({ activeProject: null }); - const mediaStore = useMediaStore.getState(); - const timelineStore = useTimelineStore.getState(); - mediaStore.clearAllMedia(); - timelineStore.clearTimeline(); - } - } catch (error) { - console.error("Failed to delete project:", error); - } - }, - - closeProject: () => { - set({ activeProject: null }); - - // Clear data from stores when closing project - const mediaStore = useMediaStore.getState(); - const timelineStore = useTimelineStore.getState(); - mediaStore.clearAllMedia(); - timelineStore.clearTimeline(); - }, - - renameProject: async (id: string, name: string) => { - const { savedProjects } = get(); - - // Find the project to rename - const projectToRename = savedProjects.find((p) => p.id === id); - if (!projectToRename) { - toast.error("Project not found", { - description: "Please try again", - }); - return; - } - - const updatedProject = { - ...projectToRename, - name, - updatedAt: new Date(), - }; - - try { - // Save to storage - await storageService.saveProject(updatedProject); - - await get().loadAllProjects(); - - // Update activeProject if it's the same project - const { activeProject } = get(); - if (activeProject?.id === id) { - set({ activeProject: updatedProject }); - } - } catch (error) { - console.error("Failed to rename project:", error); - toast.error("Failed to rename project", { - description: - error instanceof Error ? error.message : "Please try again", - }); - } - }, - - duplicateProject: async (projectId: string) => { - try { - const project = await storageService.loadProject(projectId); - if (!project) { - toast.error("Project not found", { - description: "Please try again", - }); - throw new Error("Project not found"); - } - - const { savedProjects } = get(); - - // Extract the base name (remove any existing numbering) - const numberMatch = project.name.match(/^\((\d+)\)\s+(.+)$/); - const baseName = numberMatch ? numberMatch[2] : project.name; - const existingNumbers: number[] = []; - - // Check for pattern "(number) baseName" in existing projects - savedProjects.forEach((p) => { - const match = p.name.match(/^\((\d+)\)\s+(.+)$/); - if (match && match[2] === baseName) { - existingNumbers.push(parseInt(match[1], 10)); - } - }); - - const nextNumber = - existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1; - - const newProject: TProject = { - id: generateUUID(), - name: `(${nextNumber}) ${baseName}`, - thumbnail: project.thumbnail, - createdAt: new Date(), - updatedAt: new Date(), - }; - - await storageService.saveProject(newProject); - await get().loadAllProjects(); - return newProject.id; - } catch (error) { - console.error("Failed to duplicate project:", error); - toast.error("Failed to duplicate project", { - description: - error instanceof Error ? error.message : "Please try again", - }); - throw error; - } - }, - - updateProjectBackground: async (backgroundColor: string) => { - const { activeProject } = get(); - if (!activeProject) return; - - const updatedProject = { - ...activeProject, - backgroundColor, - updatedAt: new Date(), - }; - - try { - await storageService.saveProject(updatedProject); - set({ activeProject: updatedProject }); - await get().loadAllProjects(); // Refresh the list - } catch (error) { - console.error("Failed to update project background:", error); - toast.error("Failed to update background", { - description: "Please try again", - }); - } - }, - - updateBackgroundType: async ( - type: "color" | "blur", - options?: { backgroundColor?: string; blurIntensity?: number }, - ) => { - const { activeProject } = get(); - if (!activeProject) return; - - const updatedProject = { - ...activeProject, - backgroundType: type, - ...(options?.backgroundColor && { - backgroundColor: options.backgroundColor, - }), - ...(options?.blurIntensity && { blurIntensity: options.blurIntensity }), - updatedAt: new Date(), - }; - - try { - await storageService.saveProject(updatedProject); - set({ activeProject: updatedProject }); - await get().loadAllProjects(); // Refresh the list - } catch (error) { - console.error("Failed to update background type:", error); - toast.error("Failed to update background", { - description: "Please try again", - }); - } - }, - - updateProjectFps: async (fps: number) => { - const { activeProject } = get(); - if (!activeProject) return; - - const updatedProject = { - ...activeProject, - fps, - updatedAt: new Date(), - }; - - try { - await storageService.saveProject(updatedProject); - set({ activeProject: updatedProject }); - await get().loadAllProjects(); // Refresh the list - } catch (error) { - console.error("Failed to update project FPS:", error); - toast.error("Failed to update project FPS", { - description: "Please try again", - }); - } - }, - - getFilteredAndSortedProjects: (searchQuery: string, sortOption: string) => { - const { savedProjects } = get(); - - // Filter projects by search query - const filteredProjects = savedProjects.filter((project) => - project.name.toLowerCase().includes(searchQuery.toLowerCase()), - ); - - // Sort filtered projects - const sortedProjects = [...filteredProjects].sort((a, b) => { - const [key, order] = sortOption.split("-"); - - if (key !== "createdAt" && key !== "name") { - console.warn(`Invalid sort key: ${key}`); - return 0; - } - - const aValue = a[key]; - const bValue = b[key]; - - if (aValue === undefined || bValue === undefined) return 0; - - if (order === "asc") { - if (aValue < bValue) return -1; - if (aValue > bValue) return 1; - return 0; - } else { - if (aValue > bValue) return -1; - if (aValue < bValue) return 1; - return 0; - } - }); - - return sortedProjects; - }, -})); +import { TProject } from "@/types/project";
+import { create } from "zustand";
+import { storageService } from "@/lib/storage/storage-service";
+import { toast } from "sonner";
+import { useMediaStore } from "./media-store";
+import { useTimelineStore } from "./timeline-store";
+import { generateUUID } from "@/lib/utils";
+
+interface ProjectStore {
+ activeProject: TProject | null;
+ savedProjects: TProject[];
+ isLoading: boolean;
+ isInitialized: boolean;
+
+ // Actions
+ createNewProject: (name: string) => Promise<string>;
+ loadProject: (id: string) => Promise<void>;
+ saveCurrentProject: () => Promise<void>;
+ loadAllProjects: () => Promise<void>;
+ deleteProject: (id: string) => Promise<void>;
+ closeProject: () => void;
+ renameProject: (projectId: string, name: string) => Promise<void>;
+ duplicateProject: (projectId: string) => Promise<string>;
+ updateProjectBackground: (backgroundColor: string) => Promise<void>;
+ updateBackgroundType: (
+ type: "color" | "blur",
+ options?: { backgroundColor?: string; blurIntensity?: number }
+ ) => Promise<void>;
+ updateProjectFps: (fps: number) => Promise<void>;
+
+ getFilteredAndSortedProjects: (
+ searchQuery: string,
+ sortOption: string
+ ) => TProject[];
+}
+
+export const useProjectStore = create<ProjectStore>((set, get) => ({
+ activeProject: null,
+ savedProjects: [],
+ isLoading: true,
+ isInitialized: false,
+
+ createNewProject: async (name: string) => {
+ const newProject: TProject = {
+ id: generateUUID(),
+ name,
+ thumbnail: "",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ backgroundColor: "#000000",
+ backgroundType: "color",
+ blurIntensity: 8,
+ };
+
+ set({ activeProject: newProject });
+
+ try {
+ await storageService.saveProject(newProject);
+ // Reload all projects to update the list
+ await get().loadAllProjects();
+ return newProject.id;
+ } catch (error) {
+ toast.error("Failed to save new project");
+ throw error;
+ }
+ },
+
+ loadProject: async (id: string) => {
+ if (!get().isInitialized) {
+ set({ isLoading: true });
+ }
+
+ // Clear media and timeline immediately to prevent flickering when switching projects
+ const mediaStore = useMediaStore.getState();
+ const timelineStore = useTimelineStore.getState();
+ mediaStore.clearAllMedia();
+ timelineStore.clearTimeline();
+
+ try {
+ const project = await storageService.loadProject(id);
+ if (project) {
+ set({ activeProject: project });
+
+ // Load project-specific data in parallel
+ await Promise.all([
+ mediaStore.loadProjectMedia(id),
+ timelineStore.loadProjectTimeline(id),
+ ]);
+ } else {
+ throw new Error(`Project with id ${id} not found`);
+ }
+ } catch (error) {
+ console.error("Failed to load project:", error);
+ throw error; // Re-throw so the editor page can handle it
+ } finally {
+ set({ isLoading: false });
+ }
+ },
+
+ saveCurrentProject: async () => {
+ const { activeProject } = get();
+ if (!activeProject) return;
+
+ try {
+ // Save project metadata and timeline data in parallel
+ const timelineStore = useTimelineStore.getState();
+ await Promise.all([
+ storageService.saveProject(activeProject),
+ timelineStore.saveProjectTimeline(activeProject.id),
+ ]);
+ await get().loadAllProjects(); // Refresh the list
+ } catch (error) {
+ console.error("Failed to save project:", error);
+ }
+ },
+
+ loadAllProjects: async () => {
+ if (!get().isInitialized) {
+ set({ isLoading: true });
+ }
+
+ try {
+ const projects = await storageService.loadAllProjects();
+ set({ savedProjects: projects });
+ } catch (error) {
+ console.error("Failed to load projects:", error);
+ } finally {
+ set({ isLoading: false, isInitialized: true });
+ }
+ },
+
+ deleteProject: async (id: string) => {
+ try {
+ // Delete project data in parallel
+ await Promise.all([
+ storageService.deleteProjectMedia(id),
+ storageService.deleteProjectTimeline(id),
+ storageService.deleteProject(id),
+ ]);
+ await get().loadAllProjects(); // Refresh the list
+
+ // If we deleted the active project, close it and clear data
+ const { activeProject } = get();
+ if (activeProject?.id === id) {
+ set({ activeProject: null });
+ const mediaStore = useMediaStore.getState();
+ const timelineStore = useTimelineStore.getState();
+ mediaStore.clearAllMedia();
+ timelineStore.clearTimeline();
+ }
+ } catch (error) {
+ console.error("Failed to delete project:", error);
+ }
+ },
+
+ closeProject: () => {
+ set({ activeProject: null });
+
+ // Clear data from stores when closing project
+ const mediaStore = useMediaStore.getState();
+ const timelineStore = useTimelineStore.getState();
+ mediaStore.clearAllMedia();
+ timelineStore.clearTimeline();
+ },
+
+ renameProject: async (id: string, name: string) => {
+ const { savedProjects } = get();
+
+ // Find the project to rename
+ const projectToRename = savedProjects.find((p) => p.id === id);
+ if (!projectToRename) {
+ toast.error("Project not found", {
+ description: "Please try again",
+ });
+ return;
+ }
+
+ const updatedProject = {
+ ...projectToRename,
+ name,
+ updatedAt: new Date(),
+ };
+
+ try {
+ // Save to storage
+ await storageService.saveProject(updatedProject);
+
+ await get().loadAllProjects();
+
+ // Update activeProject if it's the same project
+ const { activeProject } = get();
+ if (activeProject?.id === id) {
+ set({ activeProject: updatedProject });
+ }
+ } catch (error) {
+ console.error("Failed to rename project:", error);
+ toast.error("Failed to rename project", {
+ description:
+ error instanceof Error ? error.message : "Please try again",
+ });
+ }
+ },
+
+ duplicateProject: async (projectId: string) => {
+ try {
+ const project = await storageService.loadProject(projectId);
+ if (!project) {
+ toast.error("Project not found", {
+ description: "Please try again",
+ });
+ throw new Error("Project not found");
+ }
+
+ const { savedProjects } = get();
+
+ // Extract the base name (remove any existing numbering)
+ const numberMatch = project.name.match(/^\((\d+)\)\s+(.+)$/);
+ const baseName = numberMatch ? numberMatch[2] : project.name;
+ const existingNumbers: number[] = [];
+
+ // Check for pattern "(number) baseName" in existing projects
+ savedProjects.forEach((p) => {
+ const match = p.name.match(/^\((\d+)\)\s+(.+)$/);
+ if (match && match[2] === baseName) {
+ existingNumbers.push(parseInt(match[1], 10));
+ }
+ });
+
+ const nextNumber =
+ existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1;
+
+ const newProject: TProject = {
+ id: generateUUID(),
+ name: `(${nextNumber}) ${baseName}`,
+ thumbnail: project.thumbnail,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+
+ await storageService.saveProject(newProject);
+ await get().loadAllProjects();
+ return newProject.id;
+ } catch (error) {
+ console.error("Failed to duplicate project:", error);
+ toast.error("Failed to duplicate project", {
+ description:
+ error instanceof Error ? error.message : "Please try again",
+ });
+ throw error;
+ }
+ },
+
+ updateProjectBackground: async (backgroundColor: string) => {
+ const { activeProject } = get();
+ if (!activeProject) return;
+
+ const updatedProject = {
+ ...activeProject,
+ backgroundColor,
+ updatedAt: new Date(),
+ };
+
+ try {
+ await storageService.saveProject(updatedProject);
+ set({ activeProject: updatedProject });
+ await get().loadAllProjects(); // Refresh the list
+ } catch (error) {
+ console.error("Failed to update project background:", error);
+ toast.error("Failed to update background", {
+ description: "Please try again",
+ });
+ }
+ },
+
+ updateBackgroundType: async (
+ type: "color" | "blur",
+ options?: { backgroundColor?: string; blurIntensity?: number }
+ ) => {
+ const { activeProject } = get();
+ if (!activeProject) return;
+
+ const updatedProject = {
+ ...activeProject,
+ backgroundType: type,
+ ...(options?.backgroundColor && {
+ backgroundColor: options.backgroundColor,
+ }),
+ ...(options?.blurIntensity && { blurIntensity: options.blurIntensity }),
+ updatedAt: new Date(),
+ };
+
+ try {
+ await storageService.saveProject(updatedProject);
+ set({ activeProject: updatedProject });
+ await get().loadAllProjects(); // Refresh the list
+ } catch (error) {
+ console.error("Failed to update background type:", error);
+ toast.error("Failed to update background", {
+ description: "Please try again",
+ });
+ }
+ },
+
+ updateProjectFps: async (fps: number) => {
+ const { activeProject } = get();
+ if (!activeProject) return;
+
+ const updatedProject = {
+ ...activeProject,
+ fps,
+ updatedAt: new Date(),
+ };
+
+ try {
+ await storageService.saveProject(updatedProject);
+ set({ activeProject: updatedProject });
+ await get().loadAllProjects(); // Refresh the list
+ } catch (error) {
+ console.error("Failed to update project FPS:", error);
+ toast.error("Failed to update project FPS", {
+ description: "Please try again",
+ });
+ }
+ },
+
+ getFilteredAndSortedProjects: (searchQuery: string, sortOption: string) => {
+ const { savedProjects } = get();
+
+ // Filter projects by search query
+ const filteredProjects = savedProjects.filter((project) =>
+ project.name.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+
+ // Sort filtered projects
+ const sortedProjects = [...filteredProjects].sort((a, b) => {
+ const [key, order] = sortOption.split("-");
+
+ if (key !== "createdAt" && key !== "name") {
+ console.warn(`Invalid sort key: ${key}`);
+ return 0;
+ }
+
+ const aValue = a[key];
+ const bValue = b[key];
+
+ if (aValue === undefined || bValue === undefined) return 0;
+
+ if (order === "asc") {
+ if (aValue < bValue) return -1;
+ if (aValue > bValue) return 1;
+ return 0;
+ } else {
+ if (aValue > bValue) return -1;
+ if (aValue < bValue) return 1;
+ return 0;
+ }
+ });
+
+ return sortedProjects;
+ },
+}));
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index ec724539..9a2c7361 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"; @@ -24,1108 +24,1106 @@ import { toast } from "sonner"; // 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; - // 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; - 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; + 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>; - // 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: [], + return { + _tracks: initialTracks, + tracks: sortedInitialTracks, + history: [], + redoStack: [], + selectedElements: [], - // 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) => { - get().pushHistory(); - updateTracksAndSave( - get()._tracks.filter((track) => track.id !== trackId), - ); - }, + removeTrack: (trackId) => { + get().pushHistory(); + updateTracksAndSave( + get()._tracks.filter((track) => track.id !== trackId) + ); + }, - 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) => { - 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), - ); - }, + removeElementFromTrack: (trackId, elementId) => { + 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) + ); + }, - 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 + ) + ); + }, - 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 })); + }, - 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; + }, + }; }); |
