diff options
Diffstat (limited to 'apps/web/src/stores')
| -rw-r--r-- | apps/web/src/stores/playback-store.ts | 20 | ||||
| -rw-r--r-- | apps/web/src/stores/project-store.ts | 126 | ||||
| -rw-r--r-- | apps/web/src/stores/timeline-store.ts | 19 |
3 files changed, 159 insertions, 6 deletions
diff --git a/apps/web/src/stores/playback-store.ts b/apps/web/src/stores/playback-store.ts index 70ff490d..feea0f46 100644 --- a/apps/web/src/stores/playback-store.ts +++ b/apps/web/src/stores/playback-store.ts @@ -1,5 +1,7 @@ import { create } from "zustand"; import type { PlaybackState, PlaybackControls } from "@/types/playback"; +import { useTimelineStore } from "@/stores/timeline-store"; +import { useProjectStore } from "./project-store"; interface PlaybackStore extends PlaybackState, PlaybackControls { setDuration: (duration: number) => void; @@ -22,7 +24,6 @@ const startTimer = (store: () => PlaybackStore) => { const newTime = state.currentTime + delta * state.speed; // Get actual content duration from timeline store - const { useTimelineStore } = require("@/stores/timeline-store"); const actualContentDuration = useTimelineStore .getState() .getTotalDuration(); @@ -33,12 +34,21 @@ const startTimer = (store: () => PlaybackStore) => { actualContentDuration > 0 ? actualContentDuration : state.duration; if (newTime >= effectiveDuration) { - // When content completes, pause and reset playhead to start + // When content completes, pause just before the end so we can see the last frame + const projectFps = useProjectStore.getState().activeProject?.fps; + if (!projectFps) + console.error("Project FPS is not set, assuming 30fps"); + + const frameOffset = 1 / (projectFps ?? 30); // Stop 1 frame before end based on project FPS + const stopTime = Math.max(0, effectiveDuration - frameOffset); + state.pause(); - state.setCurrentTime(0); - // Notify video elements to sync with reset + state.setCurrentTime(stopTime); + // Notify video elements to sync with end position window.dispatchEvent( - new CustomEvent("playback-seek", { detail: { time: 0 } }) + new CustomEvent("playback-seek", { + detail: { time: stopTime }, + }) ); } else { state.setCurrentTime(newTime); diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index 7bb38039..5c617dff 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -11,6 +11,7 @@ interface ProjectStore { savedProjects: TProject[]; isLoading: boolean; isInitialized: boolean; + invalidProjectIds?: Set<string>; // Actions createNewProject: (name: string) => Promise<string>; @@ -28,10 +29,20 @@ interface ProjectStore { ) => Promise<void>; updateProjectFps: (fps: number) => Promise<void>; + // Bookmark methods + toggleBookmark: (time: number) => Promise<void>; + isBookmarked: (time: number) => boolean; + removeBookmark: (time: number) => Promise<void>; + getFilteredAndSortedProjects: ( searchQuery: string, sortOption: string ) => TProject[]; + + // Global invalid project ID tracking + isInvalidProjectId: (id: string) => boolean; + markProjectIdAsInvalid: (id: string) => void; + clearInvalidProjectIds: () => void; } export const useProjectStore = create<ProjectStore>((set, get) => ({ @@ -39,6 +50,98 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ savedProjects: [], isLoading: true, isInitialized: false, + invalidProjectIds: new Set<string>(), + + // Implementation of bookmark methods + toggleBookmark: async (time: number) => { + const { activeProject } = get(); + if (!activeProject) return; + + // Round time to the nearest frame + const fps = activeProject.fps || 30; + const frameTime = Math.round(time * fps) / fps; + + const bookmarks = activeProject.bookmarks || []; + let updatedBookmarks: number[]; + + // Check if already bookmarked + const bookmarkIndex = bookmarks.findIndex( + (bookmark) => Math.abs(bookmark - frameTime) < 0.001 + ); + + if (bookmarkIndex !== -1) { + // Remove bookmark + updatedBookmarks = bookmarks.filter((_, i) => i !== bookmarkIndex); + } else { + // Add bookmark + updatedBookmarks = [...bookmarks, frameTime].sort((a, b) => a - b); + } + + const updatedProject = { + ...activeProject, + bookmarks: updatedBookmarks, + 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 bookmarks:", error); + toast.error("Failed to update bookmarks", { + description: "Please try again", + }); + } + }, + + isBookmarked: (time: number) => { + const { activeProject } = get(); + if (!activeProject || !activeProject.bookmarks) return false; + + // Round time to the nearest frame + const fps = activeProject.fps || 30; + const frameTime = Math.round(time * fps) / fps; + + return activeProject.bookmarks.some( + (bookmark) => Math.abs(bookmark - frameTime) < 0.001 + ); + }, + + removeBookmark: async (time: number) => { + const { activeProject } = get(); + if (!activeProject || !activeProject.bookmarks) return; + + // Round time to the nearest frame + const fps = activeProject.fps || 30; + const frameTime = Math.round(time * fps) / fps; + + const updatedBookmarks = activeProject.bookmarks.filter( + (bookmark) => Math.abs(bookmark - frameTime) >= 0.001 + ); + + if (updatedBookmarks.length === activeProject.bookmarks.length) { + // No bookmark found to remove + return; + } + + const updatedProject = { + ...activeProject, + bookmarks: updatedBookmarks, + 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 bookmarks:", error); + toast.error("Failed to remove bookmark", { + description: "Please try again", + }); + } + }, createNewProject: async (name: string) => { const newProject: TProject = { @@ -50,6 +153,8 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ backgroundColor: "#000000", backgroundType: "color", blurIntensity: 8, + bookmarks: [], + fps: 30, }; set({ activeProject: newProject }); @@ -230,9 +335,9 @@ 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 id: generateUUID(), name: `(${nextNumber}) ${baseName}`, - thumbnail: project.thumbnail, createdAt: new Date(), updatedAt: new Date(), }; @@ -357,4 +462,23 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ return sortedProjects; }, + + // Global invalid project ID tracking implementation + 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() }); + }, })); diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index f3497be9..0f441176 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -126,6 +126,7 @@ interface TimelineStore { pushHistory?: boolean ) => void; toggleTrackMute: (trackId: string) => void; + toggleElementHidden: (trackId: string, elementId: string) => void; // Split operations for elements splitElement: ( @@ -868,6 +869,24 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { ); }, + toggleElementHidden: (trackId, elementId) => { + get().pushHistory(); + updateTracksAndSave( + get()._tracks.map((track) => + track.id === trackId + ? { + ...track, + elements: track.elements.map((element) => + element.id === elementId + ? { ...element, hidden: !element.hidden } + : element + ), + } + : track + ) + ); + }, + updateTextElement: (trackId, elementId, updates) => { get().pushHistory(); updateTracksAndSave( |
