OpenCut/apps/web/src/core/managers/scenes-manager.ts

321 lines
7.5 KiB
TypeScript

import type { EditorCore } from "@/core";
import type { SceneTracks, TScene } from "@/lib/timeline";
import { storageService } from "@/services/storage/service";
import {
getMainScene,
ensureMainScene,
canDeleteScene,
findCurrentScene,
} from "@/lib/scenes";
import {
getBookmarkAtTime,
getFrameTime,
isBookmarkAtTime,
} from "@/lib/timeline/bookmarks";
import {
CreateSceneCommand,
DeleteSceneCommand,
MoveBookmarkCommand,
RemoveBookmarkCommand,
RenameSceneCommand,
ToggleBookmarkCommand,
UpdateBookmarkCommand,
} from "@/lib/commands/scene";
export class ScenesManager {
private active: TScene | null = null;
private list: TScene[] = [];
private listeners = new Set<() => void>();
constructor(private editor: EditorCore) {}
async createScene({
name,
isMain = false,
}: {
name: string;
isMain: boolean;
}): Promise<string> {
if (!this.editor.project.getActive()) {
throw new Error("No active project");
}
const command = new CreateSceneCommand(name, isMain);
this.editor.command.execute({ command });
return command.getSceneId();
}
async deleteScene({ sceneId }: { sceneId: string }): Promise<void> {
const sceneToDelete = this.list.find((s) => s.id === sceneId);
if (!sceneToDelete) {
throw new Error("Scene not found");
}
const { canDelete, reason } = canDeleteScene({ scene: sceneToDelete });
if (!canDelete) {
throw new Error(reason);
}
if (!this.editor.project.getActive()) {
throw new Error("No active project");
}
const command = new DeleteSceneCommand(sceneId);
this.editor.command.execute({ command });
}
async renameScene({
sceneId,
name,
}: {
sceneId: string;
name: string;
}): Promise<void> {
if (!this.editor.project.getActive()) {
throw new Error("No active project");
}
const command = new RenameSceneCommand(sceneId, name);
this.editor.command.execute({ command });
}
async switchToScene({ sceneId }: { sceneId: string }): Promise<void> {
const targetScene = this.list.find((s) => s.id === sceneId);
if (!targetScene) {
throw new Error("Scene not found");
}
const activeProject = this.editor.project.getActive();
if (activeProject) {
const updatedProject = {
...activeProject,
currentSceneId: sceneId,
metadata: {
...activeProject.metadata,
updatedAt: new Date(),
},
};
this.editor.project.setActiveProject({ project: updatedProject });
}
this.active = targetScene;
this.notify();
}
async toggleBookmark({ time }: { time: number }): Promise<void> {
const command = new ToggleBookmarkCommand(time);
this.editor.command.execute({ command });
}
isBookmarked({ time }: { time: number }): boolean {
const activeScene = this.getActiveScene();
const activeProject = this.editor.project.getActive();
if (!activeScene || !this.active || !activeProject) return false;
const frameTime = getFrameTime({
time,
fps: activeProject.settings.fps,
});
return isBookmarkAtTime({ bookmarks: activeScene.bookmarks, frameTime });
}
async removeBookmark({ time }: { time: number }): Promise<void> {
const command = new RemoveBookmarkCommand(time);
this.editor.command.execute({ command });
}
async updateBookmark({
time,
updates,
}: {
time: number;
updates: Partial<{ note: string; color: string; duration: number }>;
}): Promise<void> {
const command = new UpdateBookmarkCommand(time, updates);
this.editor.command.execute({ command });
}
async moveBookmark({
fromTime,
toTime,
}: {
fromTime: number;
toTime: number;
}): Promise<void> {
const command = new MoveBookmarkCommand(fromTime, toTime);
this.editor.command.execute({ command });
}
getBookmarkAtTime({ time }: { time: number }) {
const activeScene = this.active;
const activeProject = this.editor.project.getActive();
if (!activeScene || !activeProject) return null;
const frameTime = getFrameTime({
time,
fps: activeProject.settings.fps,
});
return getBookmarkAtTime({
bookmarks: activeScene.bookmarks,
frameTime,
});
}
async loadProjectScenes({ projectId }: { projectId: string }): Promise<void> {
try {
const result = await storageService.loadProject({ id: projectId });
if (result?.project.scenes) {
const ensuredScenes = result.project.scenes ?? [];
const currentScene = findCurrentScene({
scenes: ensuredScenes,
currentSceneId: result.project.currentSceneId,
});
this.list = ensuredScenes;
this.active = currentScene;
this.notify();
}
} catch (error) {
console.error("Failed to load project scenes:", error);
this.list = [];
this.active = null;
this.notify();
}
}
initializeScenes({
scenes,
currentSceneId,
}: {
scenes: TScene[];
currentSceneId?: string;
}): void {
const ensuredScenes = ensureMainScene({ scenes });
const currentScene = currentSceneId
? ensuredScenes.find((s) => s.id === currentSceneId)
: null;
const fallbackScene = getMainScene({ scenes: ensuredScenes });
this.list = ensuredScenes;
this.active = currentScene || fallbackScene;
this.notify();
const hasAddedMainScene = ensuredScenes.length > scenes.length;
if (hasAddedMainScene) {
const activeProject = this.editor.project.getActive();
if (activeProject) {
const updatedProject = {
...activeProject,
scenes: ensuredScenes,
metadata: {
...activeProject.metadata,
updatedAt: new Date(),
},
};
this.editor.project.setActiveProject({ project: updatedProject });
this.editor.save.markDirty({ force: true });
}
}
}
clearScenes(): void {
this.list = [];
this.active = null;
this.notify();
}
getActiveScene(): TScene {
if (!this.active) {
throw new Error("No active scene.");
}
return this.active;
}
getActiveSceneOrNull(): TScene | null {
return this.active;
}
getScenes(): TScene[] {
return this.list;
}
setScenes({
scenes,
activeSceneId,
}: {
scenes: TScene[];
activeSceneId?: string;
}): void {
this.list = scenes;
const nextActiveSceneId = activeSceneId ?? this.active?.id ?? null;
this.active = nextActiveSceneId
? (scenes.find((scene) => scene.id === nextActiveSceneId) ?? null)
: null;
this.notify();
const activeProject = this.editor.project.getActive();
if (activeProject) {
const updatedProject = {
...activeProject,
scenes,
metadata: {
...activeProject.metadata,
updatedAt: new Date(),
},
};
this.editor.project.setActiveProject({ project: updatedProject });
}
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private notify(): void {
this.listeners.forEach((fn) => {
fn();
});
}
updateSceneTracks({ tracks }: { tracks: SceneTracks }): void {
if (!this.active) return;
const updatedScene: TScene = {
...this.active,
tracks,
updatedAt: new Date(),
};
this.list = this.list.map((s) =>
s.id === this.active?.id ? updatedScene : s,
);
this.active = updatedScene;
this.notify();
const activeProject = this.editor.project.getActive();
if (activeProject) {
const updatedProject = {
...activeProject,
scenes: this.list,
metadata: {
...activeProject.metadata,
updatedAt: new Date(),
},
};
this.editor.project.setActiveProject({ project: updatedProject });
}
}
}