From f255ccb818358b90edd0c1bc1f3a690dc1e729ac Mon Sep 17 00:00:00 2001 From: melad Date: Thu, 31 Jul 2025 13:50:22 +0300 Subject: feat: add hide or show for media elements, mute or unmute for audio elements (#500) * feat: create external tools section (#493) * feat: create external tools section * fix: better wording * Feature, added hide or show for media elements, mute or unmute for audio elements both effective on timeline and preview panel, with overlay icon for indicating so and state storing, * Minor UI vertical separator between magnifier and other buttons * fixed the AbortError issue in the AudioWaveform component. The error was occurring because of a race condition during cleanup when the component unmounts. * feat: implement bookmarking functionality in the timeline and project storage - Added bookmark management methods in the project store for toggling and checking bookmarks. - Updated the timeline component to display bookmark markers and integrate bookmark actions in the context menu. - Enhanced the storage service to handle bookmarks in project serialization and deserialization. - Updated project types to include bookmarks as an array of numbers. --------- Co-authored-by: Dominik K. Co-authored-by: Maze Winther --- apps/web/src/stores/project-store.ts | 97 ++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) (limited to 'apps/web/src/stores/project-store.ts') diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index 7bb38039..69aca3ec 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -27,6 +27,11 @@ interface ProjectStore { options?: { backgroundColor?: string; blurIntensity?: number } ) => Promise; updateProjectFps: (fps: number) => Promise; + + // Bookmark methods + toggleBookmark: (time: number) => Promise; + isBookmarked: (time: number) => boolean; + removeBookmark: (time: number) => Promise; getFilteredAndSortedProjects: ( searchQuery: string, @@ -39,6 +44,97 @@ export const useProjectStore = create((set, get) => ({ savedProjects: [], isLoading: true, isInitialized: false, + + // 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 +146,7 @@ export const useProjectStore = create((set, get) => ({ backgroundColor: "#000000", backgroundType: "color", blurIntensity: 8, + bookmarks: [], }; set({ activeProject: newProject }); -- cgit v1.3.1 From 5f4f3cc6c420d5fe37bf8f8dffa9368e75b9b60e Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Thu, 31 Jul 2025 13:10:37 +0200 Subject: Merge pull request [#499](https://github.com/mazeincoding/AppCut/issues/499) --- apps/web/src/app/editor/[project_id]/page.tsx | 98 ++++++++++++++++++++++++--- apps/web/src/stores/project-store.ts | 26 +++++++ 2 files changed, 116 insertions(+), 8 deletions(-) (limited to 'apps/web/src/stores/project-store.ts') diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx index 26fdcf8a..5e294fe7 100644 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ b/apps/web/src/app/editor/[project_id]/page.tsx @@ -32,39 +32,121 @@ export default function Editor() { setPropertiesPanel, } = usePanelStore(); - const { activeProject, loadProject, createNewProject } = useProjectStore(); + const { + activeProject, + loadProject, + createNewProject, + isInvalidProjectId, + markProjectIdAsInvalid, + } = useProjectStore(); const params = useParams(); const router = useRouter(); const projectId = params.project_id as string; const handledProjectIds = useRef>(new Set()); + const isInitializingRef = useRef(false); usePlaybackControls(); useEffect(() => { + let isCancelled = false; + const initProject = async () => { - if (!projectId) return; + if (!projectId) { + return; + } + // Prevent duplicate initialization + if (isInitializingRef.current) { + return; + } + + // Check if project is already loaded if (activeProject?.id === projectId) { return; } + // Check global invalid tracking first (most important for preventing duplicates) + if (isInvalidProjectId(projectId)) { + return; + } + + // Check if we've already handled this project ID locally if (handledProjectIds.current.has(projectId)) { return; } + // Mark as initializing to prevent race conditions + isInitializingRef.current = true; + handledProjectIds.current.add(projectId); + try { await loadProject(projectId); - } catch (error) { - handledProjectIds.current.add(projectId); - const newProjectId = await createNewProject("Untitled Project"); - router.replace(`/editor/${newProjectId}`); - return; + // Check if component was unmounted during async operation + if (isCancelled) { + return; + } + + // Project loaded successfully + isInitializingRef.current = false; + } catch (error) { + // Check if component was unmounted during async operation + if (isCancelled) { + return; + } + + // More specific error handling - only create new project for actual "not found" errors + const isProjectNotFound = + error instanceof Error && + (error.message.includes("not found") || + error.message.includes("does not exist") || + error.message.includes("Project not found")); + + if (isProjectNotFound) { + // Mark this project ID as invalid globally BEFORE creating project + markProjectIdAsInvalid(projectId); + + try { + const newProjectId = await createNewProject("Untitled Project"); + + // Check again if component was unmounted + if (isCancelled) { + return; + } + + router.replace(`/editor/${newProjectId}`); + } catch (createError) { + console.error("Failed to create new project:", createError); + } + } else { + // For other errors (storage issues, corruption, etc.), don't create new project + console.error( + "Project loading failed with recoverable error:", + error + ); + // Remove from handled set so user can retry + handledProjectIds.current.delete(projectId); + } + + isInitializingRef.current = false; } }; initProject(); - }, [projectId, activeProject?.id, loadProject, createNewProject, router]); + + // Cleanup function to cancel async operations + return () => { + isCancelled = true; + isInitializingRef.current = false; + }; + }, [ + projectId, + loadProject, + createNewProject, + router, + isInvalidProjectId, + markProjectIdAsInvalid, + ]); return ( diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index 7bb38039..6744bdec 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; // Actions createNewProject: (name: string) => Promise; @@ -32,6 +33,11 @@ interface ProjectStore { searchQuery: string, sortOption: string ) => TProject[]; + + // Global invalid project ID tracking + isInvalidProjectId: (id: string) => boolean; + markProjectIdAsInvalid: (id: string) => void; + clearInvalidProjectIds: () => void; } export const useProjectStore = create((set, get) => ({ @@ -39,6 +45,7 @@ export const useProjectStore = create((set, get) => ({ savedProjects: [], isLoading: true, isInitialized: false, + invalidProjectIds: new Set(), createNewProject: async (name: string) => { const newProject: TProject = { @@ -357,4 +364,23 @@ export const useProjectStore = create((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() }); + }, })); -- cgit v1.3.1 From 1603dd0e58c84c3469e8c6fecd33bca618b57e47 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Thu, 31 Jul 2025 21:47:02 +0200 Subject: fix: set default fps on project creation --- apps/web/src/stores/project-store.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'apps/web/src/stores/project-store.ts') diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index bfabba1f..5c617dff 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -154,6 +154,7 @@ export const useProjectStore = create((set, get) => ({ backgroundType: "color", blurIntensity: 8, bookmarks: [], + fps: 30, }; set({ activeProject: newProject }); @@ -334,9 +335,9 @@ export const useProjectStore = create((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(), }; -- cgit v1.3.1