summaryrefslogtreecommitdiff
path: root/apps/web/src/stores
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-09-01 16:17:06 +0200
committerMaze Winther <mazewinther@gmail.com>2025-09-01 16:17:06 +0200
commit350b55081cde3a4332c982ea5c53b7d359029f6b (patch)
treef63b66635d2a13a0a43c622a9bea25d9b9822960 /apps/web/src/stores
parent2b700b7474ffc728f5318c178244e035c93716b5 (diff)
downloadopencut-350b55081cde3a4332c982ea5c53b7d359029f6b.tar.gz
refactor(core): switch to scene-based architecture
Diffstat (limited to 'apps/web/src/stores')
-rw-r--r--apps/web/src/stores/media-store.ts8
-rw-r--r--apps/web/src/stores/project-store.ts145
-rw-r--r--apps/web/src/stores/scene-store.ts219
-rw-r--r--apps/web/src/stores/sounds-store.ts4
-rw-r--r--apps/web/src/stores/timeline-store.ts76
5 files changed, 382 insertions, 70 deletions
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);
}