summaryrefslogtreecommitdiff
path: root/apps/web/src/stores
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/stores')
-rw-r--r--apps/web/src/stores/keybindings-store.ts4
-rw-r--r--apps/web/src/stores/media-store.ts19
-rw-r--r--apps/web/src/stores/project-store.ts158
-rw-r--r--apps/web/src/stores/scene-store.ts318
-rw-r--r--apps/web/src/stores/sounds-store.ts4
-rw-r--r--apps/web/src/stores/stickers-store.ts53
-rw-r--r--apps/web/src/stores/timeline-store.ts580
7 files changed, 936 insertions, 200 deletions
diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/stores/keybindings-store.ts
index 7fa0da5e..febcaa30 100644
--- a/apps/web/src/stores/keybindings-store.ts
+++ b/apps/web/src/stores/keybindings-store.ts
@@ -206,6 +206,9 @@ function getPressedKey(ev: KeyboardEvent): string | null {
const key = (ev.key ?? "").toLowerCase();
const code = ev.code ?? "";
+ if (code === "Space" || key === " " || key === "spacebar" || key === "space")
+ return "space";
+
// Check arrow keys
if (key.startsWith("arrow")) {
return key.slice(5);
@@ -213,7 +216,6 @@ function getPressedKey(ev: KeyboardEvent): string | null {
// Check for special keys
if (key === "tab") return "tab";
- if (key === " " || key === "space") return "space";
if (key === "home") return "home";
if (key === "end") return "end";
if (key === "delete") return "delete";
diff --git a/apps/web/src/stores/media-store.ts b/apps/web/src/stores/media-store.ts
index 2b7cb62e..61c771db 100644
--- a/apps/web/src/stores/media-store.ts
+++ b/apps/web/src/stores/media-store.ts
@@ -3,6 +3,7 @@ import { storageService } from "@/lib/storage/storage-service";
import { useTimelineStore } from "./timeline-store";
import { generateUUID } from "@/lib/utils";
import { MediaType, MediaFile } from "@/types/media";
+import { videoCache } from "@/lib/video-cache";
interface MediaStore {
mediaFiles: MediaFile[];
@@ -151,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
@@ -165,6 +166,8 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
const state = get();
const item = state.mediaFiles.find((media) => media.id === id);
+ videoCache.clearVideo(id);
+
// Cleanup object URLs to prevent memory leaks
if (item?.url) {
URL.revokeObjectURL(item.url);
@@ -200,6 +203,12 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
// If there are elements to remove, push history once before batch removal
if (elementsToRemove.length > 0) {
+ const {
+ removeElementFromTrack,
+ removeElementFromTrackWithRipple,
+ rippleEditingEnabled,
+ pushHistory,
+ } = useTimelineStore.getState();
pushHistory();
// Remove all elements without pushing additional history entries
@@ -214,7 +223,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);
}
@@ -224,7 +233,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(
@@ -279,7 +288,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);
@@ -289,6 +298,8 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
clearAllMedia: () => {
const state = get();
+ videoCache.clearAll();
+
// Cleanup all object URLs
state.mediaFiles.forEach((item) => {
if (item.url) {
diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts
index b924ea22..c58f7309 100644
--- a/apps/web/src/stores/project-store.ts
+++ b/apps/web/src/stores/project-store.ts
@@ -1,28 +1,45 @@
-import { TProject } 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",
+export 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 {
@@ -44,7 +61,7 @@ interface ProjectStore {
updateProjectBackground: (backgroundColor: string) => Promise<void>;
updateBackgroundType: (
type: "color" | "blur",
- options?: { backgroundColor?: string; blurIntensity?: number }
+ options?: { backgroundColor?: string; blurIntensity?: BlurIntensity }
) => Promise<void>;
updateProjectFps: (fps: number) => Promise<void>;
updateCanvasSize: (size: CanvasSize, mode: CanvasMode) => Promise<void>;
@@ -104,7 +121,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 +169,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 +181,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 +213,39 @@ 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
+ let currentScene = null;
+ if (project.scenes && project.scenes.length > 0) {
+ sceneStore.initializeScenes({
+ scenes: project.scenes,
+ currentSceneId: project.currentSceneId,
+ });
+ // Get current scene directly from project data (don't rely on store state)
+ currentScene =
+ project.scenes.find((s) => s.id === project.currentSceneId) ||
+ project.scenes.find((s) => s.isMain) ||
+ project.scenes[0];
+ }
+
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 +263,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 +297,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 +324,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 +352,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 +372,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 +399,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 +430,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", {
@@ -399,7 +443,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
updateBackgroundType: async (
type: "color" | "blur",
- options?: { backgroundColor?: string; blurIntensity?: number }
+ options?: { backgroundColor?: string; blurIntensity?: BlurIntensity }
) => {
const { activeProject } = get();
if (!activeProject) return;
@@ -410,14 +454,16 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
...(options?.backgroundColor && {
backgroundColor: options.backgroundColor,
}),
- ...(options?.blurIntensity && { blurIntensity: options.blurIntensity }),
+ ...(options?.blurIntensity !== undefined && {
+ blurIntensity: options.blurIntensity,
+ }),
updatedAt: new Date(),
};
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", {
@@ -437,9 +483,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", {
@@ -460,9 +506,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", {
@@ -474,12 +520,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("-");
@@ -506,7 +550,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..a9f65a49
--- /dev/null
+++ b/apps/web/src/stores/scene-store.ts
@@ -0,0 +1,318 @@
+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;
+}
+
+function ensureMainScene(scenes: Scene[]): Scene[] {
+ const hasMain = scenes.some((scene) => scene.isMain);
+ if (!hasMain) {
+ const mainScene: Scene = {
+ id: generateUUID(),
+ name: "Main scene",
+ isMain: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+ return [mainScene, ...scenes];
+ }
+ return scenes;
+}
+
+interface SceneStore {
+ // Current scene state
+ currentScene: Scene | null;
+ scenes: Scene[];
+
+ // Scene management
+ createScene: ({
+ name,
+ isMain,
+ }: {
+ name: string;
+ isMain: boolean;
+ }) => Promise<string>;
+ deleteScene: ({ sceneId }: { sceneId: string }) => Promise<void>;
+ 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,
+ isBackground: false,
+ 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;
+ }
+ },
+
+ deleteScene: async ({ sceneId }: { sceneId: string }) => {
+ const { scenes, currentScene } = get();
+ const sceneToDelete = scenes.find((s) => s.id === sceneId);
+
+ if (!sceneToDelete) {
+ throw new Error("Scene not found");
+ }
+
+ if (sceneToDelete.isMain) {
+ throw new Error("Cannot delete main scene");
+ }
+
+ const updatedScenes = scenes.filter((s) => s.id !== sceneId);
+
+ // Determine new current scene if we're deleting the current one
+ let newCurrentScene = currentScene;
+ if (currentScene?.id === sceneId) {
+ newCurrentScene = getMainScene({ scenes: updatedScenes });
+ }
+
+ // 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: newCurrentScene,
+ });
+
+ // If we switched scenes, load the new scene's timeline
+ if (newCurrentScene && newCurrentScene.id !== currentScene?.id) {
+ const timelineStore = useTimelineStore.getState();
+ await timelineStore.loadProjectTimeline({
+ projectId: activeProject.id,
+ sceneId: newCurrentScene.id,
+ });
+ }
+ } catch (error) {
+ console.error("Failed to delete 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 ensuredScenes = ensureMainScene(scenes);
+ const currentScene = currentSceneId
+ ? ensuredScenes.find((s) => s.id === currentSceneId)
+ : null;
+
+ const fallbackScene = getMainScene({ scenes: ensuredScenes });
+
+ set({
+ scenes: ensuredScenes,
+ currentScene: currentScene || fallbackScene,
+ });
+
+ if (ensuredScenes.length > scenes.length) {
+ const projectStore = useProjectStore.getState();
+ const { activeProject } = projectStore;
+
+ if (activeProject) {
+ const updatedProject = {
+ ...activeProject,
+ scenes: ensuredScenes,
+ updatedAt: new Date(),
+ };
+
+ storageService
+ .saveProject({ project: updatedProject })
+ .then(() => {
+ useProjectStore.setState({ activeProject: updatedProject });
+ })
+ .catch((error) => {
+ console.error(
+ "Failed to save project with background scene:",
+ error
+ );
+ });
+ }
+ }
+ },
+
+ 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/stickers-store.ts b/apps/web/src/stores/stickers-store.ts
index da8b926b..c7c9ef98 100644
--- a/apps/web/src/stores/stickers-store.ts
+++ b/apps/web/src/stores/stickers-store.ts
@@ -9,6 +9,12 @@ import {
type CollectionInfo,
type IconSearchResult,
} from "@/lib/iconify-api";
+import { useProjectStore } from "@/stores/project-store";
+import { useMediaStore } from "@/stores/media-store";
+import { useTimelineStore } from "@/stores/timeline-store";
+import { usePlaybackStore } from "@/stores/playback-store";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import type { MediaFile } from "@/types/media";
export type StickerCategory = "all" | "general" | "brands" | "emoji";
@@ -26,6 +32,7 @@ interface StickersStore {
isLoadingCollection: boolean;
isSearching: boolean;
isDownloading: boolean;
+ addingSticker: string | null;
setSearchQuery: (query: string) => void;
setSelectedCategory: (category: StickerCategory) => void;
@@ -36,6 +43,7 @@ interface StickersStore {
loadCollection: (prefix: string) => Promise<void>;
searchStickers: (query: string) => Promise<void>;
downloadSticker: (iconName: string) => Promise<File | null>;
+ addStickerToTimeline: (iconName: string) => Promise<void>;
addToRecentStickers: (iconName: string) => void;
clearRecentStickers: () => void;
@@ -58,6 +66,7 @@ export const useStickersStore = create<StickersStore>((set, get) => ({
isLoadingCollection: false,
isSearching: false,
isDownloading: false,
+ addingSticker: null,
setSearchQuery: (query) => set({ searchQuery: query }),
@@ -162,6 +171,50 @@ export const useStickersStore = create<StickersStore>((set, get) => ({
}
},
+ addStickerToTimeline: async (iconName: string) => {
+ set({ addingSticker: iconName });
+ try {
+ const { activeProject } = useProjectStore.getState();
+ if (!activeProject) {
+ throw new Error("No active project");
+ }
+
+ const file = await get().downloadSticker(iconName);
+ if (!file) {
+ throw new Error("Failed to download sticker");
+ }
+
+ const mediaItem: Omit<MediaFile, "id"> = {
+ name: iconName.replace(":", "-"),
+ type: "image",
+ file,
+ url: URL.createObjectURL(file),
+ width: 200,
+ height: 200,
+ duration: TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION,
+ ephemeral: false,
+ };
+
+ const { addMediaFile } = useMediaStore.getState();
+ await addMediaFile(activeProject.id, mediaItem);
+
+ const added = useMediaStore
+ .getState()
+ .mediaFiles.find(
+ (m) => m.url === mediaItem.url && m.name === mediaItem.name
+ );
+ if (!added) {
+ throw new Error("Sticker not in media store");
+ }
+
+ const { currentTime } = usePlaybackStore.getState();
+ const { addElementAtTime } = useTimelineStore.getState();
+ addElementAtTime(added, currentTime);
+ } finally {
+ set({ addingSticker: null });
+ }
+ },
+
addToRecentStickers: (iconName: string) => {
set((state) => {
const recent = [
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts
index f0bae00c..aeeb65c5 100644
--- a/apps/web/src/stores/timeline-store.ts
+++ b/apps/web/src/stores/timeline-store.ts
@@ -6,19 +6,22 @@ import {
TimelineTrack,
TextElement,
DragData,
+ MediaElement,
sortTracksByOrder,
ensureMainTrack,
validateElementTrackCompatibility,
} from "@/types/timeline";
import { useMediaStore, getMediaAspectRatio } from "./media-store";
-import { MediaFile } from "@/types/media";
+import { MediaFile, MediaType } 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";
import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
+import { usePlaybackStore } from "./playback-store";
// Helper function to manage element naming with suffixes
const getElementNameWithSuffix = (
@@ -98,11 +101,7 @@ interface TimelineStore {
removeTrack: (trackId: string) => void;
removeTrackWithRipple: (trackId: string) => void;
addElementToTrack: (trackId: string, element: CreateTimelineElement) => void;
- removeElementFromTrack: (
- trackId: string,
- elementId: string,
- pushHistory?: boolean
- ) => void;
+
moveElementToTrack: (
fromTrackId: string,
toTrackId: string,
@@ -128,15 +127,6 @@ interface TimelineStore {
pushHistory?: boolean
) => void;
toggleTrackMute: (trackId: string) => void;
- toggleElementHidden: (trackId: string, elementId: string) => void;
- toggleElementMuted: (trackId: string, elementId: string) => void;
-
- // Split operations for elements
- splitElement: (
- trackId: string,
- elementId: string,
- splitTime: number
- ) => string | null;
splitAndKeepLeft: (
trackId: string,
elementId: string,
@@ -178,13 +168,52 @@ 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
copySelected: () => void;
pasteAtTime: (time: number) => void;
+
+ // Unified selection-aware actions
+ deleteSelected: (trackId?: string, elementId?: string) => void;
+ splitSelected: (
+ splitTime: number,
+ trackId?: string,
+ elementId?: string
+ ) => void;
+ toggleSelectedHidden: (trackId?: string, elementId?: string) => void;
+ toggleSelectedMuted: (trackId?: string, elementId?: string) => void;
+ duplicateElement: (trackId: string, elementId: string) => void;
+ revealElementInMedia: (elementId: string) => void;
+ replaceElementWithFile: (
+ trackId: string,
+ elementId: string,
+ file: File
+ ) => Promise<void>;
+ getContextMenuState: (
+ trackId: string,
+ elementId: string
+ ) => {
+ isMultipleSelected: boolean;
+ isCurrentElementSelected: boolean;
+ hasAudioElements: boolean;
+ canSplitSelected: boolean;
+ currentTime: number;
+ };
updateTextElement: (
trackId: string,
elementId: string,
@@ -235,12 +264,27 @@ 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);
}
+ } else {
+ console.warn(
+ "Auto-save skipped - missing activeProject or currentScene:",
+ {
+ hasProject: !!activeProject,
+ hasScene: !!currentScene,
+ sceneName: currentScene?.name,
+ }
+ );
}
};
@@ -547,16 +591,19 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
get().selectElement(trackId, newElement.id);
},
- removeElementFromTrack: (trackId, elementId, pushHistory = true) => {
- const { rippleEditingEnabled } = get();
+ removeElementFromTrackWithRipple: (
+ trackId,
+ elementId,
+ pushHistory = true
+ ) => {
+ const { _tracks, rippleEditingEnabled } = get();
- if (rippleEditingEnabled) {
- get().removeElementFromTrackWithRipple(trackId, elementId, pushHistory);
- } else {
+ if (!rippleEditingEnabled) {
+ // Inline non-ripple removal logic
if (pushHistory) get().pushHistory();
updateTracksAndSave(
- get()
- ._tracks.map((track) =>
+ _tracks
+ .map((track) =>
track.id === trackId
? {
...track,
@@ -568,18 +615,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
)
.filter((track) => track.elements.length > 0)
);
- }
- },
-
- removeElementFromTrackWithRipple: (
- trackId,
- elementId,
- pushHistory = true
- ) => {
- const { _tracks, rippleEditingEnabled } = get();
-
- if (!rippleEditingEnabled) {
- get().removeElementFromTrack(trackId, elementId, pushHistory);
return;
}
@@ -836,42 +871,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
);
},
- toggleElementHidden: (trackId, elementId) => {
- get().pushHistory();
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((element) =>
- element.id === elementId
- ? { ...element, hidden: !element.hidden }
- : element
- ),
- }
- : track
- )
- );
- },
-
- toggleElementMuted: (trackId, elementId) => {
- get().pushHistory();
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.map((element) =>
- element.id === elementId && element.type === "media"
- ? { ...element, muted: !element.muted }
- : element
- ),
- }
- : track
- )
- );
- },
-
updateTextElement: (trackId, elementId, updates) => {
get().pushHistory();
updateTracksAndSave(
@@ -890,60 +889,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
);
},
- splitElement: (trackId, elementId, splitTime) => {
- const { _tracks } = get();
- const track = _tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((c) => c.id === elementId);
-
- if (!element) return null;
-
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
-
- if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return null;
-
- get().pushHistory();
-
- const relativeTime = splitTime - element.startTime;
- const firstDuration = relativeTime;
- const secondDuration =
- element.duration - element.trimStart - element.trimEnd - relativeTime;
-
- const secondElementId = generateUUID();
-
- updateTracksAndSave(
- get()._tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- elements: track.elements.flatMap((c) =>
- c.id === elementId
- ? [
- {
- ...c,
- trimEnd: c.trimEnd + secondDuration,
- name: getElementNameWithSuffix(c.name, "left"),
- },
- {
- ...c,
- id: secondElementId,
- startTime: splitTime,
- trimStart: c.trimStart + firstDuration,
- name: getElementNameWithSuffix(c.name, "right"),
- },
- ]
- : [c]
- ),
- }
- : track
- )
- );
-
- return secondElementId;
- },
-
// Split element and keep only the left portion
splitAndKeepLeft: (trackId, elementId, splitTime) => {
const { _tracks } = get();
@@ -1122,9 +1067,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
};
}
- const mediaData: any = {
+ const mediaData: Omit<MediaFile, "id"> = {
name: newFile.name,
- type: fileType,
+ type: fileType as MediaType,
file: newFile,
url: URL.createObjectURL(newFile),
};
@@ -1229,8 +1174,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 +1291,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 +1319,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);
}
@@ -1541,6 +1523,332 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
}
}
},
+
+ deleteSelected: (trackId?: string, elementId?: string) => {
+ const { selectedElements, rippleEditingEnabled } = get();
+
+ const elementsToDelete =
+ trackId && elementId
+ ? [{ trackId, elementId }]
+ : selectedElements.length > 0
+ ? selectedElements
+ : [];
+
+ if (elementsToDelete.length === 0) return;
+
+ get().pushHistory();
+
+ if (rippleEditingEnabled) {
+ for (const { trackId: tId, elementId: eId } of elementsToDelete) {
+ get().removeElementFromTrackWithRipple(tId, eId, false);
+ }
+ } else {
+ updateTracksAndSave(
+ get()
+ ._tracks.map((track) => ({
+ ...track,
+ elements: track.elements.filter(
+ (element) =>
+ !elementsToDelete.some(
+ ({ trackId: tId, elementId: eId }) =>
+ track.id === tId && element.id === eId
+ )
+ ),
+ }))
+ .filter((track) => track.elements.length > 0)
+ );
+ }
+
+ get().clearSelectedElements();
+ },
+
+ splitSelected: (splitTime, trackId?: string, elementId?: string) => {
+ const { selectedElements, _tracks } = get();
+
+ const elementsToProcess =
+ trackId && elementId
+ ? [{ trackId, elementId }]
+ : selectedElements.length > 0
+ ? selectedElements
+ : [];
+
+ if (elementsToProcess.length === 0) return;
+
+ const elementsToSplit: Array<{
+ trackId: string;
+ elementId: string;
+ element: TimelineElement;
+ }> = [];
+
+ for (const { trackId: tId, elementId: eId } of elementsToProcess) {
+ const track = _tracks.find((t) => t.id === tId);
+ const element = track?.elements.find((e) => e.id === eId);
+ if (!track || !element) continue;
+
+ const effectiveStart = element.startTime;
+ const effectiveEnd =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd);
+
+ if (splitTime > effectiveStart && splitTime < effectiveEnd) {
+ elementsToSplit.push({ trackId: tId, elementId: eId, element });
+ }
+ }
+
+ if (elementsToSplit.length === 0) {
+ const { toast } = require("sonner");
+ const isMultiple = elementsToProcess.length > 1;
+ toast.error(
+ isMultiple
+ ? "Playhead must be within all selected elements to split"
+ : "Playhead must be within element to split"
+ );
+ return;
+ }
+
+ get().pushHistory();
+
+ updateTracksAndSave(
+ get()._tracks.map((track) => {
+ const elementsToSplitInTrack = elementsToSplit.filter(
+ ({ trackId: tId }) => tId === track.id
+ );
+
+ if (elementsToSplitInTrack.length === 0) return track;
+
+ return {
+ ...track,
+ elements: track.elements.flatMap((c) => {
+ const elementToSplit = elementsToSplitInTrack.find(
+ ({ elementId: eId }) => eId === c.id
+ );
+
+ if (!elementToSplit) return [c];
+
+ const relativeTime = splitTime - elementToSplit.element.startTime;
+ const firstDuration = relativeTime;
+ const secondDuration =
+ elementToSplit.element.duration -
+ elementToSplit.element.trimStart -
+ elementToSplit.element.trimEnd -
+ relativeTime;
+
+ const secondElementId = generateUUID();
+
+ return [
+ {
+ ...c,
+ trimEnd: c.trimEnd + secondDuration,
+ name: getElementNameWithSuffix(c.name, "left"),
+ },
+ {
+ ...c,
+ id: secondElementId,
+ startTime: splitTime,
+ trimStart: c.trimStart + firstDuration,
+ name: getElementNameWithSuffix(c.name, "right"),
+ },
+ ];
+ }),
+ };
+ })
+ );
+ },
+
+ toggleSelectedHidden: (trackId?: string, elementId?: string) => {
+ const { selectedElements, _tracks } = get();
+
+ const elementsToProcess =
+ trackId && elementId
+ ? [{ trackId, elementId }]
+ : selectedElements.length > 0
+ ? selectedElements
+ : [];
+
+ if (elementsToProcess.length === 0) return;
+
+ get().pushHistory();
+
+ const shouldHide = elementsToProcess.some(
+ ({ trackId: tId, elementId: eId }) => {
+ const track = _tracks.find((t) => t.id === tId);
+ const element = track?.elements.find((e) => e.id === eId);
+ return element && !element.hidden;
+ }
+ );
+
+ updateTracksAndSave(
+ _tracks.map((track) => ({
+ ...track,
+ elements: track.elements.map((element) => {
+ const shouldUpdate = elementsToProcess.some(
+ ({ trackId: tId, elementId: eId }) =>
+ track.id === tId && element.id === eId
+ );
+ return shouldUpdate && element.hidden !== shouldHide
+ ? { ...element, hidden: shouldHide }
+ : element;
+ }),
+ }))
+ );
+ },
+
+ toggleSelectedMuted: (trackId?: string, elementId?: string) => {
+ const { selectedElements, _tracks } = get();
+
+ const elementsToProcess =
+ trackId && elementId
+ ? [{ trackId, elementId }]
+ : selectedElements.length > 0
+ ? selectedElements
+ : [];
+
+ if (elementsToProcess.length === 0) return;
+
+ get().pushHistory();
+
+ const audioElements = elementsToProcess.filter(
+ ({ trackId: tId, elementId: eId }) => {
+ const track = _tracks.find((t) => t.id === tId);
+ const element = track?.elements.find((e) => e.id === eId);
+ return element?.type === "media";
+ }
+ );
+
+ if (audioElements.length === 0) return;
+
+ const shouldMute = audioElements.some(
+ ({ trackId: tId, elementId: eId }) => {
+ const track = _tracks.find((t) => t.id === tId);
+ const element = track?.elements.find((e) => e.id === eId);
+ return element?.type === "media" && !element.muted;
+ }
+ );
+
+ updateTracksAndSave(
+ _tracks.map((track) => ({
+ ...track,
+ elements: track.elements.map((element) => {
+ const shouldUpdate = audioElements.some(
+ ({ trackId: tId, elementId: eId }) =>
+ track.id === tId && element.id === eId
+ );
+ return shouldUpdate &&
+ element.type === "media" &&
+ element.muted !== shouldMute
+ ? { ...element, muted: shouldMute }
+ : element;
+ }),
+ }))
+ );
+ },
+
+ duplicateElement: (trackId, elementId) => {
+ const { _tracks } = get();
+ const track = _tracks.find((t) => t.id === trackId);
+ const element = track?.elements.find((e) => e.id === elementId);
+ if (!track || !element) return;
+
+ const { id, ...elementWithoutId } = element;
+ const effectiveDuration =
+ element.duration - element.trimStart - element.trimEnd;
+
+ get().addElementToTrack(trackId, {
+ ...elementWithoutId,
+ name: `${element.name} (copy)`,
+ startTime: element.startTime + effectiveDuration + 0.1,
+ } as CreateTimelineElement);
+ },
+
+ revealElementInMedia: (elementId) => {
+ const {
+ useMediaPanelStore,
+ } = require("../components/editor/media-panel/store");
+ const { requestRevealMedia } = useMediaPanelStore.getState();
+
+ const { _tracks } = get();
+ const element = _tracks
+ .flatMap((track) => track.elements)
+ .find((el) => el.id === elementId);
+
+ if (element?.type === "media") {
+ requestRevealMedia(element.mediaId);
+ }
+ },
+
+ replaceElementWithFile: async (trackId, elementId, file) => {
+ try {
+ const result = await get().replaceElementMedia(
+ trackId,
+ elementId,
+ file
+ );
+ if (result.success) {
+ const { toast } = await import("sonner");
+ toast.success("Clip replaced successfully");
+ } else {
+ const { toast } = await import("sonner");
+ toast.error(result.error || "Failed to replace clip");
+ }
+ } catch (error) {
+ console.error("Unexpected error replacing clip:", error);
+ const { toast } = await import("sonner");
+ toast.error(
+ `Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`
+ );
+ }
+ },
+
+ getContextMenuState: (trackId, elementId) => {
+ const { selectedElements, _tracks } = get();
+ const { currentTime } = usePlaybackStore.getState();
+ const { mediaFiles } = useMediaStore.getState();
+
+ const isMultipleSelected = selectedElements.length > 1;
+ const isCurrentElementSelected = selectedElements.some(
+ (sel) => sel.trackId === trackId && sel.elementId === elementId
+ );
+
+ const hasAudioElements = selectedElements.some(
+ ({ trackId: tId, elementId: eId }) => {
+ const selectedTrack = _tracks.find((t) => t.id === tId);
+ const selectedElement = selectedTrack?.elements.find(
+ (e) => e.id === eId
+ );
+ if (selectedElement?.type !== "media") return false;
+ const mediaElement = selectedElement as MediaElement;
+ const mediaItem = mediaFiles.find(
+ (file: MediaFile) => file.id === mediaElement.mediaId
+ );
+ return mediaItem?.type === "audio" || mediaItem?.type === "video";
+ }
+ );
+
+ const canSplitSelected = selectedElements.every(
+ ({ trackId: tId, elementId: eId }) => {
+ const selectedTrack = _tracks.find((t) => t.id === tId);
+ const selectedElement = selectedTrack?.elements.find(
+ (e) => e.id === eId
+ );
+ if (!selectedElement) return false;
+ const effectiveStart = selectedElement.startTime;
+ const effectiveEnd =
+ selectedElement.startTime +
+ (selectedElement.duration -
+ selectedElement.trimStart -
+ selectedElement.trimEnd);
+ return currentTime > effectiveStart && currentTime < effectiveEnd;
+ }
+ );
+
+ return {
+ isMultipleSelected,
+ isCurrentElementSelected,
+ hasAudioElements,
+ canSplitSelected,
+ currentTime,
+ };
+ },
};
});