mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor(core): switch to scene-based architecture
This commit is contained in:
@@ -5,6 +5,7 @@ import "./globals.css";
|
|||||||
import { Toaster } from "../components/ui/sonner";
|
import { Toaster } from "../components/ui/sonner";
|
||||||
import { TooltipProvider } from "../components/ui/tooltip";
|
import { TooltipProvider } from "../components/ui/tooltip";
|
||||||
import { StorageProvider } from "../components/storage-provider";
|
import { StorageProvider } from "../components/storage-provider";
|
||||||
|
import { ScenesMigrator } from "../components/providers/migrators/scenes-migrator";
|
||||||
import { baseMetaData } from "./metadata";
|
import { baseMetaData } from "./metadata";
|
||||||
import { defaultFont } from "../lib/font-config";
|
import { defaultFont } from "../lib/font-config";
|
||||||
import { BotIdClient } from "botid/client";
|
import { BotIdClient } from "botid/client";
|
||||||
@@ -36,7 +37,9 @@ export default function RootLayout({
|
|||||||
<body className={`${defaultFont.className} font-sans antialiased`}>
|
<body className={`${defaultFont.className} font-sans antialiased`}>
|
||||||
<ThemeProvider attribute="class" defaultTheme="dark">
|
<ThemeProvider attribute="class" defaultTheme="dark">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<StorageProvider>{children}</StorageProvider>
|
<StorageProvider>
|
||||||
|
<ScenesMigrator>{children}</ScenesMigrator>
|
||||||
|
</StorageProvider>
|
||||||
<Analytics />
|
<Analytics />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Script
|
<Script
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { usePlaybackStore } from "@/stores/playback-store";
|
import { usePlaybackStore } from "@/stores/playback-store";
|
||||||
import { useProjectStore } from "@/stores/project-store";
|
import { useProjectStore } from "@/stores/project-store";
|
||||||
import { useTimelineStore } from "@/stores/timeline-store";
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
|
import { useSceneStore } from "@/stores/scene-store";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
@@ -38,6 +39,7 @@ import { DEFAULT_FPS } from "@/stores/project-store";
|
|||||||
import { formatTimeCode } from "@/lib/time";
|
import { formatTimeCode } from "@/lib/time";
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
import { EditableTimecode } from "@/components/ui/editable-timecode";
|
import { EditableTimecode } from "@/components/ui/editable-timecode";
|
||||||
|
import { ScenesView } from "../scenes-view";
|
||||||
|
|
||||||
export function TimelineToolbar({
|
export function TimelineToolbar({
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
@@ -65,6 +67,7 @@ export function TimelineToolbar({
|
|||||||
} = useTimelineStore();
|
} = useTimelineStore();
|
||||||
const { currentTime, duration, isPlaying, toggle, seek } = usePlaybackStore();
|
const { currentTime, duration, isPlaying, toggle, seek } = usePlaybackStore();
|
||||||
const { toggleBookmark, isBookmarked, activeProject } = useProjectStore();
|
const { toggleBookmark, isBookmarked, activeProject } = useProjectStore();
|
||||||
|
const { currentScene } = useSceneStore();
|
||||||
|
|
||||||
const handleSplitSelected = () => {
|
const handleSplitSelected = () => {
|
||||||
if (selectedElements.length === 0) return;
|
if (selectedElements.length === 0) return;
|
||||||
@@ -357,11 +360,13 @@ export function TimelineToolbar({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<SplitButton>
|
<SplitButton>
|
||||||
<SplitButtonLeft>Main scene</SplitButtonLeft>
|
<SplitButtonLeft>{currentScene?.name || "No Scene"}</SplitButtonLeft>
|
||||||
<SplitButtonSeparator />
|
<SplitButtonSeparator />
|
||||||
|
<ScenesView>
|
||||||
<SplitButtonRight onClick={() => {}}>
|
<SplitButtonRight onClick={() => {}}>
|
||||||
<LayersIcon />
|
<LayersIcon />
|
||||||
</SplitButtonRight>
|
</SplitButtonRight>
|
||||||
|
</ScenesView>
|
||||||
</SplitButton>
|
</SplitButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
|||||||
@@ -2,28 +2,11 @@
|
|||||||
|
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { RiDiscordFill, RiTwitterXLine } from "react-icons/ri";
|
import { RiDiscordFill, RiTwitterXLine } from "react-icons/ri";
|
||||||
import { FaGithub } from "react-icons/fa6";
|
import { FaGithub } from "react-icons/fa6";
|
||||||
import { getStars } from "@/lib/fetch-github-stars";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
export function Footer() {
|
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 (
|
return (
|
||||||
<motion.footer
|
<motion.footer
|
||||||
className="bg-background border-t"
|
className="bg-background border-t"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ const SheetOverlay = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -31,7 +31,7 @@ const SheetOverlay = React.forwardRef<
|
|||||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const sheetVariants = cva(
|
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: {
|
variants: {
|
||||||
side: {
|
side: {
|
||||||
@@ -62,10 +62,14 @@ const SheetContent = React.forwardRef<
|
|||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(sheetVariants({ side }), className)}
|
className={cn(sheetVariants({ side }), className)}
|
||||||
|
onOpenAutoFocus={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
{...props}
|
{...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">
|
<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="h-4 w-4" />
|
<X className="size-5" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
MediaFileData,
|
MediaFileData,
|
||||||
StorageConfig,
|
StorageConfig,
|
||||||
SerializedProject,
|
SerializedProject,
|
||||||
|
SerializedScene,
|
||||||
TimelineData,
|
TimelineData,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { TimelineTrack } from "@/types/timeline";
|
import { TimelineTrack } from "@/types/timeline";
|
||||||
@@ -39,7 +40,7 @@ class StorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to get project-specific media adapters
|
// Helper to get project-specific media adapters
|
||||||
private getProjectMediaAdapters(projectId: string) {
|
private getProjectMediaAdapters({ projectId }: { projectId: string }) {
|
||||||
const mediaMetadataAdapter = new IndexedDBAdapter<MediaFileData>(
|
const mediaMetadataAdapter = new IndexedDBAdapter<MediaFileData>(
|
||||||
`${this.config.mediaDb}-${projectId}`,
|
`${this.config.mediaDb}-${projectId}`,
|
||||||
"media-metadata",
|
"media-metadata",
|
||||||
@@ -52,23 +53,43 @@ class StorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to get project-specific timeline adapter
|
// 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>(
|
return new IndexedDBAdapter<TimelineData>(
|
||||||
`${this.config.timelineDb}-${projectId}`,
|
dbName,
|
||||||
"timeline",
|
"timeline",
|
||||||
this.config.version
|
this.config.version
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project operations
|
// Project operations
|
||||||
async saveProject(project: TProject): Promise<void> {
|
async saveProject({ project }: { project: TProject }): Promise<void> {
|
||||||
// Convert TProject to serializable format
|
// 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 = {
|
const serializedProject: SerializedProject = {
|
||||||
id: project.id,
|
id: project.id,
|
||||||
name: project.name,
|
name: project.name,
|
||||||
thumbnail: project.thumbnail,
|
thumbnail: project.thumbnail,
|
||||||
createdAt: project.createdAt.toISOString(),
|
createdAt: project.createdAt.toISOString(),
|
||||||
updatedAt: project.updatedAt.toISOString(),
|
updatedAt: project.updatedAt.toISOString(),
|
||||||
|
scenes: serializedScenes,
|
||||||
|
currentSceneId: project.currentSceneId,
|
||||||
backgroundColor: project.backgroundColor,
|
backgroundColor: project.backgroundColor,
|
||||||
backgroundType: project.backgroundType,
|
backgroundType: project.backgroundType,
|
||||||
blurIntensity: project.blurIntensity,
|
blurIntensity: project.blurIntensity,
|
||||||
@@ -81,11 +102,21 @@ class StorageService {
|
|||||||
await this.projectsAdapter.set(project.id, serializedProject);
|
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);
|
const serializedProject = await this.projectsAdapter.get(id);
|
||||||
|
|
||||||
if (!serializedProject) return null;
|
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
|
// Convert back to TProject format
|
||||||
return {
|
return {
|
||||||
id: serializedProject.id,
|
id: serializedProject.id,
|
||||||
@@ -93,6 +124,8 @@ class StorageService {
|
|||||||
thumbnail: serializedProject.thumbnail,
|
thumbnail: serializedProject.thumbnail,
|
||||||
createdAt: new Date(serializedProject.createdAt),
|
createdAt: new Date(serializedProject.createdAt),
|
||||||
updatedAt: new Date(serializedProject.updatedAt),
|
updatedAt: new Date(serializedProject.updatedAt),
|
||||||
|
scenes,
|
||||||
|
currentSceneId: serializedProject.currentSceneId || "",
|
||||||
backgroundColor: serializedProject.backgroundColor,
|
backgroundColor: serializedProject.backgroundColor,
|
||||||
backgroundType: serializedProject.backgroundType,
|
backgroundType: serializedProject.backgroundType,
|
||||||
blurIntensity: serializedProject.blurIntensity,
|
blurIntensity: serializedProject.blurIntensity,
|
||||||
@@ -108,7 +141,7 @@ class StorageService {
|
|||||||
const projects: TProject[] = [];
|
const projects: TProject[] = [];
|
||||||
|
|
||||||
for (const id of projectIds) {
|
for (const id of projectIds) {
|
||||||
const project = await this.loadProject(id);
|
const project = await this.loadProject({ id });
|
||||||
if (project) {
|
if (project) {
|
||||||
projects.push(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);
|
await this.projectsAdapter.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Media operations
|
// Media operations
|
||||||
async saveMediaFile(projectId: string, mediaItem: MediaFile): Promise<void> {
|
async saveMediaFile({
|
||||||
|
projectId,
|
||||||
|
mediaItem,
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
mediaItem: MediaFile;
|
||||||
|
}): Promise<void> {
|
||||||
const { mediaMetadataAdapter, mediaFilesAdapter } =
|
const { mediaMetadataAdapter, mediaFilesAdapter } =
|
||||||
this.getProjectMediaAdapters(projectId);
|
this.getProjectMediaAdapters({ projectId });
|
||||||
|
|
||||||
// Save file to project-specific OPFS
|
// Save file to project-specific OPFS
|
||||||
await mediaFilesAdapter.set(mediaItem.id, mediaItem.file);
|
await mediaFilesAdapter.set(mediaItem.id, mediaItem.file);
|
||||||
@@ -148,12 +187,15 @@ class StorageService {
|
|||||||
await mediaMetadataAdapter.set(mediaItem.id, metadata);
|
await mediaMetadataAdapter.set(mediaItem.id, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadMediaFile(
|
async loadMediaFile({
|
||||||
projectId: string,
|
projectId,
|
||||||
id: string
|
id,
|
||||||
): Promise<MediaFile | null> {
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
id: string;
|
||||||
|
}): Promise<MediaFile | null> {
|
||||||
const { mediaMetadataAdapter, mediaFilesAdapter } =
|
const { mediaMetadataAdapter, mediaFilesAdapter } =
|
||||||
this.getProjectMediaAdapters(projectId);
|
this.getProjectMediaAdapters({ projectId });
|
||||||
|
|
||||||
const [file, metadata] = await Promise.all([
|
const [file, metadata] = await Promise.all([
|
||||||
mediaFilesAdapter.get(id),
|
mediaFilesAdapter.get(id),
|
||||||
@@ -192,14 +234,20 @@ class StorageService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadAllMediaFiles(projectId: string): Promise<MediaFile[]> {
|
async loadAllMediaFiles({
|
||||||
const { mediaMetadataAdapter } = this.getProjectMediaAdapters(projectId);
|
projectId,
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
}): Promise<MediaFile[]> {
|
||||||
|
const { mediaMetadataAdapter } = this.getProjectMediaAdapters({
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
|
||||||
const mediaIds = await mediaMetadataAdapter.list();
|
const mediaIds = await mediaMetadataAdapter.list();
|
||||||
const mediaItems: MediaFile[] = [];
|
const mediaItems: MediaFile[] = [];
|
||||||
|
|
||||||
for (const id of mediaIds) {
|
for (const id of mediaIds) {
|
||||||
const item = await this.loadMediaFile(projectId, id);
|
const item = await this.loadMediaFile({ projectId, id });
|
||||||
if (item) {
|
if (item) {
|
||||||
mediaItems.push(item);
|
mediaItems.push(item);
|
||||||
}
|
}
|
||||||
@@ -208,9 +256,15 @@ class StorageService {
|
|||||||
return mediaItems;
|
return mediaItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteMediaFile(projectId: string, id: string): Promise<void> {
|
async deleteMediaFile({
|
||||||
|
projectId,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
id: string;
|
||||||
|
}): Promise<void> {
|
||||||
const { mediaMetadataAdapter, mediaFilesAdapter } =
|
const { mediaMetadataAdapter, mediaFilesAdapter } =
|
||||||
this.getProjectMediaAdapters(projectId);
|
this.getProjectMediaAdapters({ projectId });
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
mediaFilesAdapter.remove(id),
|
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 } =
|
const { mediaMetadataAdapter, mediaFilesAdapter } =
|
||||||
this.getProjectMediaAdapters(projectId);
|
this.getProjectMediaAdapters({ projectId });
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
mediaMetadataAdapter.clear(),
|
mediaMetadataAdapter.clear(),
|
||||||
@@ -228,12 +286,20 @@ class StorageService {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeline operations - now project-specific
|
// Timeline operations - supports both legacy and scene-based storage
|
||||||
async saveTimeline(
|
async saveTimeline({
|
||||||
projectId: string,
|
projectId,
|
||||||
tracks: TimelineTrack[]
|
tracks,
|
||||||
): Promise<void> {
|
sceneId,
|
||||||
const timelineAdapter = this.getProjectTimelineAdapter(projectId);
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
tracks: TimelineTrack[];
|
||||||
|
sceneId?: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const timelineAdapter = this.getProjectTimelineAdapter({
|
||||||
|
projectId,
|
||||||
|
sceneId,
|
||||||
|
});
|
||||||
const timelineData: TimelineData = {
|
const timelineData: TimelineData = {
|
||||||
tracks,
|
tracks,
|
||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
@@ -241,14 +307,27 @@ class StorageService {
|
|||||||
await timelineAdapter.set("timeline", timelineData);
|
await timelineAdapter.set("timeline", timelineData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadTimeline(projectId: string): Promise<TimelineTrack[] | null> {
|
async loadTimeline({
|
||||||
const timelineAdapter = this.getProjectTimelineAdapter(projectId);
|
projectId,
|
||||||
|
sceneId,
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
sceneId?: string;
|
||||||
|
}): Promise<TimelineTrack[] | null> {
|
||||||
|
const timelineAdapter = this.getProjectTimelineAdapter({
|
||||||
|
projectId,
|
||||||
|
sceneId,
|
||||||
|
});
|
||||||
const timelineData = await timelineAdapter.get("timeline");
|
const timelineData = await timelineAdapter.get("timeline");
|
||||||
return timelineData ? timelineData.tracks : null;
|
return timelineData ? timelineData.tracks : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteProjectTimeline(projectId: string): Promise<void> {
|
async deleteProjectTimeline({
|
||||||
const timelineAdapter = this.getProjectTimelineAdapter(projectId);
|
projectId,
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const timelineAdapter = this.getProjectTimelineAdapter({ projectId });
|
||||||
await timelineAdapter.remove("timeline");
|
await timelineAdapter.remove("timeline");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,12 +353,14 @@ class StorageService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectStorageInfo(projectId: string): Promise<{
|
async getProjectStorageInfo({ projectId }: { projectId: string }): Promise<{
|
||||||
mediaItems: number;
|
mediaItems: number;
|
||||||
hasTimeline: boolean;
|
hasTimeline: boolean;
|
||||||
}> {
|
}> {
|
||||||
const { mediaMetadataAdapter } = this.getProjectMediaAdapters(projectId);
|
const { mediaMetadataAdapter } = this.getProjectMediaAdapters({
|
||||||
const timelineAdapter = this.getProjectTimelineAdapter(projectId);
|
projectId,
|
||||||
|
});
|
||||||
|
const timelineAdapter = this.getProjectTimelineAdapter({ projectId });
|
||||||
|
|
||||||
const [mediaIds, timelineData] = await Promise.all([
|
const [mediaIds, timelineData] = await Promise.all([
|
||||||
mediaMetadataAdapter.list(),
|
mediaMetadataAdapter.list(),
|
||||||
@@ -307,7 +388,11 @@ class StorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSoundEffect(soundEffect: SoundEffect): Promise<void> {
|
async saveSoundEffect({
|
||||||
|
soundEffect,
|
||||||
|
}: {
|
||||||
|
soundEffect: SoundEffect;
|
||||||
|
}): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const currentData = await this.loadSavedSounds();
|
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 {
|
try {
|
||||||
const currentData = await this.loadSavedSounds();
|
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 {
|
try {
|
||||||
const currentData = await this.loadSavedSounds();
|
const currentData = await this.loadSavedSounds();
|
||||||
return currentData.sounds.some((sound) => sound.id === soundId);
|
return currentData.sounds.some((sound) => sound.id === soundId);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TProject } from "@/types/project";
|
import { TProject, Scene } from "@/types/project";
|
||||||
import { TimelineTrack } from "@/types/timeline";
|
import { TimelineTrack } from "@/types/timeline";
|
||||||
|
|
||||||
export interface StorageAdapter<T> {
|
export interface StorageAdapter<T> {
|
||||||
@@ -23,11 +23,23 @@ export interface MediaFileData {
|
|||||||
// File will be stored separately in OPFS
|
// File will be stored separately in OPFS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy timeline data, kept for backward compatibility
|
||||||
export interface TimelineData {
|
export interface TimelineData {
|
||||||
tracks: TimelineTrack[];
|
tracks: TimelineTrack[];
|
||||||
lastModified: string;
|
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 {
|
export interface StorageConfig {
|
||||||
projectsDb: string;
|
projectsDb: string;
|
||||||
mediaDb: string;
|
mediaDb: string;
|
||||||
@@ -37,9 +49,13 @@ export interface StorageConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper type for serialization - converts Date objects to strings
|
// 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;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
scenes: SerializedScene[];
|
||||||
bookmarks?: number[];
|
bookmarks?: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
|
|||||||
|
|
||||||
// Save to persistent storage in background
|
// Save to persistent storage in background
|
||||||
try {
|
try {
|
||||||
await storageService.saveMediaFile(projectId, newItem);
|
await storageService.saveMediaFile({ projectId, mediaItem: newItem });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save media item:", error);
|
console.error("Failed to save media item:", error);
|
||||||
// Remove from local state if save failed
|
// Remove from local state if save failed
|
||||||
@@ -217,7 +217,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
|
|||||||
|
|
||||||
// 3) Remove from persistent storage
|
// 3) Remove from persistent storage
|
||||||
try {
|
try {
|
||||||
await storageService.deleteMediaFile(projectId, id);
|
await storageService.deleteMediaFile({ projectId, id });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete media item:", error);
|
console.error("Failed to delete media item:", error);
|
||||||
}
|
}
|
||||||
@@ -227,7 +227,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
|
|||||||
set({ isLoading: true });
|
set({ isLoading: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mediaItems = await storageService.loadAllMediaFiles(projectId);
|
const mediaItems = await storageService.loadAllMediaFiles({ projectId });
|
||||||
|
|
||||||
// Regenerate thumbnails for video items
|
// Regenerate thumbnails for video items
|
||||||
const updatedMediaItems = await Promise.all(
|
const updatedMediaItems = await Promise.all(
|
||||||
@@ -282,7 +282,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
|
|||||||
try {
|
try {
|
||||||
const mediaIds = state.mediaFiles.map((item) => item.id);
|
const mediaIds = state.mediaFiles.map((item) => item.id);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
mediaIds.map((id) => storageService.deleteMediaFile(projectId, id))
|
mediaIds.map((id) => storageService.deleteMediaFile({ projectId, id }))
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to clear media items from storage:", error);
|
console.error("Failed to clear media items from storage:", error);
|
||||||
|
|||||||
@@ -1,21 +1,36 @@
|
|||||||
import { TProject, BlurIntensity } from "@/types/project";
|
import { TProject, BlurIntensity, Scene } from "@/types/project";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { storageService } from "@/lib/storage/storage-service";
|
import { storageService } from "@/lib/storage/storage-service";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useMediaStore } from "./media-store";
|
import { useMediaStore } from "./media-store";
|
||||||
import { useTimelineStore } from "./timeline-store";
|
import { useTimelineStore } from "./timeline-store";
|
||||||
|
import { useSceneStore } from "./scene-store";
|
||||||
import { generateUUID } from "@/lib/utils";
|
import { generateUUID } from "@/lib/utils";
|
||||||
import { CanvasSize, CanvasMode } from "@/types/editor";
|
import { CanvasSize, CanvasMode } from "@/types/editor";
|
||||||
|
|
||||||
export const DEFAULT_CANVAS_SIZE: CanvasSize = { width: 1920, height: 1080 };
|
export const DEFAULT_CANVAS_SIZE: CanvasSize = { width: 1920, height: 1080 };
|
||||||
export const DEFAULT_FPS = 30;
|
export const DEFAULT_FPS = 30;
|
||||||
|
|
||||||
const DEFAULT_PROJECT: TProject = {
|
function createMainScene(): Scene {
|
||||||
|
return {
|
||||||
id: generateUUID(),
|
id: generateUUID(),
|
||||||
name: "Untitled",
|
name: "Main Scene",
|
||||||
|
isMain: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDefaultProject = (name: string): TProject => {
|
||||||
|
const mainScene = createMainScene();
|
||||||
|
return {
|
||||||
|
id: generateUUID(),
|
||||||
|
name,
|
||||||
thumbnail: "",
|
thumbnail: "",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
scenes: [mainScene],
|
||||||
|
currentSceneId: mainScene.id,
|
||||||
backgroundColor: "#000000",
|
backgroundColor: "#000000",
|
||||||
backgroundType: "color",
|
backgroundType: "color",
|
||||||
blurIntensity: 8,
|
blurIntensity: 8,
|
||||||
@@ -23,6 +38,7 @@ const DEFAULT_PROJECT: TProject = {
|
|||||||
fps: DEFAULT_FPS,
|
fps: DEFAULT_FPS,
|
||||||
canvasSize: DEFAULT_CANVAS_SIZE,
|
canvasSize: DEFAULT_CANVAS_SIZE,
|
||||||
canvasMode: "preset",
|
canvasMode: "preset",
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ProjectStore {
|
interface ProjectStore {
|
||||||
@@ -104,7 +120,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await storageService.saveProject(updatedProject);
|
await storageService.saveProject({ project: updatedProject });
|
||||||
set({ activeProject: updatedProject });
|
set({ activeProject: updatedProject });
|
||||||
await get().loadAllProjects(); // Refresh the list
|
await get().loadAllProjects(); // Refresh the list
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -152,7 +168,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await storageService.saveProject(updatedProject);
|
await storageService.saveProject({ project: updatedProject });
|
||||||
set({ activeProject: updatedProject });
|
set({ activeProject: updatedProject });
|
||||||
await get().loadAllProjects(); // Refresh the list
|
await get().loadAllProjects(); // Refresh the list
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -164,23 +180,24 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
createNewProject: async (name: string) => {
|
createNewProject: async (name: string) => {
|
||||||
const newProject: TProject = {
|
const newProject = createDefaultProject(name);
|
||||||
...DEFAULT_PROJECT,
|
|
||||||
id: generateUUID(),
|
|
||||||
name,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
set({ activeProject: newProject });
|
set({ activeProject: newProject });
|
||||||
|
|
||||||
const mediaStore = useMediaStore.getState();
|
const mediaStore = useMediaStore.getState();
|
||||||
const timelineStore = useTimelineStore.getState();
|
const timelineStore = useTimelineStore.getState();
|
||||||
|
const sceneStore = useSceneStore.getState();
|
||||||
|
|
||||||
mediaStore.clearAllMedia();
|
mediaStore.clearAllMedia();
|
||||||
timelineStore.clearTimeline();
|
timelineStore.clearTimeline();
|
||||||
|
|
||||||
|
sceneStore.initializeScenes({
|
||||||
|
scenes: newProject.scenes,
|
||||||
|
currentSceneId: newProject.currentSceneId,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await storageService.saveProject(newProject);
|
await storageService.saveProject({ project: newProject });
|
||||||
// Reload all projects to update the list
|
// Reload all projects to update the list
|
||||||
await get().loadAllProjects();
|
await get().loadAllProjects();
|
||||||
return newProject.id;
|
return newProject.id;
|
||||||
@@ -195,21 +212,35 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
set({ isLoading: true });
|
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 mediaStore = useMediaStore.getState();
|
||||||
const timelineStore = useTimelineStore.getState();
|
const timelineStore = useTimelineStore.getState();
|
||||||
|
const sceneStore = useSceneStore.getState();
|
||||||
|
|
||||||
mediaStore.clearAllMedia();
|
mediaStore.clearAllMedia();
|
||||||
timelineStore.clearTimeline();
|
timelineStore.clearTimeline();
|
||||||
|
sceneStore.clearScenes();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const project = await storageService.loadProject(id);
|
const project = await storageService.loadProject({ id });
|
||||||
if (project) {
|
if (project) {
|
||||||
set({ activeProject: 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([
|
await Promise.all([
|
||||||
mediaStore.loadProjectMedia(id),
|
mediaStore.loadProjectMedia(id),
|
||||||
timelineStore.loadProjectTimeline(id),
|
timelineStore.loadProjectTimeline({
|
||||||
|
projectId: id,
|
||||||
|
sceneId: currentScene?.id,
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Project with id ${id} not found`);
|
throw new Error(`Project with id ${id} not found`);
|
||||||
@@ -227,11 +258,16 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
if (!activeProject) return;
|
if (!activeProject) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Save project metadata and timeline data in parallel
|
|
||||||
const timelineStore = useTimelineStore.getState();
|
const timelineStore = useTimelineStore.getState();
|
||||||
|
const sceneStore = useSceneStore.getState();
|
||||||
|
const currentScene = sceneStore.currentScene;
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
storageService.saveProject(activeProject),
|
storageService.saveProject({ project: activeProject }),
|
||||||
timelineStore.saveProjectTimeline(activeProject.id),
|
timelineStore.saveProjectTimeline({
|
||||||
|
projectId: activeProject.id,
|
||||||
|
sceneId: currentScene?.id,
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
await get().loadAllProjects(); // Refresh the list
|
await get().loadAllProjects(); // Refresh the list
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -256,22 +292,24 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
|
|
||||||
deleteProject: async (id: string) => {
|
deleteProject: async (id: string) => {
|
||||||
try {
|
try {
|
||||||
// Delete project data in parallel
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
storageService.deleteProjectMedia(id),
|
storageService.deleteProjectMedia({ projectId: id }),
|
||||||
storageService.deleteProjectTimeline(id),
|
storageService.deleteProjectTimeline({ projectId: id }),
|
||||||
storageService.deleteProject(id),
|
storageService.deleteProject({ id }),
|
||||||
]);
|
]);
|
||||||
await get().loadAllProjects(); // Refresh the list
|
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();
|
const { activeProject } = get();
|
||||||
if (activeProject?.id === id) {
|
if (activeProject?.id === id) {
|
||||||
set({ activeProject: null });
|
set({ activeProject: null });
|
||||||
const mediaStore = useMediaStore.getState();
|
const mediaStore = useMediaStore.getState();
|
||||||
const timelineStore = useTimelineStore.getState();
|
const timelineStore = useTimelineStore.getState();
|
||||||
|
const sceneStore = useSceneStore.getState();
|
||||||
|
|
||||||
mediaStore.clearAllMedia();
|
mediaStore.clearAllMedia();
|
||||||
timelineStore.clearTimeline();
|
timelineStore.clearTimeline();
|
||||||
|
sceneStore.clearScenes();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete project:", error);
|
console.error("Failed to delete project:", error);
|
||||||
@@ -281,11 +319,13 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
closeProject: () => {
|
closeProject: () => {
|
||||||
set({ activeProject: null });
|
set({ activeProject: null });
|
||||||
|
|
||||||
// Clear data from stores when closing project
|
|
||||||
const mediaStore = useMediaStore.getState();
|
const mediaStore = useMediaStore.getState();
|
||||||
const timelineStore = useTimelineStore.getState();
|
const timelineStore = useTimelineStore.getState();
|
||||||
|
const sceneStore = useSceneStore.getState();
|
||||||
|
|
||||||
mediaStore.clearAllMedia();
|
mediaStore.clearAllMedia();
|
||||||
timelineStore.clearTimeline();
|
timelineStore.clearTimeline();
|
||||||
|
sceneStore.clearScenes();
|
||||||
},
|
},
|
||||||
|
|
||||||
renameProject: async (id: string, name: string) => {
|
renameProject: async (id: string, name: string) => {
|
||||||
@@ -307,12 +347,11 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Save to storage
|
await storageService.saveProject({ project: updatedProject });
|
||||||
await storageService.saveProject(updatedProject);
|
|
||||||
|
|
||||||
await get().loadAllProjects();
|
await get().loadAllProjects();
|
||||||
|
|
||||||
// Update activeProject if it's the same project
|
// Update activeProject if same project
|
||||||
const { activeProject } = get();
|
const { activeProject } = get();
|
||||||
if (activeProject?.id === id) {
|
if (activeProject?.id === id) {
|
||||||
set({ activeProject: updatedProject });
|
set({ activeProject: updatedProject });
|
||||||
@@ -328,7 +367,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
|
|
||||||
duplicateProject: async (projectId: string) => {
|
duplicateProject: async (projectId: string) => {
|
||||||
try {
|
try {
|
||||||
const project = await storageService.loadProject(projectId);
|
const project = await storageService.loadProject({ id: projectId });
|
||||||
if (!project) {
|
if (!project) {
|
||||||
toast.error("Project not found", {
|
toast.error("Project not found", {
|
||||||
description: "Please try again",
|
description: "Please try again",
|
||||||
@@ -355,14 +394,14 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1;
|
existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1;
|
||||||
|
|
||||||
const newProject: TProject = {
|
const newProject: TProject = {
|
||||||
...project, // Copy all properties from the original project
|
...project,
|
||||||
id: generateUUID(),
|
id: generateUUID(),
|
||||||
name: `(${nextNumber}) ${baseName}`,
|
name: `(${nextNumber}) ${baseName}`,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
await storageService.saveProject(newProject);
|
await storageService.saveProject({ project: newProject });
|
||||||
await get().loadAllProjects();
|
await get().loadAllProjects();
|
||||||
return newProject.id;
|
return newProject.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -386,9 +425,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await storageService.saveProject(updatedProject);
|
await storageService.saveProject({ project: updatedProject });
|
||||||
set({ activeProject: updatedProject });
|
set({ activeProject: updatedProject });
|
||||||
await get().loadAllProjects(); // Refresh the list
|
await get().loadAllProjects();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update project background:", error);
|
console.error("Failed to update project background:", error);
|
||||||
toast.error("Failed to update background", {
|
toast.error("Failed to update background", {
|
||||||
@@ -417,9 +456,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await storageService.saveProject(updatedProject);
|
await storageService.saveProject({ project: updatedProject });
|
||||||
set({ activeProject: updatedProject });
|
set({ activeProject: updatedProject });
|
||||||
await get().loadAllProjects(); // Refresh the list
|
await get().loadAllProjects();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update background type:", error);
|
console.error("Failed to update background type:", error);
|
||||||
toast.error("Failed to update background", {
|
toast.error("Failed to update background", {
|
||||||
@@ -439,9 +478,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await storageService.saveProject(updatedProject);
|
await storageService.saveProject({ project: updatedProject });
|
||||||
set({ activeProject: updatedProject });
|
set({ activeProject: updatedProject });
|
||||||
await get().loadAllProjects(); // Refresh the list
|
await get().loadAllProjects();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update project FPS:", error);
|
console.error("Failed to update project FPS:", error);
|
||||||
toast.error("Failed to update project FPS", {
|
toast.error("Failed to update project FPS", {
|
||||||
@@ -462,9 +501,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await storageService.saveProject(updatedProject);
|
await storageService.saveProject({ project: updatedProject });
|
||||||
set({ activeProject: updatedProject });
|
set({ activeProject: updatedProject });
|
||||||
await get().loadAllProjects(); // Refresh the list
|
await get().loadAllProjects();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update canvas size:", error);
|
console.error("Failed to update canvas size:", error);
|
||||||
toast.error("Failed to update canvas size", {
|
toast.error("Failed to update canvas size", {
|
||||||
@@ -476,12 +515,10 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
getFilteredAndSortedProjects: (searchQuery: string, sortOption: string) => {
|
getFilteredAndSortedProjects: (searchQuery: string, sortOption: string) => {
|
||||||
const { savedProjects } = get();
|
const { savedProjects } = get();
|
||||||
|
|
||||||
// Filter projects by search query
|
|
||||||
const filteredProjects = savedProjects.filter((project) =>
|
const filteredProjects = savedProjects.filter((project) =>
|
||||||
project.name.toLowerCase().includes(searchQuery.toLowerCase())
|
project.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sort filtered projects
|
|
||||||
const sortedProjects = [...filteredProjects].sort((a, b) => {
|
const sortedProjects = [...filteredProjects].sort((a, b) => {
|
||||||
const [key, order] = sortOption.split("-");
|
const [key, order] = sortOption.split("-");
|
||||||
|
|
||||||
@@ -508,7 +545,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
return sortedProjects;
|
return sortedProjects;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Global invalid project ID tracking implementation
|
// Global invalid project ID tracking
|
||||||
isInvalidProjectId: (id: string) => {
|
isInvalidProjectId: (id: string) => {
|
||||||
const invalidIds = get().invalidProjectIds || new Set();
|
const invalidIds = get().invalidProjectIds || new Set();
|
||||||
return invalidIds.has(id);
|
return invalidIds.has(id);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -162,7 +162,7 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
|
|||||||
|
|
||||||
saveSoundEffect: async (soundEffect: SoundEffect) => {
|
saveSoundEffect: async (soundEffect: SoundEffect) => {
|
||||||
try {
|
try {
|
||||||
await storageService.saveSoundEffect(soundEffect);
|
await storageService.saveSoundEffect({ soundEffect });
|
||||||
|
|
||||||
// Refresh saved sounds
|
// Refresh saved sounds
|
||||||
const savedSoundsData = await storageService.loadSavedSounds();
|
const savedSoundsData = await storageService.loadSavedSounds();
|
||||||
@@ -178,7 +178,7 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
|
|||||||
|
|
||||||
removeSavedSound: async (soundId: number) => {
|
removeSavedSound: async (soundId: number) => {
|
||||||
try {
|
try {
|
||||||
await storageService.removeSavedSound(soundId);
|
await storageService.removeSavedSound({ soundId });
|
||||||
|
|
||||||
// Update local state immediately
|
// Update local state immediately
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { MediaFile } from "@/types/media";
|
|||||||
import { findBestCanvasPreset } from "@/lib/editor-utils";
|
import { findBestCanvasPreset } from "@/lib/editor-utils";
|
||||||
import { storageService } from "@/lib/storage/storage-service";
|
import { storageService } from "@/lib/storage/storage-service";
|
||||||
import { useProjectStore } from "./project-store";
|
import { useProjectStore } from "./project-store";
|
||||||
|
import { useSceneStore } from "./scene-store";
|
||||||
import { generateUUID } from "@/lib/utils";
|
import { generateUUID } from "@/lib/utils";
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
import { checkElementOverlaps, resolveElementOverlaps } from "@/lib/timeline";
|
import { checkElementOverlaps, resolveElementOverlaps } from "@/lib/timeline";
|
||||||
@@ -178,8 +179,20 @@ interface TimelineStore {
|
|||||||
pushHistory: () => void;
|
pushHistory: () => void;
|
||||||
|
|
||||||
// Persistence actions
|
// Persistence actions
|
||||||
loadProjectTimeline: (projectId: string) => Promise<void>;
|
loadProjectTimeline: ({
|
||||||
saveProjectTimeline: (projectId: string) => Promise<void>;
|
projectId,
|
||||||
|
sceneId,
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
sceneId?: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
saveProjectTimeline: ({
|
||||||
|
projectId,
|
||||||
|
sceneId,
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
sceneId?: string;
|
||||||
|
}) => Promise<void>;
|
||||||
clearTimeline: () => void;
|
clearTimeline: () => void;
|
||||||
|
|
||||||
// Clipboard actions
|
// Clipboard actions
|
||||||
@@ -235,9 +248,15 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||||||
// Helper to auto-save timeline changes
|
// Helper to auto-save timeline changes
|
||||||
const autoSaveTimeline = async () => {
|
const autoSaveTimeline = async () => {
|
||||||
const activeProject = useProjectStore.getState().activeProject;
|
const activeProject = useProjectStore.getState().activeProject;
|
||||||
if (activeProject) {
|
const currentScene = useSceneStore.getState().currentScene;
|
||||||
|
|
||||||
|
if (activeProject && currentScene) {
|
||||||
try {
|
try {
|
||||||
await storageService.saveTimeline(activeProject.id, get()._tracks);
|
await storageService.saveTimeline({
|
||||||
|
projectId: activeProject.id,
|
||||||
|
tracks: get()._tracks,
|
||||||
|
sceneId: currentScene.id,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to auto-save timeline:", error);
|
console.error("Failed to auto-save timeline:", error);
|
||||||
}
|
}
|
||||||
@@ -1229,8 +1248,24 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||||||
|
|
||||||
getProjectThumbnail: async (projectId) => {
|
getProjectThumbnail: async (projectId) => {
|
||||||
try {
|
try {
|
||||||
const tracks = await storageService.loadTimeline(projectId);
|
const project = await storageService.loadProject({ id: projectId });
|
||||||
const mediaItems = await storageService.loadAllMediaFiles(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;
|
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 {
|
try {
|
||||||
const tracks = await storageService.loadTimeline(projectId);
|
const tracks = await storageService.loadTimeline({
|
||||||
|
projectId,
|
||||||
|
sceneId,
|
||||||
|
});
|
||||||
|
|
||||||
if (tracks) {
|
if (tracks) {
|
||||||
updateTracks(tracks);
|
updateTracks(tracks);
|
||||||
} else {
|
} 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 {
|
try {
|
||||||
await storageService.saveTimeline(projectId, get()._tracks);
|
await storageService.saveTimeline({
|
||||||
|
projectId,
|
||||||
|
tracks: _tracks,
|
||||||
|
sceneId,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save timeline:", error);
|
console.error("Failed to save timeline:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,22 @@ import { CanvasSize } from "./editor";
|
|||||||
|
|
||||||
export type BlurIntensity = 4 | 8 | 18;
|
export type BlurIntensity = 4 | 8 | 18;
|
||||||
|
|
||||||
|
export interface Scene {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
isMain: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TProject {
|
export interface TProject {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
scenes: Scene[];
|
||||||
|
currentSceneId: string;
|
||||||
mediaItems?: string[];
|
mediaItems?: string[];
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
backgroundType?: "color" | "blur";
|
backgroundType?: "color" | "blur";
|
||||||
|
|||||||
Reference in New Issue
Block a user