mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
fuck malware, fuck you
This commit is contained in:
@@ -5,6 +5,7 @@ import { ProjectManager } from "./managers/project-manager";
|
||||
import { MediaManager } from "./managers/media-manager";
|
||||
import { RendererManager } from "./managers/renderer-manager";
|
||||
import { CommandManager } from "./managers/commands";
|
||||
import { SaveManager } from "./managers/save-manager";
|
||||
import { buildScene } from "@/services/renderer/scene-builder";
|
||||
import { SceneExporter } from "@/services/renderer/scene-exporter";
|
||||
import type { ExportOptions } from "@/types/export";
|
||||
@@ -19,6 +20,7 @@ export class EditorCore {
|
||||
public readonly project: ProjectManager;
|
||||
public readonly media: MediaManager;
|
||||
public readonly renderer: RendererManager;
|
||||
public readonly save: SaveManager;
|
||||
|
||||
private constructor() {
|
||||
this.command = new CommandManager();
|
||||
@@ -28,6 +30,8 @@ export class EditorCore {
|
||||
this.project = new ProjectManager(this);
|
||||
this.media = new MediaManager(this);
|
||||
this.renderer = new RendererManager(this);
|
||||
this.save = new SaveManager(this);
|
||||
this.save.start();
|
||||
}
|
||||
|
||||
static getInstance(): EditorCore {
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { TimelineElement } from "@/types/timeline";
|
||||
import { storageService } from "@/lib/storage/storage-service";
|
||||
import { toast } from "sonner";
|
||||
import { generateUUID } from "@/lib/utils";
|
||||
import { UpdateProjectSettingsCommand } from "@/lib/commands/project";
|
||||
import {
|
||||
DEFAULT_FPS,
|
||||
DEFAULT_CANVAS_SIZE,
|
||||
@@ -130,6 +131,7 @@ export class ProjectManager {
|
||||
this.notify();
|
||||
}
|
||||
|
||||
this.editor.save.pause();
|
||||
await this.ensureStorageMigrations();
|
||||
this.editor.media.clearAllAssets();
|
||||
this.editor.scenes.clearScenes();
|
||||
@@ -159,6 +161,7 @@ export class ProjectManager {
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.notify();
|
||||
this.editor.save.resume();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,27 +342,20 @@ export class ProjectManager {
|
||||
|
||||
async updateSettings({
|
||||
settings,
|
||||
pushHistory = true,
|
||||
}: {
|
||||
settings: Partial<TProjectSettings>;
|
||||
pushHistory?: boolean;
|
||||
}): Promise<void> {
|
||||
if (!this.active) return;
|
||||
|
||||
const updatedProject: TProject = {
|
||||
...this.active,
|
||||
settings: { ...this.active.settings, ...settings },
|
||||
metadata: { ...this.active.metadata, updatedAt: new Date() },
|
||||
};
|
||||
|
||||
try {
|
||||
await storageService.saveProject({ project: updatedProject });
|
||||
this.active = updatedProject;
|
||||
this.notify();
|
||||
} catch (error) {
|
||||
console.error("Failed to update settings:", error);
|
||||
toast.error("Failed to update settings", {
|
||||
description: "Please try again",
|
||||
});
|
||||
const command = new UpdateProjectSettingsCommand(settings);
|
||||
if (pushHistory) {
|
||||
this.editor.command.execute({ command });
|
||||
return;
|
||||
}
|
||||
|
||||
command.execute();
|
||||
}
|
||||
|
||||
async updateThumbnail({ thumbnail }: { thumbnail: string }): Promise<void> {
|
||||
@@ -369,15 +365,10 @@ export class ProjectManager {
|
||||
...this.active,
|
||||
metadata: { ...this.active.metadata, thumbnail, updatedAt: new Date() },
|
||||
};
|
||||
|
||||
try {
|
||||
await storageService.saveProject({ project: updatedProject });
|
||||
this.active = updatedProject;
|
||||
this.notify();
|
||||
this.updateMetadata(updatedProject);
|
||||
} catch (error) {
|
||||
console.error("Failed to update thumbnail:", error);
|
||||
}
|
||||
this.active = updatedProject;
|
||||
this.notify();
|
||||
this.updateMetadata(updatedProject);
|
||||
this.editor.save.markDirty();
|
||||
}
|
||||
|
||||
async prepareExit(): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
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();
|
||||
}
|
||||
|
||||
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.getActiveOrNull();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,24 @@
|
||||
import type { EditorCore } from "@/core";
|
||||
import type { TScene } from "@/types/timeline";
|
||||
import type { TimelineTrack, TScene } from "@/types/timeline";
|
||||
import { storageService } from "@/lib/storage/storage-service";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
updateSceneInArray,
|
||||
getMainScene,
|
||||
ensureMainScene,
|
||||
buildDefaultScene,
|
||||
canDeleteScene,
|
||||
getFallbackSceneAfterDelete,
|
||||
findCurrentScene,
|
||||
} from "@/lib/scene-utils";
|
||||
import {
|
||||
getFrameTime,
|
||||
toggleBookmarkInArray,
|
||||
removeBookmarkFromArray,
|
||||
isBookmarkAtTime,
|
||||
} from "@/lib/timeline/bookmark-utils";
|
||||
import { ensureMainTrack } from "@/lib/timeline/track-utils";
|
||||
import {
|
||||
CreateSceneCommand,
|
||||
DeleteSceneCommand,
|
||||
RemoveBookmarkCommand,
|
||||
RenameSceneCommand,
|
||||
ToggleBookmarkCommand,
|
||||
} from "@/lib/commands/scene";
|
||||
|
||||
export class ScenesManager {
|
||||
private active: TScene | null = null;
|
||||
@@ -33,16 +34,13 @@ export class ScenesManager {
|
||||
name: string;
|
||||
isMain: boolean;
|
||||
}): Promise<string> {
|
||||
const newScene = buildDefaultScene({ name, isMain });
|
||||
const updatedScenes = [...this.list, newScene];
|
||||
|
||||
try {
|
||||
await this.updateProjectWithScenes({ updatedScenes });
|
||||
return newScene.id;
|
||||
} catch (error) {
|
||||
console.error("Failed to create scene:", error);
|
||||
throw error;
|
||||
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> {
|
||||
@@ -57,23 +55,12 @@ export class ScenesManager {
|
||||
throw new Error(reason);
|
||||
}
|
||||
|
||||
const updatedScenes = this.list.filter((s) => s.id !== sceneId);
|
||||
|
||||
const newCurrentScene = getFallbackSceneAfterDelete({
|
||||
scenes: updatedScenes,
|
||||
deletedSceneId: sceneId,
|
||||
currentSceneId: this.active?.id || null,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.updateProjectWithScenes({
|
||||
updatedScenes,
|
||||
updatedSceneId: newCurrentScene?.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to delete scene:", error);
|
||||
throw error;
|
||||
if (!this.editor.project.getActive()) {
|
||||
throw new Error("No active project");
|
||||
}
|
||||
|
||||
const command = new DeleteSceneCommand(sceneId);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
async renameScene({
|
||||
@@ -83,21 +70,12 @@ export class ScenesManager {
|
||||
sceneId: string;
|
||||
name: string;
|
||||
}): Promise<void> {
|
||||
const updatedScenes = updateSceneInArray({
|
||||
scenes: this.list,
|
||||
sceneId,
|
||||
updates: { name, updatedAt: new Date() },
|
||||
});
|
||||
|
||||
try {
|
||||
await this.updateProjectWithScenes({
|
||||
updatedScenes,
|
||||
updatedSceneId: sceneId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to rename scene:", error);
|
||||
throw error;
|
||||
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> {
|
||||
@@ -119,7 +97,6 @@ export class ScenesManager {
|
||||
},
|
||||
};
|
||||
|
||||
await storageService.saveProject({ project: updatedProject });
|
||||
this.editor.project.setActiveProject({ project: updatedProject });
|
||||
}
|
||||
|
||||
@@ -128,39 +105,8 @@ export class ScenesManager {
|
||||
}
|
||||
|
||||
async toggleBookmark({ time }: { time: number }): Promise<void> {
|
||||
const activeScene = this.getActiveScene();
|
||||
if (!activeScene || !this.active) return;
|
||||
|
||||
const activeProject = this.editor.project.getActive();
|
||||
if (!activeProject) return;
|
||||
|
||||
const frameTime = getFrameTime({
|
||||
time,
|
||||
fps: activeProject.settings.fps,
|
||||
});
|
||||
|
||||
const updatedBookmarks = toggleBookmarkInArray({
|
||||
bookmarks: activeScene.bookmarks,
|
||||
frameTime,
|
||||
});
|
||||
|
||||
const updatedScenes = updateSceneInArray({
|
||||
scenes: this.list,
|
||||
sceneId: activeScene.id,
|
||||
updates: { bookmarks: updatedBookmarks },
|
||||
});
|
||||
|
||||
try {
|
||||
await this.updateProjectWithScenes({
|
||||
updatedScenes,
|
||||
updatedSceneId: activeScene.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update scene bookmarks:", error);
|
||||
toast.error("Failed to update bookmarks", {
|
||||
description: "Please try again",
|
||||
});
|
||||
}
|
||||
const command = new ToggleBookmarkCommand(time);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
isBookmarked({ time }: { time: number }): boolean {
|
||||
@@ -178,43 +124,8 @@ export class ScenesManager {
|
||||
}
|
||||
|
||||
async removeBookmark({ time }: { time: number }): Promise<void> {
|
||||
const activeScene = this.getActiveScene();
|
||||
if (!activeScene || !this.active) return;
|
||||
|
||||
const activeProject = this.editor.project.getActive();
|
||||
if (!activeProject) return;
|
||||
|
||||
const frameTime = getFrameTime({
|
||||
time,
|
||||
fps: activeProject.settings.fps,
|
||||
});
|
||||
|
||||
const updatedBookmarks = removeBookmarkFromArray({
|
||||
bookmarks: activeScene.bookmarks,
|
||||
frameTime,
|
||||
});
|
||||
|
||||
if (updatedBookmarks.length === activeScene.bookmarks.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedScenes = updateSceneInArray({
|
||||
scenes: this.list,
|
||||
sceneId: activeScene.id,
|
||||
updates: { bookmarks: updatedBookmarks },
|
||||
});
|
||||
|
||||
try {
|
||||
await this.updateProjectWithScenes({
|
||||
updatedScenes,
|
||||
updatedSceneId: activeScene.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update scene bookmarks:", error);
|
||||
toast.error("Failed to remove bookmark", {
|
||||
description: "Please try again",
|
||||
});
|
||||
}
|
||||
const command = new RemoveBookmarkCommand(time);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
async loadProjectScenes({ projectId }: { projectId: string }): Promise<void> {
|
||||
@@ -245,8 +156,8 @@ export class ScenesManager {
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
};
|
||||
await storageService.saveProject({ project: updatedProject });
|
||||
this.editor.project.setActiveProject({ project: updatedProject });
|
||||
this.editor.save.markDirty({ force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,17 +203,8 @@ export class ScenesManager {
|
||||
},
|
||||
};
|
||||
|
||||
storageService
|
||||
.saveProject({ project: updatedProject })
|
||||
.then(() => {
|
||||
this.editor.project.setActiveProject({ project: updatedProject });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"Failed to save project with background scene:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
this.editor.project.setActiveProject({ project: updatedProject });
|
||||
this.editor.save.markDirty({ force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,9 +249,6 @@ export class ScenesManager {
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
};
|
||||
storageService.saveProject({ project: updatedProject }).catch((error) => {
|
||||
console.error("Failed to persist scenes:", error);
|
||||
});
|
||||
this.editor.project.setActiveProject({ project: updatedProject });
|
||||
}
|
||||
}
|
||||
@@ -366,7 +265,7 @@ export class ScenesManager {
|
||||
updateSceneTracks({
|
||||
tracks,
|
||||
}: {
|
||||
tracks: import("@/types/timeline").TimelineTrack[];
|
||||
tracks: TimelineTrack[];
|
||||
}): void {
|
||||
if (!this.active) return;
|
||||
|
||||
@@ -421,36 +320,4 @@ export class ScenesManager {
|
||||
return { scenes: ensuredScenes, hasAddedMainTrack };
|
||||
}
|
||||
|
||||
private async updateProjectWithScenes({
|
||||
updatedScenes,
|
||||
updatedSceneId,
|
||||
}: {
|
||||
updatedScenes: TScene[];
|
||||
updatedSceneId?: string;
|
||||
}): Promise<void> {
|
||||
const activeProject = this.editor.project.getActive();
|
||||
|
||||
if (!activeProject) {
|
||||
throw new Error("No active project");
|
||||
}
|
||||
|
||||
const updatedScene = updatedSceneId
|
||||
? updatedScenes.find((s) => s.id === updatedSceneId)
|
||||
: this.active;
|
||||
|
||||
const updatedProject = {
|
||||
...activeProject,
|
||||
scenes: updatedScenes,
|
||||
metadata: {
|
||||
...activeProject.metadata,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
await storageService.saveProject({ project: updatedProject });
|
||||
this.editor.project.setActiveProject({ project: updatedProject });
|
||||
this.list = updatedScenes;
|
||||
this.active = updatedScene || null;
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
AddTrackCommand,
|
||||
RemoveTrackCommand,
|
||||
ToggleTrackMuteCommand,
|
||||
ToggleTrackVisibilityCommand,
|
||||
AddElementToTrackCommand,
|
||||
UpdateElementTrimCommand,
|
||||
UpdateElementDurationCommand,
|
||||
@@ -54,20 +55,17 @@ export class TimelineManager {
|
||||
}
|
||||
|
||||
updateElementTrim({
|
||||
trackId,
|
||||
elementId,
|
||||
trimStart,
|
||||
trimEnd,
|
||||
pushHistory = true,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
trimStart: number;
|
||||
trimEnd: number;
|
||||
pushHistory?: boolean;
|
||||
}): void {
|
||||
const command = new UpdateElementTrimCommand(
|
||||
trackId,
|
||||
elementId,
|
||||
trimStart,
|
||||
trimEnd,
|
||||
@@ -118,17 +116,20 @@ export class TimelineManager {
|
||||
targetTrackId,
|
||||
elementId,
|
||||
newStartTime,
|
||||
createTrack,
|
||||
}: {
|
||||
sourceTrackId: string;
|
||||
targetTrackId: string;
|
||||
elementId: string;
|
||||
newStartTime: number;
|
||||
createTrack?: { type: TrackType; index: number };
|
||||
}): void {
|
||||
const command = new MoveElementCommand(
|
||||
sourceTrackId,
|
||||
targetTrackId,
|
||||
elementId,
|
||||
newStartTime,
|
||||
createTrack,
|
||||
);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
@@ -138,6 +139,11 @@ export class TimelineManager {
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
toggleTrackVisibility({ trackId }: { trackId: string }): void {
|
||||
const command = new ToggleTrackVisibilityCommand(trackId);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
splitElements({
|
||||
elements,
|
||||
splitTime,
|
||||
@@ -257,20 +263,6 @@ export class TimelineManager {
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
checkElementOverlap({
|
||||
trackId,
|
||||
startTime,
|
||||
duration,
|
||||
excludeElementId,
|
||||
}: {
|
||||
trackId: string;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
excludeElementId?: string;
|
||||
}): boolean {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
getTracks(): TimelineTrack[] {
|
||||
return this.editor.scenes.getActiveScene()?.tracks ?? [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user