diff options
| author | Maze Winther <mazewinther@gmail.com> | 2025-09-01 16:17:06 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2025-09-01 16:17:06 +0200 |
| commit | 350b55081cde3a4332c982ea5c53b7d359029f6b (patch) | |
| tree | f63b66635d2a13a0a43c622a9bea25d9b9822960 /apps/web/src | |
| parent | 2b700b7474ffc728f5318c178244e035c93716b5 (diff) | |
| download | opencut-350b55081cde3a4332c982ea5c53b7d359029f6b.tar.gz | |
refactor(core): switch to scene-based architecture
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/app/layout.tsx | 5 | ||||
| -rw-r--r-- | apps/web/src/components/editor/scenes-view.tsx | 64 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline/timeline-toolbar.tsx | 13 | ||||
| -rw-r--r-- | apps/web/src/components/footer.tsx | 17 | ||||
| -rw-r--r-- | apps/web/src/components/providers/migrators/scenes-migrator.tsx | 176 | ||||
| -rw-r--r-- | apps/web/src/components/ui/sheet.tsx | 12 | ||||
| -rw-r--r-- | apps/web/src/lib/fetch-github-stars.ts | 29 | ||||
| -rw-r--r-- | apps/web/src/lib/storage/storage-service.ts | 159 | ||||
| -rw-r--r-- | apps/web/src/lib/storage/types.ts | 20 | ||||
| -rw-r--r-- | apps/web/src/stores/media-store.ts | 8 | ||||
| -rw-r--r-- | apps/web/src/stores/project-store.ts | 145 | ||||
| -rw-r--r-- | apps/web/src/stores/scene-store.ts | 219 | ||||
| -rw-r--r-- | apps/web/src/stores/sounds-store.ts | 4 | ||||
| -rw-r--r-- | apps/web/src/stores/timeline-store.ts | 76 | ||||
| -rw-r--r-- | apps/web/src/types/project.ts | 10 |
15 files changed, 793 insertions, 164 deletions
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index ee0bdbd2..7e32ea22 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -5,6 +5,7 @@ import "./globals.css"; import { Toaster } from "../components/ui/sonner"; import { TooltipProvider } from "../components/ui/tooltip"; import { StorageProvider } from "../components/storage-provider"; +import { ScenesMigrator } from "../components/providers/migrators/scenes-migrator"; import { baseMetaData } from "./metadata"; import { defaultFont } from "../lib/font-config"; import { BotIdClient } from "botid/client"; @@ -36,7 +37,9 @@ export default function RootLayout({ <body className={`${defaultFont.className} font-sans antialiased`}> <ThemeProvider attribute="class" defaultTheme="dark"> <TooltipProvider> - <StorageProvider>{children}</StorageProvider> + <StorageProvider> + <ScenesMigrator>{children}</ScenesMigrator> + </StorageProvider> <Analytics /> <Toaster /> <Script 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 (
+ <Sheet>
+ <SheetTrigger asChild>{children}</SheetTrigger>
+ <SheetContent>
+ <SheetHeader>
+ <SheetTitle>Scenes</SheetTitle>
+ <SheetDescription>
+ Switch between scenes in your project
+ </SheetDescription>
+ </SheetHeader>
+ <div className="py-4">
+ {scenes.length === 0 ? (
+ <div className="text-sm text-muted-foreground">
+ No scenes available
+ </div>
+ ) : (
+ <div className="space-y-2">
+ {scenes.map((scene) => (
+ <Button
+ key={scene.id}
+ variant={
+ currentScene?.id === scene.id ? "default" : "outline"
+ }
+ className="w-full justify-between"
+ onClick={() => handleSceneSwitch(scene.id)}
+ >
+ <span>{scene.name}</span>
+ {currentScene?.id === scene.id && (
+ <Check className="h-4 w-4" />
+ )}
+ </Button>
+ ))}
+ </div>
+ )}
+ </div>
+ </SheetContent>
+ </Sheet>
+ );
+}
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({ </div> <div> <SplitButton> - <SplitButtonLeft>Main scene</SplitButtonLeft> + <SplitButtonLeft>{currentScene?.name || "No Scene"}</SplitButtonLeft> <SplitButtonSeparator /> - <SplitButtonRight onClick={() => {}}> - <LayersIcon /> - </SplitButtonRight> + <ScenesView> + <SplitButtonRight onClick={() => {}}> + <LayersIcon /> + </SplitButtonRight> + </ScenesView> </SplitButton> </div> <div className="flex items-center gap-1"> 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<string>(); - - useEffect(() => { - const fetchStars = async () => { - try { - const data = await getStars(); - setStar(data); - } catch (err) { - console.error("Failed to fetch GitHub stars", err); - } - }; - - fetchStars(); - }, []); - return ( <motion.footer className="bg-background border-t" diff --git a/apps/web/src/components/providers/migrators/scenes-migrator.tsx b/apps/web/src/components/providers/migrators/scenes-migrator.tsx new file mode 100644 index 00000000..77640327 --- /dev/null +++ b/apps/web/src/components/providers/migrators/scenes-migrator.tsx @@ -0,0 +1,176 @@ +"use client";
+
+import { useEffect, useState } from "react";
+import { usePathname } from "next/navigation";
+import { storageService } from "@/lib/storage/storage-service";
+import { TProject, Scene } from "@/types/project";
+import { generateUUID } from "@/lib/utils";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Progress } from "@/components/ui/progress";
+
+interface MigrationProgress {
+ current: number;
+ total: number;
+ currentProjectName: string;
+}
+
+export function ScenesMigrator({ children }: { children: React.ReactNode }) {
+ const pathname = usePathname();
+ const [isMigrating, setIsMigrating] = useState(false);
+ const [progress, setProgress] = useState<MigrationProgress>({
+ 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 (
+ <Dialog open={true}>
+ <DialogContent
+ className="sm:max-w-md"
+ onPointerDownOutside={(e) => e.preventDefault()}
+ >
+ <DialogHeader>
+ <DialogTitle>Updating Projects</DialogTitle>
+ <DialogDescription>
+ We're adding scene support to your projects. This will only take a
+ moment.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ <div className="space-y-2">
+ <div className="flex justify-between text-sm">
+ <span>Progress</span>
+ <span>
+ {progress.current} of {progress.total}
+ </span>
+ </div>
+ <Progress value={progressPercent} className="w-full" />
+ </div>
+
+ {progress.currentProjectName && (
+ <div className="text-sm text-muted-foreground">
+ {progress.current < progress.total
+ ? `Updating: ${progress.currentProjectName}`
+ : progress.currentProjectName}
+ </div>
+ )}
+ </div>
+ </DialogContent>
+ </Dialog>
+ );
+ }
+
+ 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) => ( <SheetPrimitive.Overlay className={cn( - "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", + "fixed inset-0 z-200 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", className )} {...props} @@ -31,7 +31,7 @@ const SheetOverlay = React.forwardRef< SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; const sheetVariants = cva( - "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease data-[state=closed]:duration-250 data-[state=open]:duration-250 data-[state=open]:animate-in data-[state=closed]:animate-out", + "fixed z-250 gap-4 bg-background p-6 shadow-lg transition ease data-[state=closed]:duration-250 data-[state=open]:duration-250 data-[state=open]:animate-in data-[state=closed]:animate-out", { variants: { side: { @@ -62,10 +62,14 @@ const SheetContent = React.forwardRef< <SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} + onOpenAutoFocus={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} {...props} > - <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> - <X className="h-4 w-4" /> + <SheetPrimitive.Close className="absolute cursor-pointer right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> + <X className="size-5" /> <span className="sr-only">Close</span> </SheetPrimitive.Close> {children} diff --git a/apps/web/src/lib/fetch-github-stars.ts b/apps/web/src/lib/fetch-github-stars.ts deleted file mode 100644 index 0372f25b..00000000 --- a/apps/web/src/lib/fetch-github-stars.ts +++ /dev/null @@ -1,29 +0,0 @@ -export async function getStars(): Promise<string> { - try { - const res = await fetch( - "https://api.github.com/repos/OpenCut-app/OpenCut", - { - next: { revalidate: 3600 }, - } - ); - - if (!res.ok) { - throw new Error(`GitHub API error: ${res.status} ${res.statusText}`); - } - const data = (await res.json()) as { stargazers_count: number }; - const count = data.stargazers_count; - - if (typeof count !== "number") { - throw new Error("Invalid stargazers_count from GitHub API"); - } - - if (count >= 1_000_000) - return (count / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M"; - if (count >= 1000) - return (count / 1000).toFixed(1).replace(/\.0$/, "") + "k"; - return count.toString(); - } catch (error) { - console.error("Failed to fetch GitHub stars:", error); - return "1.5k"; - } -} diff --git a/apps/web/src/lib/storage/storage-service.ts b/apps/web/src/lib/storage/storage-service.ts index 3aadf883..80f10d3d 100644 --- a/apps/web/src/lib/storage/storage-service.ts +++ b/apps/web/src/lib/storage/storage-service.ts @@ -6,6 +6,7 @@ import { MediaFileData, StorageConfig, SerializedProject, + SerializedScene, TimelineData, } from "./types"; import { TimelineTrack } from "@/types/timeline"; @@ -39,7 +40,7 @@ class StorageService { } // Helper to get project-specific media adapters - private getProjectMediaAdapters(projectId: string) { + private getProjectMediaAdapters({ projectId }: { projectId: string }) { const mediaMetadataAdapter = new IndexedDBAdapter<MediaFileData>( `${this.config.mediaDb}-${projectId}`, "media-metadata", @@ -52,23 +53,43 @@ class StorageService { } // Helper to get project-specific timeline adapter - private getProjectTimelineAdapter(projectId: string) { + private getProjectTimelineAdapter({ + projectId, + sceneId, + }: { + projectId: string; + sceneId?: string; + }) { + const dbName = sceneId + ? `${this.config.timelineDb}-${projectId}-${sceneId}` + : `${this.config.timelineDb}-${projectId}`; + return new IndexedDBAdapter<TimelineData>( - `${this.config.timelineDb}-${projectId}`, + dbName, "timeline", this.config.version ); } // Project operations - async saveProject(project: TProject): Promise<void> { + async saveProject({ project }: { project: TProject }): Promise<void> { // Convert TProject to serializable format + const serializedScenes: SerializedScene[] = project.scenes.map((scene) => ({ + id: scene.id, + name: scene.name, + isMain: scene.isMain, + createdAt: scene.createdAt.toISOString(), + updatedAt: scene.updatedAt.toISOString(), + })); + const serializedProject: SerializedProject = { id: project.id, name: project.name, thumbnail: project.thumbnail, createdAt: project.createdAt.toISOString(), updatedAt: project.updatedAt.toISOString(), + scenes: serializedScenes, + currentSceneId: project.currentSceneId, backgroundColor: project.backgroundColor, backgroundType: project.backgroundType, blurIntensity: project.blurIntensity, @@ -81,11 +102,21 @@ class StorageService { await this.projectsAdapter.set(project.id, serializedProject); } - async loadProject(id: string): Promise<TProject | null> { + async loadProject({ id }: { id: string }): Promise<TProject | null> { const serializedProject = await this.projectsAdapter.get(id); if (!serializedProject) return null; + // Now convert serialized scenes back to Scene objects + const scenes = + serializedProject.scenes?.map((scene) => ({ + id: scene.id, + name: scene.name, + isMain: scene.isMain, + createdAt: new Date(scene.createdAt), + updatedAt: new Date(scene.updatedAt), + })) || []; + // Convert back to TProject format return { id: serializedProject.id, @@ -93,6 +124,8 @@ class StorageService { thumbnail: serializedProject.thumbnail, createdAt: new Date(serializedProject.createdAt), updatedAt: new Date(serializedProject.updatedAt), + scenes, + currentSceneId: serializedProject.currentSceneId || "", backgroundColor: serializedProject.backgroundColor, backgroundType: serializedProject.backgroundType, blurIntensity: serializedProject.blurIntensity, @@ -108,7 +141,7 @@ class StorageService { const projects: TProject[] = []; for (const id of projectIds) { - const project = await this.loadProject(id); + const project = await this.loadProject({ id }); if (project) { projects.push(project); } @@ -120,14 +153,20 @@ class StorageService { ); } - async deleteProject(id: string): Promise<void> { + async deleteProject({ id }: { id: string }): Promise<void> { await this.projectsAdapter.remove(id); } // Media operations - async saveMediaFile(projectId: string, mediaItem: MediaFile): Promise<void> { + async saveMediaFile({ + projectId, + mediaItem, + }: { + projectId: string; + mediaItem: MediaFile; + }): Promise<void> { const { mediaMetadataAdapter, mediaFilesAdapter } = - this.getProjectMediaAdapters(projectId); + this.getProjectMediaAdapters({ projectId }); // Save file to project-specific OPFS await mediaFilesAdapter.set(mediaItem.id, mediaItem.file); @@ -148,12 +187,15 @@ class StorageService { await mediaMetadataAdapter.set(mediaItem.id, metadata); } - async loadMediaFile( - projectId: string, - id: string - ): Promise<MediaFile | null> { + async loadMediaFile({ + projectId, + id, + }: { + projectId: string; + id: string; + }): Promise<MediaFile | null> { const { mediaMetadataAdapter, mediaFilesAdapter } = - this.getProjectMediaAdapters(projectId); + this.getProjectMediaAdapters({ projectId }); const [file, metadata] = await Promise.all([ mediaFilesAdapter.get(id), @@ -192,14 +234,20 @@ class StorageService { }; } - async loadAllMediaFiles(projectId: string): Promise<MediaFile[]> { - const { mediaMetadataAdapter } = this.getProjectMediaAdapters(projectId); + async loadAllMediaFiles({ + projectId, + }: { + projectId: string; + }): Promise<MediaFile[]> { + const { mediaMetadataAdapter } = this.getProjectMediaAdapters({ + projectId, + }); const mediaIds = await mediaMetadataAdapter.list(); const mediaItems: MediaFile[] = []; for (const id of mediaIds) { - const item = await this.loadMediaFile(projectId, id); + const item = await this.loadMediaFile({ projectId, id }); if (item) { mediaItems.push(item); } @@ -208,9 +256,15 @@ class StorageService { return mediaItems; } - async deleteMediaFile(projectId: string, id: string): Promise<void> { + async deleteMediaFile({ + projectId, + id, + }: { + projectId: string; + id: string; + }): Promise<void> { const { mediaMetadataAdapter, mediaFilesAdapter } = - this.getProjectMediaAdapters(projectId); + this.getProjectMediaAdapters({ projectId }); await Promise.all([ mediaFilesAdapter.remove(id), @@ -218,9 +272,13 @@ class StorageService { ]); } - async deleteProjectMedia(projectId: string): Promise<void> { + async deleteProjectMedia({ + projectId, + }: { + projectId: string; + }): Promise<void> { const { mediaMetadataAdapter, mediaFilesAdapter } = - this.getProjectMediaAdapters(projectId); + this.getProjectMediaAdapters({ projectId }); await Promise.all([ mediaMetadataAdapter.clear(), @@ -228,12 +286,20 @@ class StorageService { ]); } - // Timeline operations - now project-specific - async saveTimeline( - projectId: string, - tracks: TimelineTrack[] - ): Promise<void> { - const timelineAdapter = this.getProjectTimelineAdapter(projectId); + // Timeline operations - supports both legacy and scene-based storage + async saveTimeline({ + projectId, + tracks, + sceneId, + }: { + projectId: string; + tracks: TimelineTrack[]; + sceneId?: string; + }): Promise<void> { + const timelineAdapter = this.getProjectTimelineAdapter({ + projectId, + sceneId, + }); const timelineData: TimelineData = { tracks, lastModified: new Date().toISOString(), @@ -241,14 +307,27 @@ class StorageService { await timelineAdapter.set("timeline", timelineData); } - async loadTimeline(projectId: string): Promise<TimelineTrack[] | null> { - const timelineAdapter = this.getProjectTimelineAdapter(projectId); + async loadTimeline({ + projectId, + sceneId, + }: { + projectId: string; + sceneId?: string; + }): Promise<TimelineTrack[] | null> { + const timelineAdapter = this.getProjectTimelineAdapter({ + projectId, + sceneId, + }); const timelineData = await timelineAdapter.get("timeline"); return timelineData ? timelineData.tracks : null; } - async deleteProjectTimeline(projectId: string): Promise<void> { - const timelineAdapter = this.getProjectTimelineAdapter(projectId); + async deleteProjectTimeline({ + projectId, + }: { + projectId: string; + }): Promise<void> { + const timelineAdapter = this.getProjectTimelineAdapter({ projectId }); await timelineAdapter.remove("timeline"); } @@ -274,12 +353,14 @@ class StorageService { }; } - async getProjectStorageInfo(projectId: string): Promise<{ + async getProjectStorageInfo({ projectId }: { projectId: string }): Promise<{ mediaItems: number; hasTimeline: boolean; }> { - const { mediaMetadataAdapter } = this.getProjectMediaAdapters(projectId); - const timelineAdapter = this.getProjectTimelineAdapter(projectId); + const { mediaMetadataAdapter } = this.getProjectMediaAdapters({ + projectId, + }); + const timelineAdapter = this.getProjectTimelineAdapter({ projectId }); const [mediaIds, timelineData] = await Promise.all([ mediaMetadataAdapter.list(), @@ -307,7 +388,11 @@ class StorageService { } } - async saveSoundEffect(soundEffect: SoundEffect): Promise<void> { + async saveSoundEffect({ + soundEffect, + }: { + soundEffect: SoundEffect; + }): Promise<void> { try { const currentData = await this.loadSavedSounds(); @@ -340,7 +425,7 @@ class StorageService { } } - async removeSavedSound(soundId: number): Promise<void> { + async removeSavedSound({ soundId }: { soundId: number }): Promise<void> { try { const currentData = await this.loadSavedSounds(); @@ -356,7 +441,7 @@ class StorageService { } } - async isSoundSaved(soundId: number): Promise<boolean> { + async isSoundSaved({ soundId }: { soundId: number }): Promise<boolean> { try { const currentData = await this.loadSavedSounds(); return currentData.sounds.some((sound) => sound.id === soundId); diff --git a/apps/web/src/lib/storage/types.ts b/apps/web/src/lib/storage/types.ts index 8f593867..e66239cd 100644 --- a/apps/web/src/lib/storage/types.ts +++ b/apps/web/src/lib/storage/types.ts @@ -1,4 +1,4 @@ -import { TProject } from "@/types/project"; +import { TProject, Scene } from "@/types/project"; import { TimelineTrack } from "@/types/timeline"; export interface StorageAdapter<T> { @@ -23,11 +23,23 @@ export interface MediaFileData { // File will be stored separately in OPFS } +// Legacy timeline data, kept for backward compatibility export interface TimelineData { tracks: TimelineTrack[]; lastModified: string; } +export interface SceneTimelineData { + sceneId: string; + tracks: TimelineTrack[]; + lastModified: string; +} + +export type SerializedScene = Omit<Scene, "createdAt" | "updatedAt"> & { + createdAt: string; + updatedAt: string; +}; + export interface StorageConfig { projectsDb: string; mediaDb: string; @@ -37,9 +49,13 @@ export interface StorageConfig { } // Helper type for serialization - converts Date objects to strings -export type SerializedProject = Omit<TProject, "createdAt" | "updatedAt"> & { +export type SerializedProject = Omit< + TProject, + "createdAt" | "updatedAt" | "scenes" +> & { createdAt: string; updatedAt: string; + scenes: SerializedScene[]; bookmarks?: number[]; }; diff --git a/apps/web/src/stores/media-store.ts b/apps/web/src/stores/media-store.ts index c9e29fff..e00cffd5 100644 --- a/apps/web/src/stores/media-store.ts +++ b/apps/web/src/stores/media-store.ts @@ -152,7 +152,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ // Save to persistent storage in background try { - await storageService.saveMediaFile(projectId, newItem); + await storageService.saveMediaFile({ projectId, mediaItem: newItem }); } catch (error) { console.error("Failed to save media item:", error); // Remove from local state if save failed @@ -217,7 +217,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ // 3) Remove from persistent storage try { - await storageService.deleteMediaFile(projectId, id); + await storageService.deleteMediaFile({ projectId, id }); } catch (error) { console.error("Failed to delete media item:", error); } @@ -227,7 +227,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ set({ isLoading: true }); try { - const mediaItems = await storageService.loadAllMediaFiles(projectId); + const mediaItems = await storageService.loadAllMediaFiles({ projectId }); // Regenerate thumbnails for video items const updatedMediaItems = await Promise.all( @@ -282,7 +282,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({ try { const mediaIds = state.mediaFiles.map((item) => item.id); await Promise.all( - mediaIds.map((id) => storageService.deleteMediaFile(projectId, id)) + mediaIds.map((id) => storageService.deleteMediaFile({ projectId, id })) ); } catch (error) { console.error("Failed to clear media items from storage:", error); diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index ed5eda73..d04c23d5 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -1,28 +1,44 @@ -import { TProject, BlurIntensity } from "@/types/project"; +import { TProject, BlurIntensity, Scene } 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"; +import { useSceneStore } from "./scene-store"; import { generateUUID } from "@/lib/utils"; import { CanvasSize, CanvasMode } from "@/types/editor"; export const DEFAULT_CANVAS_SIZE: CanvasSize = { width: 1920, height: 1080 }; export const DEFAULT_FPS = 30; -const DEFAULT_PROJECT: TProject = { - id: generateUUID(), - name: "Untitled", - thumbnail: "", - createdAt: new Date(), - updatedAt: new Date(), - backgroundColor: "#000000", - backgroundType: "color", - blurIntensity: 8, - bookmarks: [], - fps: DEFAULT_FPS, - canvasSize: DEFAULT_CANVAS_SIZE, - canvasMode: "preset", +function createMainScene(): Scene { + return { + id: generateUUID(), + name: "Main Scene", + isMain: true, + createdAt: new Date(), + updatedAt: new Date(), + }; +} + +const createDefaultProject = (name: string): TProject => { + const mainScene = createMainScene(); + return { + id: generateUUID(), + name, + thumbnail: "", + createdAt: new Date(), + updatedAt: new Date(), + scenes: [mainScene], + currentSceneId: mainScene.id, + backgroundColor: "#000000", + backgroundType: "color", + blurIntensity: 8, + bookmarks: [], + fps: DEFAULT_FPS, + canvasSize: DEFAULT_CANVAS_SIZE, + canvasMode: "preset", + }; }; interface ProjectStore { @@ -104,7 +120,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); await get().loadAllProjects(); // Refresh the list } catch (error) { @@ -152,7 +168,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); await get().loadAllProjects(); // Refresh the list } catch (error) { @@ -164,23 +180,24 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }, createNewProject: async (name: string) => { - const newProject: TProject = { - ...DEFAULT_PROJECT, - id: generateUUID(), - name, - createdAt: new Date(), - updatedAt: new Date(), - }; + const newProject = createDefaultProject(name); set({ activeProject: newProject }); const mediaStore = useMediaStore.getState(); const timelineStore = useTimelineStore.getState(); + const sceneStore = useSceneStore.getState(); + mediaStore.clearAllMedia(); timelineStore.clearTimeline(); + sceneStore.initializeScenes({ + scenes: newProject.scenes, + currentSceneId: newProject.currentSceneId, + }); + try { - await storageService.saveProject(newProject); + await storageService.saveProject({ project: newProject }); // Reload all projects to update the list await get().loadAllProjects(); return newProject.id; @@ -195,21 +212,35 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ set({ isLoading: true }); } - // Clear media and timeline immediately to prevent flickering when switching projects + // Prevent flicker when switching projects - clear all stores const mediaStore = useMediaStore.getState(); const timelineStore = useTimelineStore.getState(); + const sceneStore = useSceneStore.getState(); + mediaStore.clearAllMedia(); timelineStore.clearTimeline(); + sceneStore.clearScenes(); try { - const project = await storageService.loadProject(id); + const project = await storageService.loadProject({ id }); if (project) { set({ activeProject: project }); - // Load project-specific data in parallel + if (project.scenes && project.scenes.length > 0) { + sceneStore.initializeScenes({ + scenes: project.scenes, + currentSceneId: project.currentSceneId, + }); + } + + const currentScene = sceneStore.currentScene; + await Promise.all([ mediaStore.loadProjectMedia(id), - timelineStore.loadProjectTimeline(id), + timelineStore.loadProjectTimeline({ + projectId: id, + sceneId: currentScene?.id, + }), ]); } else { throw new Error(`Project with id ${id} not found`); @@ -227,11 +258,16 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ if (!activeProject) return; try { - // Save project metadata and timeline data in parallel const timelineStore = useTimelineStore.getState(); + const sceneStore = useSceneStore.getState(); + const currentScene = sceneStore.currentScene; + await Promise.all([ - storageService.saveProject(activeProject), - timelineStore.saveProjectTimeline(activeProject.id), + storageService.saveProject({ project: activeProject }), + timelineStore.saveProjectTimeline({ + projectId: activeProject.id, + sceneId: currentScene?.id, + }), ]); await get().loadAllProjects(); // Refresh the list } catch (error) { @@ -256,22 +292,24 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ deleteProject: async (id: string) => { try { - // Delete project data in parallel await Promise.all([ - storageService.deleteProjectMedia(id), - storageService.deleteProjectTimeline(id), - storageService.deleteProject(id), + storageService.deleteProjectMedia({ projectId: id }), + storageService.deleteProjectTimeline({ projectId: id }), + storageService.deleteProject({ id }), ]); await get().loadAllProjects(); // Refresh the list - // If we deleted the active project, close it and clear data + // If deleted 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(); + const sceneStore = useSceneStore.getState(); + mediaStore.clearAllMedia(); timelineStore.clearTimeline(); + sceneStore.clearScenes(); } } catch (error) { console.error("Failed to delete project:", error); @@ -281,11 +319,13 @@ 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(); + const sceneStore = useSceneStore.getState(); + mediaStore.clearAllMedia(); timelineStore.clearTimeline(); + sceneStore.clearScenes(); }, renameProject: async (id: string, name: string) => { @@ -307,12 +347,11 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - // Save to storage - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); await get().loadAllProjects(); - // Update activeProject if it's the same project + // Update activeProject if same project const { activeProject } = get(); if (activeProject?.id === id) { set({ activeProject: updatedProject }); @@ -328,7 +367,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ duplicateProject: async (projectId: string) => { try { - const project = await storageService.loadProject(projectId); + const project = await storageService.loadProject({ id: projectId }); if (!project) { toast.error("Project not found", { description: "Please try again", @@ -355,14 +394,14 @@ 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 + ...project, id: generateUUID(), name: `(${nextNumber}) ${baseName}`, createdAt: new Date(), updatedAt: new Date(), }; - await storageService.saveProject(newProject); + await storageService.saveProject({ project: newProject }); await get().loadAllProjects(); return newProject.id; } catch (error) { @@ -386,9 +425,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); - await get().loadAllProjects(); // Refresh the list + await get().loadAllProjects(); } catch (error) { console.error("Failed to update project background:", error); toast.error("Failed to update background", { @@ -417,9 +456,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); - await get().loadAllProjects(); // Refresh the list + await get().loadAllProjects(); } catch (error) { console.error("Failed to update background type:", error); toast.error("Failed to update background", { @@ -439,9 +478,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); - await get().loadAllProjects(); // Refresh the list + await get().loadAllProjects(); } catch (error) { console.error("Failed to update project FPS:", error); toast.error("Failed to update project FPS", { @@ -462,9 +501,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ }; try { - await storageService.saveProject(updatedProject); + await storageService.saveProject({ project: updatedProject }); set({ activeProject: updatedProject }); - await get().loadAllProjects(); // Refresh the list + await get().loadAllProjects(); } catch (error) { console.error("Failed to update canvas size:", error); toast.error("Failed to update canvas size", { @@ -476,12 +515,10 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ getFilteredAndSortedProjects: (searchQuery: string, sortOption: string) => { const { savedProjects } = get(); - // Filter projects by search query const filteredProjects = savedProjects.filter((project) => project.name.toLowerCase().includes(searchQuery.toLowerCase()) ); - // Sort filtered projects const sortedProjects = [...filteredProjects].sort((a, b) => { const [key, order] = sortOption.split("-"); @@ -508,7 +545,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ return sortedProjects; }, - // Global invalid project ID tracking implementation + // Global invalid project ID tracking isInvalidProjectId: (id: string) => { const invalidIds = get().invalidProjectIds || new Set(); return invalidIds.has(id); diff --git a/apps/web/src/stores/scene-store.ts b/apps/web/src/stores/scene-store.ts new file mode 100644 index 00000000..e16185e9 --- /dev/null +++ b/apps/web/src/stores/scene-store.ts @@ -0,0 +1,219 @@ +import { create } from "zustand";
+import { Scene } from "@/types/project";
+import { useProjectStore } from "./project-store";
+import { useTimelineStore } from "./timeline-store";
+import { storageService } from "@/lib/storage/storage-service";
+import { generateUUID } from "@/lib/utils";
+
+export function getMainScene({ scenes }: { scenes: Scene[] }): Scene | null {
+ return scenes.find((scene) => scene.isMain) || null;
+}
+
+interface SceneStore {
+ // Current scene state
+ currentScene: Scene | null;
+ scenes: Scene[];
+
+ // Scene management
+ createScene: ({
+ name,
+ isMain,
+ }: {
+ name: string;
+ isMain: boolean;
+ }) => Promise<string>;
+ renameScene: ({
+ sceneId,
+ name,
+ }: {
+ sceneId: string;
+ name: string;
+ }) => Promise<void>;
+ switchToScene: ({ sceneId }: { sceneId: string }) => Promise<void>;
+
+ // Scene utilities
+ getMainScene: () => Scene | null;
+ getCurrentScene: () => Scene | null;
+
+ // Project integration
+ loadProjectScenes: ({ projectId }: { projectId: string }) => Promise<void>;
+ initializeScenes: ({
+ scenes,
+ currentSceneId,
+ }: {
+ scenes: Scene[];
+ currentSceneId?: string;
+ }) => void;
+ clearScenes: () => void;
+}
+
+export const useSceneStore = create<SceneStore>((set, get) => ({
+ currentScene: null,
+ scenes: [],
+
+ createScene: async ({ name, isMain = false }) => {
+ const { scenes } = get();
+
+ const newScene = {
+ id: generateUUID(),
+ name,
+ isMain,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+ const updatedScenes = [...scenes, newScene];
+
+ const projectStore = useProjectStore.getState();
+ const { activeProject } = projectStore;
+
+ if (!activeProject) {
+ throw new Error("No active project");
+ }
+
+ const updatedProject = {
+ ...activeProject,
+ scenes: updatedScenes,
+ updatedAt: new Date(),
+ };
+
+ try {
+ await storageService.saveProject({ project: updatedProject });
+ useProjectStore.setState({ activeProject: updatedProject });
+ set({ scenes: updatedScenes });
+ return newScene.id;
+ } catch (error) {
+ console.error("Failed to create scene:", error);
+ throw error;
+ }
+ },
+
+ renameScene: async ({ sceneId, name }: { sceneId: string; name: string }) => {
+ const { scenes } = get();
+ const updatedScenes = scenes.map((scene) =>
+ scene.id === sceneId ? { ...scene, name, updatedAt: new Date() } : scene
+ );
+
+ // Update project
+ const projectStore = useProjectStore.getState();
+ const { activeProject } = projectStore;
+
+ if (!activeProject) {
+ throw new Error("No active project");
+ }
+
+ const updatedProject = {
+ ...activeProject,
+ scenes: updatedScenes,
+ updatedAt: new Date(),
+ };
+
+ try {
+ await storageService.saveProject({ project: updatedProject });
+ useProjectStore.setState({ activeProject: updatedProject });
+ set({
+ scenes: updatedScenes,
+ currentScene: updatedScenes.find((s) => s.id === sceneId) || null,
+ });
+ } catch (error) {
+ console.error("Failed to rename scene:", error);
+ throw error;
+ }
+ },
+
+ switchToScene: async ({ sceneId }: { sceneId: string }) => {
+ const { scenes } = get();
+ const targetScene = scenes.find((s) => s.id === sceneId);
+
+ if (!targetScene) {
+ throw new Error("Scene not found");
+ }
+
+ const timelineStore = useTimelineStore.getState();
+ const projectStore = useProjectStore.getState();
+ const { activeProject } = projectStore;
+ const { currentScene } = get();
+
+ if (activeProject && currentScene) {
+ await timelineStore.saveProjectTimeline({
+ projectId: activeProject.id,
+ sceneId: currentScene.id,
+ });
+ }
+
+ if (activeProject) {
+ await timelineStore.loadProjectTimeline({
+ projectId: activeProject.id,
+ sceneId,
+ });
+
+ const updatedProject = {
+ ...activeProject,
+ currentSceneId: sceneId,
+ updatedAt: new Date(),
+ };
+
+ await storageService.saveProject({ project: updatedProject });
+ useProjectStore.setState({ activeProject: updatedProject });
+ }
+
+ set({ currentScene: targetScene });
+ },
+
+ getMainScene: () => {
+ const { scenes } = get();
+ return scenes.find((scene) => scene.isMain) || null;
+ },
+
+ getCurrentScene: () => {
+ return get().currentScene;
+ },
+
+ loadProjectScenes: async ({ projectId }: { projectId: string }) => {
+ try {
+ const project = await storageService.loadProject({ id: projectId });
+ if (project?.scenes) {
+ const ensuredScenes = project.scenes.map((scene) => ({
+ ...scene,
+ isMain: scene.isMain || false,
+ }));
+ const currentScene =
+ ensuredScenes.find((s) => s.id === project.currentSceneId) ||
+ ensuredScenes[0];
+
+ set({
+ scenes: ensuredScenes,
+ currentScene,
+ });
+ }
+ } catch (error) {
+ console.error("Failed to load project scenes:", error);
+ set({ scenes: [], currentScene: null });
+ }
+ },
+
+ initializeScenes: ({
+ scenes,
+ currentSceneId,
+ }: {
+ scenes: Scene[];
+ currentSceneId?: string;
+ }) => {
+ const currentScene = currentSceneId
+ ? scenes.find((s) => s.id === currentSceneId)
+ : null;
+
+ const fallbackScene = getMainScene({ scenes });
+
+ set({
+ scenes,
+ currentScene: currentScene || fallbackScene,
+ });
+ },
+
+ clearScenes: () => {
+ set({
+ scenes: [],
+ currentScene: null,
+ });
+ },
+}));
diff --git a/apps/web/src/stores/sounds-store.ts b/apps/web/src/stores/sounds-store.ts index 6dffa198..579eee7a 100644 --- a/apps/web/src/stores/sounds-store.ts +++ b/apps/web/src/stores/sounds-store.ts @@ -162,7 +162,7 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({ saveSoundEffect: async (soundEffect: SoundEffect) => {
try {
- await storageService.saveSoundEffect(soundEffect);
+ await storageService.saveSoundEffect({ soundEffect });
// Refresh saved sounds
const savedSoundsData = await storageService.loadSavedSounds();
@@ -178,7 +178,7 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({ removeSavedSound: async (soundId: number) => {
try {
- await storageService.removeSavedSound(soundId);
+ await storageService.removeSavedSound({ soundId });
// Update local state immediately
set((state) => ({
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index f0bae00c..8caa71a7 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -15,6 +15,7 @@ import { MediaFile } from "@/types/media"; import { findBestCanvasPreset } from "@/lib/editor-utils"; import { storageService } from "@/lib/storage/storage-service"; import { useProjectStore } from "./project-store"; +import { useSceneStore } from "./scene-store"; import { generateUUID } from "@/lib/utils"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { checkElementOverlaps, resolveElementOverlaps } from "@/lib/timeline"; @@ -178,8 +179,20 @@ interface TimelineStore { pushHistory: () => void; // Persistence actions - loadProjectTimeline: (projectId: string) => Promise<void>; - saveProjectTimeline: (projectId: string) => Promise<void>; + loadProjectTimeline: ({ + projectId, + sceneId, + }: { + projectId: string; + sceneId?: string; + }) => Promise<void>; + saveProjectTimeline: ({ + projectId, + sceneId, + }: { + projectId: string; + sceneId?: string; + }) => Promise<void>; clearTimeline: () => void; // Clipboard actions @@ -235,9 +248,15 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { // Helper to auto-save timeline changes const autoSaveTimeline = async () => { const activeProject = useProjectStore.getState().activeProject; - if (activeProject) { + const currentScene = useSceneStore.getState().currentScene; + + if (activeProject && currentScene) { try { - await storageService.saveTimeline(activeProject.id, get()._tracks); + await storageService.saveTimeline({ + projectId: activeProject.id, + tracks: get()._tracks, + sceneId: currentScene.id, + }); } catch (error) { console.error("Failed to auto-save timeline:", error); } @@ -1229,8 +1248,24 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { getProjectThumbnail: async (projectId) => { try { - const tracks = await storageService.loadTimeline(projectId); - const mediaItems = await storageService.loadAllMediaFiles(projectId); + const project = await storageService.loadProject({ id: projectId }); + if (!project) return null; + + // For scene-based projects, use main scene timeline + // For legacy projects, use legacy timeline format + let sceneId: string | undefined; + if (project.scenes && project.scenes.length > 0) { + const mainScene = project.scenes.find((s) => s.isMain); + sceneId = mainScene?.id; + } + + const tracks = await storageService.loadTimeline({ + projectId, + sceneId, + }); + const mediaItems = await storageService.loadAllMediaFiles({ + projectId, + }); if (!tracks || !mediaItems.length) return null; @@ -1330,9 +1365,19 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { }); }, - loadProjectTimeline: async (projectId) => { + loadProjectTimeline: async ({ + projectId, + sceneId, + }: { + projectId: string; + sceneId?: string; + }) => { try { - const tracks = await storageService.loadTimeline(projectId); + const tracks = await storageService.loadTimeline({ + projectId, + sceneId, + }); + if (tracks) { updateTracks(tracks); } else { @@ -1348,9 +1393,20 @@ export const useTimelineStore = create<TimelineStore>((set, get) => { } }, - saveProjectTimeline: async (projectId) => { + saveProjectTimeline: async ({ + projectId, + sceneId, + }: { + projectId: string; + sceneId?: string; + }) => { + const { _tracks } = get(); try { - await storageService.saveTimeline(projectId, get()._tracks); + await storageService.saveTimeline({ + projectId, + tracks: _tracks, + sceneId, + }); } catch (error) { console.error("Failed to save timeline:", error); } diff --git a/apps/web/src/types/project.ts b/apps/web/src/types/project.ts index 66de858a..f182653d 100644 --- a/apps/web/src/types/project.ts +++ b/apps/web/src/types/project.ts @@ -2,12 +2,22 @@ import { CanvasSize } from "./editor"; export type BlurIntensity = 4 | 8 | 18; +export interface Scene { + id: string; + name: string; + isMain: boolean; + createdAt: Date; + updatedAt: Date; +} + export interface TProject { id: string; name: string; thumbnail: string; createdAt: Date; updatedAt: Date; + scenes: Scene[]; + currentSceneId: string; mediaItems?: string[]; backgroundColor?: string; backgroundType?: "color" | "blur"; |
