summaryrefslogtreecommitdiff
path: root/apps/web/src/stores
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/stores')
-rw-r--r--apps/web/src/stores/media-store.ts48
-rw-r--r--apps/web/src/stores/project-store.ts43
-rw-r--r--apps/web/src/stores/timeline-store.ts123
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: [] });
+ },
};
});