From 350b55081cde3a4332c982ea5c53b7d359029f6b Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 1 Sep 2025 16:17:06 +0200 Subject: refactor(core): switch to scene-based architecture --- apps/web/src/components/editor/scenes-view.tsx | 64 ++++++++ .../editor/timeline/timeline-toolbar.tsx | 13 +- apps/web/src/components/footer.tsx | 17 -- .../providers/migrators/scenes-migrator.tsx | 176 +++++++++++++++++++++ apps/web/src/components/ui/sheet.tsx | 12 +- 5 files changed, 257 insertions(+), 25 deletions(-) create mode 100644 apps/web/src/components/editor/scenes-view.tsx create mode 100644 apps/web/src/components/providers/migrators/scenes-migrator.tsx (limited to 'apps/web/src/components') diff --git a/apps/web/src/components/editor/scenes-view.tsx b/apps/web/src/components/editor/scenes-view.tsx new file mode 100644 index 00000000..f1df32e9 --- /dev/null +++ b/apps/web/src/components/editor/scenes-view.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { Button } from "@/components/ui/button"; +import { useSceneStore } from "@/stores/scene-store"; +import { Check } from "lucide-react"; + +export function ScenesView({ children }: { children: React.ReactNode }) { + const { scenes, currentScene, switchToScene } = useSceneStore(); + + const handleSceneSwitch = async (sceneId: string) => { + try { + await switchToScene({ sceneId }); + } catch (error) { + console.error("Failed to switch scene:", error); + } + }; + + return ( + + {children} + + + Scenes + + Switch between scenes in your project + + +
+ {scenes.length === 0 ? ( +
+ No scenes available +
+ ) : ( +
+ {scenes.map((scene) => ( + + ))} +
+ )} +
+
+
+ ); +} diff --git a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx index cc9190e2..2daf1ccc 100644 --- a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx +++ b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx @@ -1,6 +1,7 @@ import { usePlaybackStore } from "@/stores/playback-store"; import { useProjectStore } from "@/stores/project-store"; import { useTimelineStore } from "@/stores/timeline-store"; +import { useSceneStore } from "@/stores/scene-store"; import { toast } from "sonner"; import { TooltipProvider, @@ -38,6 +39,7 @@ import { DEFAULT_FPS } from "@/stores/project-store"; import { formatTimeCode } from "@/lib/time"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { EditableTimecode } from "@/components/ui/editable-timecode"; +import { ScenesView } from "../scenes-view"; export function TimelineToolbar({ zoomLevel, @@ -65,6 +67,7 @@ export function TimelineToolbar({ } = useTimelineStore(); const { currentTime, duration, isPlaying, toggle, seek } = usePlaybackStore(); const { toggleBookmark, isBookmarked, activeProject } = useProjectStore(); + const { currentScene } = useSceneStore(); const handleSplitSelected = () => { if (selectedElements.length === 0) return; @@ -357,11 +360,13 @@ export function TimelineToolbar({
- Main scene + {currentScene?.name || "No Scene"} - {}}> - - + + {}}> + + +
diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx index 0087502e..176452cd 100644 --- a/apps/web/src/components/footer.tsx +++ b/apps/web/src/components/footer.tsx @@ -2,28 +2,11 @@ import { motion } from "motion/react"; import Link from "next/link"; -import { useEffect, useState } from "react"; import { RiDiscordFill, RiTwitterXLine } from "react-icons/ri"; import { FaGithub } from "react-icons/fa6"; -import { getStars } from "@/lib/fetch-github-stars"; import Image from "next/image"; export function Footer() { - const [star, setStar] = useState(); - - useEffect(() => { - const fetchStars = async () => { - try { - const data = await getStars(); - setStar(data); - } catch (err) { - console.error("Failed to fetch GitHub stars", err); - } - }; - - fetchStars(); - }, []); - return ( ({ + current: 0, + total: 0, + currentProjectName: "", + }); + + const shouldCheckMigration = + pathname.startsWith("/editor") || pathname.startsWith("/projects"); + + useEffect(() => { + if (!shouldCheckMigration) return; + + checkAndMigrateProjects(); + }, [shouldCheckMigration]); + + const checkAndMigrateProjects = async () => { + try { + const projects = await storageService.loadAllProjects(); + const legacyProjects = projects.filter( + (project) => !project.scenes || project.scenes.length === 0 + ); + + if (legacyProjects.length === 0) { + // No migration needed + return; + } + + setIsMigrating(true); + setProgress({ + current: 0, + total: legacyProjects.length, + currentProjectName: "", + }); + + // Migrate each legacy project + for (let i = 0; i < legacyProjects.length; i++) { + const project = legacyProjects[i]; + + setProgress({ + current: i, + total: legacyProjects.length, + currentProjectName: project.name, + }); + + await migrateLegacyProject(project); + } + + setProgress({ + current: legacyProjects.length, + total: legacyProjects.length, + currentProjectName: "Complete!", + }); + + setTimeout(() => { + setIsMigrating(false); + }, 1000); + } catch (error) { + console.error("Migration failed:", error); + setIsMigrating(false); + } + }; + + const migrateLegacyProject = async (project: TProject) => { + try { + const mainScene: Scene = { + id: generateUUID(), + name: "Main Scene", + isMain: true, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const migratedProject: TProject = { + ...project, + scenes: [mainScene], + currentSceneId: mainScene.id, + updatedAt: new Date(), + }; + + // Load existing timeline data (legacy format) + const legacyTimeline = await storageService.loadTimeline({ + projectId: project.id, + }); + + await storageService.saveProject({ project: migratedProject }); + + // If timeline data, migrate it to the main scene + if (legacyTimeline && legacyTimeline.length > 0) { + await storageService.saveTimeline({ + projectId: project.id, + tracks: legacyTimeline, + sceneId: mainScene.id, + }); + } + + // Clean up legacy timeline storage + await storageService.deleteProjectTimeline({ projectId: project.id }); + } catch (error) { + console.error(`Failed to migrate project ${project.name}:`, error); + throw error; + } + }; + + if (!shouldCheckMigration) { + return children; + } + + if (isMigrating) { + const progressPercent = + progress.total > 0 ? (progress.current / progress.total) * 100 : 0; + + return ( + + e.preventDefault()} + > + + Updating Projects + + We're adding scene support to your projects. This will only take a + moment. + + + +
+
+
+ Progress + + {progress.current} of {progress.total} + +
+ +
+ + {progress.currentProjectName && ( +
+ {progress.current < progress.total + ? `Updating: ${progress.currentProjectName}` + : progress.currentProjectName} +
+ )} +
+
+
+ ); + } + + return children; +} diff --git a/apps/web/src/components/ui/sheet.tsx b/apps/web/src/components/ui/sheet.tsx index 2c5522c1..f97bd438 100644 --- a/apps/web/src/components/ui/sheet.tsx +++ b/apps/web/src/components/ui/sheet.tsx @@ -21,7 +21,7 @@ const SheetOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( { + e.preventDefault(); + e.stopPropagation(); + }} {...props} > - - + + Close {children} -- cgit v1.3.1