import { TProject, TScene } 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 "../apps/web/src/stores/timeline-store"; import { useSceneStore } from "./scene-store"; import { generateUUID } from "@/lib/utils"; import { TCanvasSize } from "@/types/editor"; import { DEFAULT_BLUR_INTENSITY, DEFAULT_CANVAS_SIZE, DEFAULT_FPS, } from "@/constants/editor-constants"; export function createMainScene(): TScene { return { id: generateUUID(), name: "Main Scene", isMain: true, createdAt: new Date(), updatedAt: new Date(), }; } const createDefaultProject = (name: string): TProject => { const mainScene: TScene = createMainScene(); return { id: generateUUID(), name, thumbnail: "", createdAt: new Date(), updatedAt: new Date(), scenes: [mainScene], currentSceneId: mainScene.id, backgroundColor: "#000000", backgroundType: "color", blurIntensity: DEFAULT_BLUR_INTENSITY, fps: DEFAULT_FPS, canvasSize: DEFAULT_CANVAS_SIZE, }; }; interface ProjectStore { activeProject: TProject | null; savedProjects: TProject[]; isLoading: boolean; isInitialized: boolean; invalidProjectIds?: Set; createNewProject: (name: string) => Promise; loadProject: (id: string) => Promise; saveCurrentProject: () => Promise; loadAllProjects: () => Promise; deleteProject: (id: string) => Promise; closeProject: () => void; renameProject: (projectId: string, name: string) => Promise; duplicateProject: (projectId: string) => Promise; updateProjectBackground: (backgroundColor: string) => Promise; updateBackgroundType: ( type: "color" | "blur", options?: { backgroundColor?: string; blurIntensity?: number }, ) => Promise; updateProjectFps: (fps: number) => Promise; updateCanvasSize: ({ size }: { size: TCanvasSize }) => Promise; getFilteredAndSortedProjects: ( searchQuery: string, sortOption: string, ) => TProject[]; isInvalidProjectId: (id: string) => boolean; markProjectIdAsInvalid: (id: string) => void; clearInvalidProjectIds: () => void; } export const useProjectStore = create((set, get) => ({ activeProject: null, savedProjects: [], isLoading: true, isInitialized: false, invalidProjectIds: new Set(), createNewProject: async (name: string) => { const newProject = createDefaultProject(name); set({ activeProject: newProject }); const mediaStore = useMediaStore.getState(); const timelineStore = useTimelineStore.getState(); const sceneStore = useSceneStore.getState(); mediaStore.clearAllMedia(); timelineStore.clearTimeline(); sceneStore.initializeScenes({ scenes: newProject.scenes, currentSceneId: newProject.currentSceneId, }); try { await storageService.saveProject({ project: 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 }); } // Prevent flicker when switching projects - clear all stores const mediaStore = useMediaStore.getState(); const timelineStore = useTimelineStore.getState(); const sceneStore = useSceneStore.getState(); mediaStore.clearAllMedia(); timelineStore.clearTimeline(); sceneStore.clearScenes(); try { const project = await storageService.loadProject({ id }); if (project) { set({ activeProject: project }); let currentScene = null; if (project.scenes && project.scenes.length > 0) { sceneStore.initializeScenes({ scenes: project.scenes, currentSceneId: project.currentSceneId, }); // Get current scene directly from project data (don't rely on store state) currentScene = project.scenes.find((s) => s.id === project.currentSceneId) || project.scenes.find((s) => s.isMain) || project.scenes[0]; } await Promise.all([ mediaStore.loadProjectMedia(id), timelineStore.loadProjectTimeline({ projectId: id, sceneId: currentScene?.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 { const timelineStore = useTimelineStore.getState(); const sceneStore = useSceneStore.getState(); const currentScene = sceneStore.currentScene; await Promise.all([ storageService.saveProject({ project: activeProject }), timelineStore.saveProjectTimeline({ projectId: activeProject.id, sceneId: currentScene?.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 { await Promise.all([ storageService.deleteProjectMedia({ projectId: id }), storageService.deleteProjectTimeline({ projectId: id }), storageService.deleteProject({ id }), ]); await get().loadAllProjects(); // Refresh the list // If deleted 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(); const sceneStore = useSceneStore.getState(); mediaStore.clearAllMedia(); timelineStore.clearTimeline(); sceneStore.clearScenes(); } } catch (error) { console.error("Failed to delete project:", error); } }, closeProject: () => { set({ activeProject: null }); const mediaStore = useMediaStore.getState(); const timelineStore = useTimelineStore.getState(); const sceneStore = useSceneStore.getState(); mediaStore.clearAllMedia(); timelineStore.clearTimeline(); sceneStore.clearScenes(); }, 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 { await storageService.saveProject({ project: updatedProject }); await get().loadAllProjects(); // Update activeProject if 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({ id: 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 = { ...project, id: generateUUID(), name: `(${nextNumber}) ${baseName}`, createdAt: new Date(), updatedAt: new Date(), }; await storageService.saveProject({ project: 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({ project: updatedProject }); set({ activeProject: updatedProject }); await get().loadAllProjects(); } 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 !== undefined && { blurIntensity: options.blurIntensity, }), updatedAt: new Date(), }; try { await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); await get().loadAllProjects(); } 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({ project: updatedProject }); set({ activeProject: updatedProject }); await get().loadAllProjects(); } catch (error) { console.error("Failed to update project FPS:", error); toast.error("Failed to update project FPS", { description: "Please try again", }); } }, updateCanvasSize: async ({ size }: { size: TCanvasSize }) => { const { activeProject } = get(); if (!activeProject) return; const updatedProject: TProject = { ...activeProject, canvasSize: size, updatedAt: new Date(), }; try { await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); await get().loadAllProjects(); } catch (error) { console.error("Failed to update canvas size:", error); toast.error("Failed to update canvas size", { description: "Please try again", }); } }, getFilteredAndSortedProjects: (searchQuery: string, sortOption: string) => { const { savedProjects } = get(); const filteredProjects = savedProjects.filter((project) => project.name.toLowerCase().includes(searchQuery.toLowerCase()), ); 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; }, // Global invalid project ID tracking isInvalidProjectId: (id: string) => { const invalidIds = get().invalidProjectIds || new Set(); return invalidIds.has(id); }, markProjectIdAsInvalid: (id: string) => { set((state) => ({ invalidProjectIds: new Set([ ...(state.invalidProjectIds || new Set()), id, ]), })); }, clearInvalidProjectIds: () => { set({ invalidProjectIds: new Set() }); }, }));