diff options
Diffstat (limited to 'apps/web/src/stores')
| -rw-r--r-- | apps/web/src/stores/media-store.ts | 48 | ||||
| -rw-r--r-- | apps/web/src/stores/project-store.ts | 43 | ||||
| -rw-r--r-- | apps/web/src/stores/timeline-store.ts | 123 |
3 files changed, 170 insertions, 44 deletions
diff --git a/apps/web/src/stores/media-store.ts b/apps/web/src/stores/media-store.ts index ab413b14..cb69a7e9 100644 --- a/apps/web/src/stores/media-store.ts +++ b/apps/web/src/stores/media-store.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; import { storageService } from "@/lib/storage/storage-service"; +import { useProjectStore } from "./project-store"; export type MediaType = "image" | "video" | "audio"; @@ -26,11 +27,15 @@ interface MediaStore { mediaItems: MediaItem[]; isLoading: boolean; - // Actions - addMediaItem: (item: Omit<MediaItem, "id">) => Promise<void>; - removeMediaItem: (id: string) => Promise<void>; - loadAllMedia: () => Promise<void>; - clearAllMedia: () => Promise<void>; + // 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 @@ -153,7 +158,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ mediaItems: [], isLoading: false, - addMediaItem: async (item) => { + addMediaItem: async (projectId, item) => { const newItem: MediaItem = { ...item, id: crypto.randomUUID(), @@ -166,7 +171,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ // Save to persistent storage in background try { - await storageService.saveMediaItem(newItem); + await storageService.saveMediaItem(projectId, newItem); } catch (error) { console.error("Failed to save media item:", error); // Remove from local state if save failed @@ -176,7 +181,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ } }, - removeMediaItem: async (id: string) => { + removeMediaItem: async (projectId, id: string) => { const state = get(); const item = state.mediaItems.find((media) => media.id === id); @@ -195,17 +200,17 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ // Remove from persistent storage try { - await storageService.deleteMediaItem(id); + await storageService.deleteMediaItem(projectId, id); } catch (error) { console.error("Failed to delete media item:", error); } }, - loadAllMedia: async () => { + loadProjectMedia: async (projectId) => { set({ isLoading: true }); try { - const mediaItems = await storageService.loadAllMediaItems(); + const mediaItems = await storageService.loadAllMediaItems(projectId); set({ mediaItems }); } catch (error) { console.error("Failed to load media items:", error); @@ -214,7 +219,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ } }, - clearAllMedia: async () => { + clearProjectMedia: async (projectId) => { const state = get(); // Cleanup all object URLs @@ -234,10 +239,27 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ try { const mediaIds = state.mediaItems.map((item) => item.id); await Promise.all( - mediaIds.map((id) => storageService.deleteMediaItem(id)) + mediaIds.map((id) => storageService.deleteMediaItem(projectId, id)) ); } catch (error) { console.error("Failed to clear media items from storage:", error); } }, + + 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); + } + }); + + // Clear local state + set({ mediaItems: [] }); + }, })); diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index 4fff83c2..9570f4a0 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -2,6 +2,8 @@ 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";
interface ProjectStore {
activeProject: TProject | null;
@@ -53,13 +55,28 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ 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 });
}
@@ -70,7 +87,12 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ if (!activeProject) return;
try {
- await storageService.saveProject(activeProject);
+ // 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);
@@ -94,13 +116,22 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ deleteProject: async (id: string) => {
try {
- await storageService.deleteProject(id);
+ // 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
+ // 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);
@@ -109,6 +140,12 @@ 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();
+ mediaStore.clearAllMedia();
+ timelineStore.clearTimeline();
},
renameProject: async (id: string, name: string) => {
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index c4e4e0e3..6d1a0bd1 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -6,9 +6,12 @@ import { TimelineTrack, sortTracksByOrder, ensureMainTrack, + validateElementTrackCompatibility, } from "@/types/timeline"; import { useEditorStore } from "./editor-store"; import { useMediaStore, getMediaAspectRatio } from "./media-store"; +import { storageService } from "@/lib/storage/storage-service"; +import { useProjectStore } from "./project-store"; // Helper function to manage element naming with suffixes const getElementNameWithSuffix = ( @@ -116,6 +119,11 @@ interface TimelineStore { undo: () => void; redo: () => void; pushHistory: () => void; + + // Persistence actions + loadProjectTimeline: (projectId: string) => Promise<void>; + saveProjectTimeline: (projectId: string) => Promise<void>; + clearTimeline: () => void; } export const useTimelineStore = create<TimelineStore>((set, get) => { @@ -129,6 +137,25 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { }); }; + // 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); + }; + // Initialize with proper track ordering const initialTracks = ensureMainTrack([]); const sortedInitialTracks = sortTracksByOrder(initialTracks); @@ -158,7 +185,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { const { history, redoStack, _tracks } = get(); if (history.length === 0) return; const prev = history[history.length - 1]; - updateTracks(prev); + updateTracksAndSave(prev); set({ history: history.slice(0, -1), redoStack: [...redoStack, JSON.parse(JSON.stringify(_tracks))], @@ -224,7 +251,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { muted: false, }; - updateTracks([...get()._tracks, newTrack]); + updateTracksAndSave([...get()._tracks, newTrack]); return newTrack.id; }, @@ -251,13 +278,13 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { const newTracks = [...get()._tracks]; newTracks.splice(index, 0, newTrack); - updateTracks(newTracks); + updateTracksAndSave(newTracks); return newTrack.id; }, removeTrack: (trackId) => { get().pushHistory(); - updateTracks(get()._tracks.filter((track) => track.id !== trackId)); + updateTracksAndSave(get()._tracks.filter((track) => track.id !== trackId)); }, addElementToTrack: (trackId, elementData) => { @@ -270,17 +297,10 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { return; } - // Validate element can be added to this track type - if (track.type === "media" && elementData.type !== "media") { - console.error("Media track only accepts media elements"); - return; - } - if (track.type === "text" && elementData.type !== "text") { - console.error("Text track only accepts text elements"); - return; - } - if (track.type === "audio" && elementData.type !== "media") { - console.error("Audio track only accepts media elements"); + // Use utility function for validation + const validation = validateElementTrackCompatibility(elementData, track); + if (!validation.isValid) { + console.error(validation.errorMessage); return; } @@ -331,7 +351,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { } } - updateTracks( + updateTracksAndSave( get()._tracks.map((track) => track.id === trackId ? { ...track, elements: [...track.elements, newElement] } @@ -342,7 +362,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { removeElementFromTrack: (trackId, elementId) => { get().pushHistory(); - updateTracks( + updateTracksAndSave( get() ._tracks.map((track) => track.id === trackId @@ -362,11 +382,22 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { 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 ); - if (!elementToMove) 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; + } const newTracks = get() ._tracks.map((track) => { @@ -387,12 +418,12 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { }) .filter((track) => track.elements.length > 0); - updateTracks(newTracks); + updateTracksAndSave(newTracks); }, updateElementTrim: (trackId, elementId, trimStart, trimEnd) => { get().pushHistory(); - updateTracks( + updateTracksAndSave( get()._tracks.map((track) => track.id === trackId ? { @@ -410,7 +441,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { updateElementStartTime: (trackId, elementId, startTime) => { get().pushHistory(); - updateTracks( + updateTracksAndSave( get()._tracks.map((track) => track.id === trackId ? { @@ -426,7 +457,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { toggleTrackMute: (trackId) => { get().pushHistory(); - updateTracks( + updateTracksAndSave( get()._tracks.map((track) => track.id === trackId ? { ...track, muted: !track.muted } : track ) @@ -456,7 +487,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { const secondElementId = crypto.randomUUID(); - updateTracks( + updateTracksAndSave( get()._tracks.map((track) => track.id === trackId ? { @@ -508,7 +539,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { const durationToRemove = element.duration - element.trimStart - element.trimEnd - relativeTime; - updateTracks( + updateTracksAndSave( get()._tracks.map((track) => track.id === trackId ? { @@ -547,7 +578,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { const relativeTime = splitTime - element.startTime; - updateTracks( + updateTracksAndSave( get()._tracks.map((track) => track.id === trackId ? { @@ -584,7 +615,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { if (existingAudioTrack) { // Add audio element to existing audio track - updateTracks( + updateTracksAndSave( get()._tracks.map((track) => track.id === existingAudioTrack.id ? { @@ -617,7 +648,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { muted: false, }; - updateTracks([...get()._tracks, newAudioTrack]); + updateTracksAndSave([...get()._tracks, newAudioTrack]); } return audioElementId; @@ -645,7 +676,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { const { redoStack } = get(); if (redoStack.length === 0) return; const next = redoStack[redoStack.length - 1]; - updateTracks(next); + updateTracksAndSave(next); set({ redoStack: redoStack.slice(0, -1) }); }, @@ -706,5 +737,41 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { }, }); }, + + // 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); + } + }, + + clearTimeline: () => { + const defaultTracks = ensureMainTrack([]); + updateTracks(defaultTracks); + set({ history: [], redoStack: [], selectedElements: [] }); + }, }; }); |
