chore: normalize line endings

This commit is contained in:
Maze Winther
2026-03-29 15:56:48 +02:00
parent 82817cb50c
commit 4d127a038b
736 changed files with 73880 additions and 73782 deletions
File diff suppressed because it is too large Load Diff
+49 -49
View File
@@ -1,49 +1,49 @@
import type { Command } from "@/lib/commands";
export class CommandManager {
private history: Command[] = [];
private redoStack: Command[] = [];
execute({ command }: { command: Command }): Command {
command.execute();
this.history.push(command);
this.redoStack = [];
return command;
}
push({ command }: { command: Command }): void {
this.history.push(command);
this.redoStack = [];
}
undo(): void {
if (this.history.length === 0) return;
const command = this.history.pop();
command?.undo();
if (command) {
this.redoStack.push(command);
}
}
redo(): void {
if (this.redoStack.length === 0) return;
const command = this.redoStack.pop();
command?.redo();
if (command) {
this.history.push(command);
}
}
canUndo(): boolean {
return this.history.length > 0;
}
canRedo(): boolean {
return this.redoStack.length > 0;
}
clear(): void {
this.history = [];
this.redoStack = [];
}
}
import type { Command } from "@/lib/commands";
export class CommandManager {
private history: Command[] = [];
private redoStack: Command[] = [];
execute({ command }: { command: Command }): Command {
command.execute();
this.history.push(command);
this.redoStack = [];
return command;
}
push({ command }: { command: Command }): void {
this.history.push(command);
this.redoStack = [];
}
undo(): void {
if (this.history.length === 0) return;
const command = this.history.pop();
command?.undo();
if (command) {
this.redoStack.push(command);
}
}
redo(): void {
if (this.redoStack.length === 0) return;
const command = this.redoStack.pop();
command?.redo();
if (command) {
this.history.push(command);
}
}
canUndo(): boolean {
return this.history.length > 0;
}
canRedo(): boolean {
return this.redoStack.length > 0;
}
clear(): void {
this.history = [];
this.redoStack = [];
}
}
@@ -30,6 +30,9 @@ export class MediaManager {
try {
await storageService.saveMediaAsset({ projectId, mediaAsset: newAsset });
this.editor.project.ratchetFpsForImportedMedia({
importedAssets: [newAsset],
});
return newAsset;
} catch (error) {
console.error("Failed to save media asset:", error);
+12 -12
View File
@@ -13,19 +13,19 @@ export class PlaybackManager {
constructor(private editor: EditorCore) {
this.editor.timeline.subscribe(() => {
const duration = this.editor.timeline.getTotalDuration();
if (this.currentTime > duration && duration > 0) {
this.currentTime = duration;
const maxTime = this.editor.timeline.getLastSeekableTime();
if (this.currentTime > maxTime && maxTime > 0) {
this.currentTime = maxTime;
this.notify();
}
});
}
play(): void {
const duration = this.editor.timeline.getTotalDuration();
const maxTime = this.editor.timeline.getLastSeekableTime();
if (duration > 0) {
if (this.currentTime >= duration) {
if (maxTime > 0) {
if (this.currentTime >= maxTime) {
this.seek({ time: 0 });
}
}
@@ -50,8 +50,8 @@ export class PlaybackManager {
}
seek({ time }: { time: number }): void {
const duration = this.editor.timeline.getTotalDuration();
this.currentTime = Math.max(0, Math.min(duration, time));
const maxTime = this.editor.timeline.getLastSeekableTime();
this.currentTime = Math.max(0, Math.min(maxTime, time));
this.notify();
window.dispatchEvent(
@@ -154,16 +154,16 @@ export class PlaybackManager {
this.lastUpdate = now;
const newTime = this.currentTime + delta;
const duration = this.editor.timeline.getTotalDuration();
const maxTime = this.editor.timeline.getLastSeekableTime();
if (duration > 0 && newTime >= duration) {
if (maxTime > 0 && newTime >= maxTime) {
this.pause();
this.currentTime = duration;
this.currentTime = maxTime;
this.notify();
window.dispatchEvent(
new CustomEvent("playback-seek", {
detail: { time: duration },
detail: { time: maxTime },
}),
);
} else {
@@ -29,6 +29,8 @@ import {
import { loadFonts } from "@/lib/fonts/google-fonts";
import { DEFAULTS } from "@/lib/timeline/defaults";
import { getElementFontFamilies } from "@/lib/timeline/element-utils";
import { getRaisedProjectFpsForImportedMedia } from "@/lib/project/fps";
import type { MediaAsset } from "@/lib/media/types";
export interface MigrationState {
isMigrating: boolean;
@@ -492,6 +494,23 @@ export class ProjectManager {
command.execute();
}
ratchetFpsForImportedMedia({
importedAssets,
}: {
importedAssets: Array<Pick<MediaAsset, "type" | "fps">>;
}): number | null {
if (!this.active) return null;
const nextFps = getRaisedProjectFpsForImportedMedia({
currentFps: this.active.settings.fps,
importedAssets,
});
if (nextFps === null) return null;
new UpdateProjectSettingsCommand({ fps: nextFps }).execute();
return nextFps;
}
async updateThumbnail({ thumbnail }: { thumbnail: string }): Promise<void> {
if (!this.active) return;
@@ -5,7 +5,7 @@ import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
import { SceneExporter } from "@/services/renderer/scene-exporter";
import { buildScene } from "@/services/renderer/scene-builder";
import { createTimelineAudioBuffer } from "@/lib/media/audio";
import { formatTimeCode, getLastFrameTime } from "@/lib/time";
import { formatTimeCode } from "@/lib/time";
import { downloadBlob } from "@/utils/browser";
type SnapshotResult =
@@ -81,9 +81,7 @@ export class RendererManager {
}
const { canvasSize, fps } = activeProject.settings;
const currentTime = this.editor.playback.getCurrentTime();
const lastFrameTime = getLastFrameTime({ duration, fps });
const renderTime = Math.min(currentTime, lastFrameTime);
const renderTime = this.editor.playback.getCurrentTime();
const renderer = new CanvasRenderer({
width: canvasSize.width,
+107 -107
View File
@@ -1,107 +1,107 @@
import type { EditorCore } from "@/core";
type SaveManagerOptions = {
debounceMs?: number;
};
export class SaveManager {
private debounceMs: number;
private isPaused = false;
private isSaving = false;
private hasPendingSave = false;
private saveTimer: ReturnType<typeof setTimeout> | null = null;
private unsubscribeHandlers: Array<() => void> = [];
constructor(
private editor: EditorCore,
{ debounceMs = 800 }: SaveManagerOptions = {},
) {
this.debounceMs = debounceMs;
}
start(): void {
if (this.unsubscribeHandlers.length > 0) return;
this.unsubscribeHandlers = [
this.editor.scenes.subscribe(() => {
this.markDirty();
}),
this.editor.timeline.subscribe(() => {
this.markDirty();
}),
];
}
stop(): void {
for (const unsubscribe of this.unsubscribeHandlers) {
unsubscribe();
}
this.unsubscribeHandlers = [];
this.clearTimer();
}
pause(): void {
this.isPaused = true;
}
resume(): void {
this.isPaused = false;
if (this.hasPendingSave) {
this.queueSave();
}
}
markDirty({ force = false }: { force?: boolean } = {}): void {
if (this.isPaused && !force) return;
this.hasPendingSave = true;
this.queueSave();
}
async flush(): Promise<void> {
this.hasPendingSave = true;
await this.saveNow();
}
getIsDirty(): boolean {
return this.hasPendingSave || this.isSaving;
}
private queueSave(): void {
if (this.isSaving) return;
if (this.saveTimer) {
clearTimeout(this.saveTimer);
}
this.saveTimer = setTimeout(() => {
void this.saveNow();
}, this.debounceMs);
}
private async saveNow(): Promise<void> {
if (this.isSaving) return;
if (!this.hasPendingSave) return;
const activeProject = this.editor.project.getActive();
if (!activeProject) return;
if (this.editor.project.getIsLoading()) return;
if (this.editor.project.getMigrationState().isMigrating) return;
this.isSaving = true;
this.hasPendingSave = false;
this.clearTimer();
try {
await this.editor.project.saveCurrentProject();
} finally {
this.isSaving = false;
if (this.hasPendingSave) {
this.queueSave();
}
}
}
private clearTimer(): void {
if (!this.saveTimer) return;
clearTimeout(this.saveTimer);
this.saveTimer = null;
}
}
import type { EditorCore } from "@/core";
type SaveManagerOptions = {
debounceMs?: number;
};
export class SaveManager {
private debounceMs: number;
private isPaused = false;
private isSaving = false;
private hasPendingSave = false;
private saveTimer: ReturnType<typeof setTimeout> | null = null;
private unsubscribeHandlers: Array<() => void> = [];
constructor(
private editor: EditorCore,
{ debounceMs = 800 }: SaveManagerOptions = {},
) {
this.debounceMs = debounceMs;
}
start(): void {
if (this.unsubscribeHandlers.length > 0) return;
this.unsubscribeHandlers = [
this.editor.scenes.subscribe(() => {
this.markDirty();
}),
this.editor.timeline.subscribe(() => {
this.markDirty();
}),
];
}
stop(): void {
for (const unsubscribe of this.unsubscribeHandlers) {
unsubscribe();
}
this.unsubscribeHandlers = [];
this.clearTimer();
}
pause(): void {
this.isPaused = true;
}
resume(): void {
this.isPaused = false;
if (this.hasPendingSave) {
this.queueSave();
}
}
markDirty({ force = false }: { force?: boolean } = {}): void {
if (this.isPaused && !force) return;
this.hasPendingSave = true;
this.queueSave();
}
async flush(): Promise<void> {
this.hasPendingSave = true;
await this.saveNow();
}
getIsDirty(): boolean {
return this.hasPendingSave || this.isSaving;
}
private queueSave(): void {
if (this.isSaving) return;
if (this.saveTimer) {
clearTimeout(this.saveTimer);
}
this.saveTimer = setTimeout(() => {
void this.saveNow();
}, this.debounceMs);
}
private async saveNow(): Promise<void> {
if (this.isSaving) return;
if (!this.hasPendingSave) return;
const activeProject = this.editor.project.getActive();
if (!activeProject) return;
if (this.editor.project.getIsLoading()) return;
if (this.editor.project.getMigrationState().isMigrating) return;
this.isSaving = true;
this.hasPendingSave = false;
this.clearTimer();
try {
await this.editor.project.saveCurrentProject();
} finally {
this.isSaving = false;
if (this.hasPendingSave) {
this.queueSave();
}
}
}
private clearTimer(): void {
if (!this.saveTimer) return;
clearTimeout(this.saveTimer);
this.saveTimer = null;
}
}
+363 -363
View File
@@ -1,363 +1,363 @@
import type { EditorCore } from "@/core";
import type { TimelineTrack, 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 { ensureMainTrack } from "@/lib/timeline/track-utils";
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 { scenes: ensuredScenes, hasAddedMainTrack } =
this.ensureScenesHaveMainTrack({
scenes: result.project.scenes ?? [],
});
const currentScene = findCurrentScene({
scenes: ensuredScenes,
currentSceneId: result.project.currentSceneId,
});
this.list = ensuredScenes;
this.active = currentScene;
this.notify();
if (hasAddedMainTrack) {
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 });
}
}
}
} 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 { scenes: scenesWithMainTracks, hasAddedMainTrack } =
this.ensureScenesHaveMainTrack({ scenes: ensuredScenes });
const currentScene = currentSceneId
? scenesWithMainTracks.find((s) => s.id === currentSceneId)
: null;
const fallbackScene = getMainScene({ scenes: scenesWithMainTracks });
this.list = scenesWithMainTracks;
this.active = currentScene || fallbackScene;
this.notify();
const hasAddedMainScene = ensuredScenes.length > scenes.length;
if (hasAddedMainScene || hasAddedMainTrack) {
const activeProject = this.editor.project.getActive();
if (activeProject) {
const updatedProject = {
...activeProject,
scenes: scenesWithMainTracks,
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;
}
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: TimelineTrack[] }): 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 });
}
}
private ensureScenesHaveMainTrack({ scenes }: { scenes: TScene[] }): {
scenes: TScene[];
hasAddedMainTrack: boolean;
} {
let hasAddedMainTrack = false;
const ensuredScenes: TScene[] = [];
for (const scene of scenes) {
const existingTracks = scene.tracks ?? [];
const updatedTracks = ensureMainTrack({ tracks: existingTracks });
if (updatedTracks !== existingTracks) {
hasAddedMainTrack = true;
ensuredScenes.push({
...scene,
tracks: updatedTracks,
updatedAt: new Date(),
});
} else {
ensuredScenes.push(scene);
}
}
return { scenes: ensuredScenes, hasAddedMainTrack };
}
}
import type { EditorCore } from "@/core";
import type { TimelineTrack, 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 { ensureMainTrack } from "@/lib/timeline/track-utils";
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 { scenes: ensuredScenes, hasAddedMainTrack } =
this.ensureScenesHaveMainTrack({
scenes: result.project.scenes ?? [],
});
const currentScene = findCurrentScene({
scenes: ensuredScenes,
currentSceneId: result.project.currentSceneId,
});
this.list = ensuredScenes;
this.active = currentScene;
this.notify();
if (hasAddedMainTrack) {
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 });
}
}
}
} 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 { scenes: scenesWithMainTracks, hasAddedMainTrack } =
this.ensureScenesHaveMainTrack({ scenes: ensuredScenes });
const currentScene = currentSceneId
? scenesWithMainTracks.find((s) => s.id === currentSceneId)
: null;
const fallbackScene = getMainScene({ scenes: scenesWithMainTracks });
this.list = scenesWithMainTracks;
this.active = currentScene || fallbackScene;
this.notify();
const hasAddedMainScene = ensuredScenes.length > scenes.length;
if (hasAddedMainScene || hasAddedMainTrack) {
const activeProject = this.editor.project.getActive();
if (activeProject) {
const updatedProject = {
...activeProject,
scenes: scenesWithMainTracks,
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;
}
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: TimelineTrack[] }): 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 });
}
}
private ensureScenesHaveMainTrack({ scenes }: { scenes: TScene[] }): {
scenes: TScene[];
hasAddedMainTrack: boolean;
} {
let hasAddedMainTrack = false;
const ensuredScenes: TScene[] = [];
for (const scene of scenes) {
const existingTracks = scene.tracks ?? [];
const updatedTracks = ensureMainTrack({ tracks: existingTracks });
if (updatedTracks !== existingTracks) {
hasAddedMainTrack = true;
ensuredScenes.push({
...scene,
tracks: updatedTracks,
updatedAt: new Date(),
});
} else {
ensuredScenes.push(scene);
}
}
return { scenes: ensuredScenes, hasAddedMainTrack };
}
}
+73 -73
View File
@@ -1,73 +1,73 @@
import type { EditorCore } from "@/core";
import type { SelectedKeyframeRef } from "@/lib/animation/types";
import type { ElementRef } from "@/lib/timeline/types";
export class SelectionManager {
private selectedElements: ElementRef[] = [];
private selectedKeyframes: SelectedKeyframeRef[] = [];
private keyframeSelectionAnchor: SelectedKeyframeRef | null = null;
private listeners = new Set<() => void>();
constructor(editor: EditorCore) {
void editor;
}
getSelectedElements(): ElementRef[] {
return this.selectedElements;
}
getSelectedKeyframes(): SelectedKeyframeRef[] {
return this.selectedKeyframes;
}
getKeyframeSelectionAnchor(): SelectedKeyframeRef | null {
return this.keyframeSelectionAnchor;
}
setSelectedElements({ elements }: { elements: ElementRef[] }): void {
this.selectedElements = elements;
this.selectedKeyframes = [];
this.keyframeSelectionAnchor = null;
this.notify();
}
setSelectedKeyframes({
keyframes,
anchorKeyframe,
}: {
keyframes: SelectedKeyframeRef[];
anchorKeyframe?: SelectedKeyframeRef | null;
}): void {
this.selectedKeyframes = keyframes;
if (anchorKeyframe !== undefined) {
this.keyframeSelectionAnchor = anchorKeyframe;
} else if (keyframes.length === 0) {
this.keyframeSelectionAnchor = null;
}
this.notify();
}
clearSelection(): void {
this.selectedElements = [];
this.selectedKeyframes = [];
this.keyframeSelectionAnchor = null;
this.notify();
}
clearKeyframeSelection(): void {
this.selectedKeyframes = [];
this.keyframeSelectionAnchor = null;
this.notify();
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private notify(): void {
this.listeners.forEach((fn) => {
fn();
});
}
}
import type { EditorCore } from "@/core";
import type { SelectedKeyframeRef } from "@/lib/animation/types";
import type { ElementRef } from "@/lib/timeline/types";
export class SelectionManager {
private selectedElements: ElementRef[] = [];
private selectedKeyframes: SelectedKeyframeRef[] = [];
private keyframeSelectionAnchor: SelectedKeyframeRef | null = null;
private listeners = new Set<() => void>();
constructor(editor: EditorCore) {
void editor;
}
getSelectedElements(): ElementRef[] {
return this.selectedElements;
}
getSelectedKeyframes(): SelectedKeyframeRef[] {
return this.selectedKeyframes;
}
getKeyframeSelectionAnchor(): SelectedKeyframeRef | null {
return this.keyframeSelectionAnchor;
}
setSelectedElements({ elements }: { elements: ElementRef[] }): void {
this.selectedElements = elements;
this.selectedKeyframes = [];
this.keyframeSelectionAnchor = null;
this.notify();
}
setSelectedKeyframes({
keyframes,
anchorKeyframe,
}: {
keyframes: SelectedKeyframeRef[];
anchorKeyframe?: SelectedKeyframeRef | null;
}): void {
this.selectedKeyframes = keyframes;
if (anchorKeyframe !== undefined) {
this.keyframeSelectionAnchor = anchorKeyframe;
} else if (keyframes.length === 0) {
this.keyframeSelectionAnchor = null;
}
this.notify();
}
clearSelection(): void {
this.selectedElements = [];
this.selectedKeyframes = [];
this.keyframeSelectionAnchor = null;
this.notify();
}
clearKeyframeSelection(): void {
this.selectedKeyframes = [];
this.keyframeSelectionAnchor = null;
this.notify();
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private notify(): void {
this.listeners.forEach((fn) => {
fn();
});
}
}
@@ -13,6 +13,7 @@ import type {
AnimationValue,
} from "@/lib/animation/types";
import { calculateTotalDuration } from "@/lib/timeline";
import { getLastFrameTime } from "@/lib/time";
import {
AddTrackCommand,
RemoveTrackCommand,
@@ -224,6 +225,13 @@ export class TimelineManager {
return calculateTotalDuration({ tracks: this.getTracks() });
}
getLastSeekableTime(): number {
const duration = this.getTotalDuration();
const fps = this.editor.project.getActive()?.settings.fps;
if (!fps || duration <= 0) return duration;
return getLastFrameTime({ duration, fps });
}
getTrackById({ trackId }: { trackId: string }): TimelineTrack | null {
return this.getTracks().find((track) => track.id === trackId) ?? null;
}