diff options
Diffstat (limited to 'apps/web/src/stores')
| -rw-r--r-- | apps/web/src/stores/media-store.ts | 8 | ||||
| -rw-r--r-- | apps/web/src/stores/project-store.ts | 145 | ||||
| -rw-r--r-- | apps/web/src/stores/scene-store.ts | 219 | ||||
| -rw-r--r-- | apps/web/src/stores/sounds-store.ts | 4 | ||||
| -rw-r--r-- | apps/web/src/stores/timeline-store.ts | 76 |
5 files changed, 382 insertions, 70 deletions
diff --git a/apps/web/src/stores/media-store.ts b/apps/web/src/stores/media-store.ts index c9e29fff..e00cffd5 100644 --- a/apps/web/src/stores/media-store.ts +++ b/apps/web/src/stores/media-store.ts @@ -152,7 +152,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ // Save to persistent storage in background try { - await storageService.saveMediaFile(projectId, newItem); + await storageService.saveMediaFile({ projectId, mediaItem: newItem }); } catch (error) { console.error("Failed to save media item:", error); // Remove from local state if save failed @@ -217,7 +217,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ // 3) Remove from persistent storage try { - await storageService.deleteMediaFile(projectId, id); + await storageService.deleteMediaFile({ projectId, id }); } catch (error) { console.error("Failed to delete media item:", error); } @@ -227,7 +227,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ set({ isLoading: true }); try { - const mediaItems = await storageService.loadAllMediaFiles(projectId); + const mediaItems = await storageService.loadAllMediaFiles({ projectId }); // Regenerate thumbnails for video items const updatedMediaItems = await Promise.all( @@ -282,7 +282,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ try { const mediaIds = state.mediaFiles.map((item) => item.id); await Promise.all( - mediaIds.map((id) => storageService.deleteMediaFile(projectId, id)) + mediaIds.map((id) => storageService.deleteMediaFile({ projectId, id })) ); } catch (error) { console.error("Failed to clear media items from storage:", error); diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index ed5eda73..d04c23d5 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -1,28 +1,44 @@ -import { TProject, BlurIntensity } from "@/types/project"; +import { TProject, BlurIntensity, Scene } 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 { useSceneStore } from "./scene-store"; import { generateUUID } from "@/lib/utils"; import { CanvasSize, CanvasMode } from "@/types/editor"; export const DEFAULT_CANVAS_SIZE: CanvasSize = { width: 1920, height: 1080 }; export const DEFAULT_FPS = 30; -const DEFAULT_PROJECT: TProject = { - id: generateUUID(), - name: "Untitled", - thumbnail: "", - createdAt: new Date(), - updatedAt: new Date(), - backgroundColor: "#000000", - backgroundType: "color", - blurIntensity: 8, - bookmarks: [], - fps: DEFAULT_FPS, - canvasSize: DEFAULT_CANVAS_SIZE, - canvasMode: "preset", +function createMainScene(): Scene { + return { + id: generateUUID(), + name: "Main Scene", + isMain: true, + createdAt: new Date(), + updatedAt: new Date(), + }; +} + +const createDefaultProject = (name: string): TProject => { + const mainScene = createMainScene(); + return { + id: generateUUID(), + name, + thumbnail: "", + createdAt: new Date(), + updatedAt: new Date(), + scenes: [mainScene], + currentSceneId: mainScene.id, + backgroundColor: "#000000", + backgroundType: "color", + blurIntensity: 8, + bookmarks: [], + fps: DEFAULT_FPS, + canvasSize: DEFAULT_CANVAS_SIZE, + canvasMode: "preset", + }; }; interface ProjectStore { @@ -104,7 +120,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); await get().loadAllProjects(); // Refresh the list } catch (error) { @@ -152,7 +168,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); await get().loadAllProjects(); // Refresh the list } catch (error) { @@ -164,23 +180,24 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }, createNewProject: async (name: string) => { - const newProject: TProject = { - ...DEFAULT_PROJECT, - id: generateUUID(), - name, - createdAt: new Date(), - updatedAt: new Date(), - }; + 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(newProject); + await storageService.saveProject({ project: newProject }); // Reload all projects to update the list await get().loadAllProjects(); return newProject.id; @@ -195,21 +212,35 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ set({ isLoading: true }); } - // Clear media and timeline immediately to prevent flickering when switching projects + // 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); + const project = await storageService.loadProject({ id }); if (project) { set({ activeProject: project }); - // Load project-specific data in parallel + if (project.scenes && project.scenes.length > 0) { + sceneStore.initializeScenes({ + scenes: project.scenes, + currentSceneId: project.currentSceneId, + }); + } + + const currentScene = sceneStore.currentScene; + await Promise.all([ mediaStore.loadProjectMedia(id), - timelineStore.loadProjectTimeline(id), + timelineStore.loadProjectTimeline({ + projectId: id, + sceneId: currentScene?.id, + }), ]); } else { throw new Error(`Project with id ${id} not found`); @@ -227,11 +258,16 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ if (!activeProject) return; try { - // Save project metadata and timeline data in parallel const timelineStore = useTimelineStore.getState(); + const sceneStore = useSceneStore.getState(); + const currentScene = sceneStore.currentScene; + await Promise.all([ - storageService.saveProject(activeProject), - timelineStore.saveProjectTimeline(activeProject.id), + storageService.saveProject({ project: activeProject }), + timelineStore.saveProjectTimeline({ + projectId: activeProject.id, + sceneId: currentScene?.id, + }), ]); await get().loadAllProjects(); // Refresh the list } catch (error) { @@ -256,22 +292,24 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ deleteProject: async (id: string) => { try { - // Delete project data in parallel await Promise.all([ - storageService.deleteProjectMedia(id), - storageService.deleteProjectTimeline(id), - storageService.deleteProject(id), + storageService.deleteProjectMedia({ projectId: id }), + storageService.deleteProjectTimeline({ projectId: id }), + storageService.deleteProject({ id }), ]); await get().loadAllProjects(); // Refresh the list - // If we deleted the active project, close it and clear data + // 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); @@ -281,11 +319,13 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ closeProject: () => { set({ activeProject: null }); - // Clear data from stores when closing project const mediaStore = useMediaStore.getState(); const timelineStore = useTimelineStore.getState(); + const sceneStore = useSceneStore.getState(); + mediaStore.clearAllMedia(); timelineStore.clearTimeline(); + sceneStore.clearScenes(); }, renameProject: async (id: string, name: string) => { @@ -307,12 +347,11 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - // Save to storage - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); await get().loadAllProjects(); - // Update activeProject if it's the same project + // Update activeProject if same project const { activeProject } = get(); if (activeProject?.id === id) { set({ activeProject: updatedProject }); @@ -328,7 +367,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ duplicateProject: async (projectId: string) => { try { - const project = await storageService.loadProject(projectId); + const project = await storageService.loadProject({ id: projectId }); if (!project) { toast.error("Project not found", { description: "Please try again", @@ -355,14 +394,14 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1; const newProject: TProject = { - ...project, // Copy all properties from the original project + ...project, id: generateUUID(), name: `(${nextNumber}) ${baseName}`, createdAt: new Date(), updatedAt: new Date(), }; - await storageService.saveProject(newProject); + await storageService.saveProject({ project: newProject }); await get().loadAllProjects(); return newProject.id; } catch (error) { @@ -386,9 +425,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); - await get().loadAllProjects(); // Refresh the list + await get().loadAllProjects(); } catch (error) { console.error("Failed to update project background:", error); toast.error("Failed to update background", { @@ -417,9 +456,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); - await get().loadAllProjects(); // Refresh the list + await get().loadAllProjects(); } catch (error) { console.error("Failed to update background type:", error); toast.error("Failed to update background", { @@ -439,9 +478,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); - await get().loadAllProjects(); // Refresh the list + await get().loadAllProjects(); } catch (error) { console.error("Failed to update project FPS:", error); toast.error("Failed to update project FPS", { @@ -462,9 +501,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); - await get().loadAllProjects(); // Refresh the list + await get().loadAllProjects(); } catch (error) { console.error("Failed to update canvas size:", error); toast.error("Failed to update canvas size", { @@ -476,12 +515,10 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ 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("-"); @@ -508,7 +545,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ return sortedProjects; }, - // Global invalid project ID tracking implementation + // Global invalid project ID tracking isInvalidProjectId: (id: string) => { const invalidIds = get().invalidProjectIds || new Set(); return invalidIds.has(id); diff --git a/apps/web/src/stores/scene-store.ts b/apps/web/src/stores/scene-store.ts new file mode 100644 index 00000000..e16185e9 --- /dev/null +++ b/apps/web/src/stores/scene-store.ts @@ -0,0 +1,219 @@ +import { create } from "zustand";
+import { Scene } from "@/types/project";
+import { useProjectStore } from "./project-store";
+import { useTimelineStore } from "./timeline-store";
+import { storageService } from "@/lib/storage/storage-service";
+import { generateUUID } from "@/lib/utils";
+
+export function getMainScene({ scenes }: { scenes: Scene[] }): Scene | null {
+ return scenes.find((scene) => scene.isMain) || null;
+}
+
+interface SceneStore {
+ // Current scene state
+ currentScene: Scene | null;
+ scenes: Scene[];
+
+ // Scene management
+ createScene: ({
+ name,
+ isMain,
+ }: {
+ name: string;
+ isMain: boolean;
+ }) => Promise<string>;
+ renameScene: ({
+ sceneId,
+ name,
+ }: {
+ sceneId: string;
+ name: string;
+ }) => Promise<void>;
+ switchToScene: ({ sceneId }: { sceneId: string }) => Promise<void>;
+
+ // Scene utilities
+ getMainScene: () => Scene | null;
+ getCurrentScene: () => Scene | null;
+
+ // Project integration
+ loadProjectScenes: ({ projectId }: { projectId: string }) => Promise<void>;
+ initializeScenes: ({
+ scenes,
+ currentSceneId,
+ }: {
+ scenes: Scene[];
+ currentSceneId?: string;
+ }) => void;
+ clearScenes: () => void;
+}
+
+export const useSceneStore = create<SceneStore>((set, get) => ({
+ currentScene: null,
+ scenes: [],
+
+ createScene: async ({ name, isMain = false }) => {
+ const { scenes } = get();
+
+ const newScene = {
+ id: generateUUID(),
+ name,
+ isMain,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+ const updatedScenes = [...scenes, newScene];
+
+ const projectStore = useProjectStore.getState();
+ const { activeProject } = projectStore;
+
+ if (!activeProject) {
+ throw new Error("No active project");
+ }
+
+ const updatedProject = {
+ ...activeProject,
+ scenes: updatedScenes,
+ updatedAt: new Date(),
+ };
+
+ try {
+ await storageService.saveProject({ project: updatedProject });
+ useProjectStore.setState({ activeProject: updatedProject });
+ set({ scenes: updatedScenes });
+ return newScene.id;
+ } catch (error) {
+ console.error("Failed to create scene:", error);
+ throw error;
+ }
+ },
+
+ renameScene: async ({ sceneId, name }: { sceneId: string; name: string }) => {
+ const { scenes } = get();
+ const updatedScenes = scenes.map((scene) =>
+ scene.id === sceneId ? { ...scene, name, updatedAt: new Date() } : scene
+ );
+
+ // Update project
+ const projectStore = useProjectStore.getState();
+ const { activeProject } = projectStore;
+
+ if (!activeProject) {
+ throw new Error("No active project");
+ }
+
+ const updatedProject = {
+ ...activeProject,
+ scenes: updatedScenes,
+ updatedAt: new Date(),
+ };
+
+ try {
+ await storageService.saveProject({ project: updatedProject });
+ useProjectStore.setState({ activeProject: updatedProject });
+ set({
+ scenes: updatedScenes,
+ currentScene: updatedScenes.find((s) => s.id === sceneId) || null,
+ });
+ } catch (error) {
+ console.error("Failed to rename scene:", error);
+ throw error;
+ }
+ },
+
+ switchToScene: async ({ sceneId }: { sceneId: string }) => {
+ const { scenes } = get();
+ const targetScene = scenes.find((s) => s.id === sceneId);
+
+ if (!targetScene) {
+ throw new Error("Scene not found");
+ }
+
+ const timelineStore = useTimelineStore.getState();
+ const projectStore = useProjectStore.getState();
+ const { activeProject } = projectStore;
+ const { currentScene } = get();
+
+ if (activeProject && currentScene) {
+ await timelineStore.saveProjectTimeline({
+ projectId: activeProject.id,
+ sceneId: currentScene.id,
+ });
+ }
+
+ if (activeProject) {
+ await timelineStore.loadProjectTimeline({
+ projectId: activeProject.id,
+ sceneId,
+ });
+
+ const updatedProject = {
+ ...activeProject,
+ currentSceneId: sceneId,
+ updatedAt: new Date(),
+ };
+
+ await storageService.saveProject({ project: updatedProject });
+ useProjectStore.setState({ activeProject: updatedProject });
+ }
+
+ set({ currentScene: targetScene });
+ },
+
+ getMainScene: () => {
+ const { scenes } = get();
+ return scenes.find((scene) => scene.isMain) || null;
+ },
+
+ getCurrentScene: () => {
+ return get().currentScene;
+ },
+
+ loadProjectScenes: async ({ projectId }: { projectId: string }) => {
+ try {
+ const project = await storageService.loadProject({ id: projectId });
+ if (project?.scenes) {
+ const ensuredScenes = project.scenes.map((scene) => ({
+ ...scene,
+ isMain: scene.isMain || false,
+ }));
+ const currentScene =
+ ensuredScenes.find((s) => s.id === project.currentSceneId) ||
+ ensuredScenes[0];
+
+ set({
+ scenes: ensuredScenes,
+ currentScene,
+ });
+ }
+ } catch (error) {
+ console.error("Failed to load project scenes:", error);
+ set({ scenes: [], currentScene: null });
+ }
+ },
+
+ initializeScenes: ({
+ scenes,
+ currentSceneId,
+ }: {
+ scenes: Scene[];
+ currentSceneId?: string;
+ }) => {
+ const currentScene = currentSceneId
+ ? scenes.find((s) => s.id === currentSceneId)
+ : null;
+
+ const fallbackScene = getMainScene({ scenes });
+
+ set({
+ scenes,
+ currentScene: currentScene || fallbackScene,
+ });
+ },
+
+ clearScenes: () => {
+ set({
+ scenes: [],
+ currentScene: null,
+ });
+ },
+}));
diff --git a/apps/web/src/stores/sounds-store.ts b/apps/web/src/stores/sounds-store.ts index 6dffa198..579eee7a 100644 --- a/apps/web/src/stores/sounds-store.ts +++ b/apps/web/src/stores/sounds-store.ts @@ -162,7 +162,7 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({ saveSoundEffect: async (soundEffect: SoundEffect) => {
try {
- await storageService.saveSoundEffect(soundEffect);
+ await storageService.saveSoundEffect({ soundEffect });
// Refresh saved sounds
const savedSoundsData = await storageService.loadSavedSounds();
@@ -178,7 +178,7 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({ removeSavedSound: async (soundId: number) => {
try {
- await storageService.removeSavedSound(soundId);
+ await storageService.removeSavedSound({ soundId });
// Update local state immediately
set((state) => ({
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index f0bae00c..8caa71a7 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -15,6 +15,7 @@ import { MediaFile } from "@/types/media"; import { findBestCanvasPreset } from "@/lib/editor-utils"; import { storageService } from "@/lib/storage/storage-service"; import { useProjectStore } from "./project-store"; +import { useSceneStore } from "./scene-store"; import { generateUUID } from "@/lib/utils"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { checkElementOverlaps, resolveElementOverlaps } from "@/lib/timeline"; @@ -178,8 +179,20 @@ interface TimelineStore { pushHistory: () => void; // Persistence actions - loadProjectTimeline: (projectId: string) => Promise<void>; - saveProjectTimeline: (projectId: string) => Promise<void>; + loadProjectTimeline: ({ + projectId, + sceneId, + }: { + projectId: string; + sceneId?: string; + }) => Promise<void>; + saveProjectTimeline: ({ + projectId, + sceneId, + }: { + projectId: string; + sceneId?: string; + }) => Promise<void>; clearTimeline: () => void; // Clipboard actions @@ -235,9 +248,15 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { // Helper to auto-save timeline changes const autoSaveTimeline = async () => { const activeProject = useProjectStore.getState().activeProject; - if (activeProject) { + const currentScene = useSceneStore.getState().currentScene; + + if (activeProject && currentScene) { try { - await storageService.saveTimeline(activeProject.id, get()._tracks); + await storageService.saveTimeline({ + projectId: activeProject.id, + tracks: get()._tracks, + sceneId: currentScene.id, + }); } catch (error) { console.error("Failed to auto-save timeline:", error); } @@ -1229,8 +1248,24 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { getProjectThumbnail: async (projectId) => { try { - const tracks = await storageService.loadTimeline(projectId); - const mediaItems = await storageService.loadAllMediaFiles(projectId); + const project = await storageService.loadProject({ id: projectId }); + if (!project) return null; + + // For scene-based projects, use main scene timeline + // For legacy projects, use legacy timeline format + let sceneId: string | undefined; + if (project.scenes && project.scenes.length > 0) { + const mainScene = project.scenes.find((s) => s.isMain); + sceneId = mainScene?.id; + } + + const tracks = await storageService.loadTimeline({ + projectId, + sceneId, + }); + const mediaItems = await storageService.loadAllMediaFiles({ + projectId, + }); if (!tracks || !mediaItems.length) return null; @@ -1330,9 +1365,19 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { }); }, - loadProjectTimeline: async (projectId) => { + loadProjectTimeline: async ({ + projectId, + sceneId, + }: { + projectId: string; + sceneId?: string; + }) => { try { - const tracks = await storageService.loadTimeline(projectId); + const tracks = await storageService.loadTimeline({ + projectId, + sceneId, + }); + if (tracks) { updateTracks(tracks); } else { @@ -1348,9 +1393,20 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { } }, - saveProjectTimeline: async (projectId) => { + saveProjectTimeline: async ({ + projectId, + sceneId, + }: { + projectId: string; + sceneId?: string; + }) => { + const { _tracks } = get(); try { - await storageService.saveTimeline(projectId, get()._tracks); + await storageService.saveTimeline({ + projectId, + tracks: _tracks, + sceneId, + }); } catch (error) { console.error("Failed to save timeline:", error); } |
