summaryrefslogtreecommitdiff
path: root/apps/web/src/stores
diff options
context:
space:
mode:
authorKha Nguyen <hi@khanguyen.com>2025-07-24 17:08:33 -0500
committerKha Nguyen <hi@khanguyen.com>2025-07-24 17:10:39 -0500
commit8dcbbfa25245624cfaa1f8b8b1a21c6c4e29d993 (patch)
tree6d5e8268b98bea8fda8356a817a0662730613cb5 /apps/web/src/stores
parent8e737d87e0accbf1d433f4c8b1e8e0f125849799 (diff)
parent65c3f9f7f78f787262a88c75b96a118ca5bee15c (diff)
downloadopencut-8dcbbfa25245624cfaa1f8b8b1a21c6c4e29d993.tar.gz
Merge branch 'main' into fix/project-title-styling
Diffstat (limited to 'apps/web/src/stores')
-rw-r--r--apps/web/src/stores/editor-store.ts209
-rw-r--r--apps/web/src/stores/media-store.ts10
-rw-r--r--apps/web/src/stores/panel-store.ts90
-rw-r--r--apps/web/src/stores/project-store.ts721
-rw-r--r--apps/web/src/stores/timeline-store.ts11
5 files changed, 522 insertions, 519 deletions
diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts
index c24929a2..aac7077e 100644
--- a/apps/web/src/stores/editor-store.ts
+++ b/apps/web/src/stores/editor-store.ts
@@ -1,105 +1,104 @@
-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) };
+ }
+ // 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 c4ee5f3e..029307d9 100644
--- a/apps/web/src/stores/media-store.ts
+++ b/apps/web/src/stores/media-store.ts
@@ -219,15 +219,19 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
mediaItems.map(async (item) => {
if (item.type === "video" && item.file) {
try {
- const { thumbnailUrl, width, height } = await generateVideoThumbnail(item.file);
+ const { thumbnailUrl, width, height } =
+ await generateVideoThumbnail(item.file);
return {
...item,
thumbnailUrl,
width: width || item.width,
- height: height || item.height
+ height: height || item.height,
};
} catch (error) {
- console.error(`Failed to regenerate thumbnail for video ${item.id}:`, error);
+ console.error(
+ `Failed to regenerate thumbnail for video ${item.id}:`,
+ error
+ );
return item;
}
}
diff --git a/apps/web/src/stores/panel-store.ts b/apps/web/src/stores/panel-store.ts
index 450b6a6e..b97235fa 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/project-store.ts b/apps/web/src/stores/project-store.ts
index a8833982..7bb38039 100644
--- a/apps/web/src/stores/project-store.ts
+++ b/apps/web/src/stores/project-store.ts
@@ -1,361 +1,360 @@
-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;
+ }
+ 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 8a4abc6a..820025cc 100644
--- a/apps/web/src/stores/timeline-store.ts
+++ b/apps/web/src/stores/timeline-store.ts
@@ -295,9 +295,8 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
{ trackId, elementId },
],
};
- } else {
- return { selectedElements: [{ trackId, elementId }] };
}
+ return { selectedElements: [{ trackId, elementId }] };
});
},
@@ -680,7 +679,8 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
(element) => element.id !== elementId
),
};
- } else if (track.id === toTrackId) {
+ }
+ if (track.id === toTrackId) {
return {
...track,
elements: [...track.elements, elementToMove],
@@ -1091,7 +1091,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
if (!fileType) return false;
// Process the new media file
- let mediaData: any = {
+ const mediaData: any = {
name: newFile.name,
type: fileType,
file: newFile,
@@ -1204,7 +1204,8 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
);
const { thumbnailUrl } = await generateVideoThumbnail(mediaItem.file);
return thumbnailUrl;
- } else if (mediaItem.type === "image" && mediaItem.url) {
+ }
+ if (mediaItem.type === "image" && mediaItem.url) {
return mediaItem.url;
}