mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
lots of stuff
This commit is contained in:
+29
-29
@@ -8,37 +8,37 @@ import { CommandManager } from "./managers/commands";
|
||||
import { SaveManager } from "./managers/save-manager";
|
||||
|
||||
export class EditorCore {
|
||||
private static instance: EditorCore | null = null;
|
||||
private static instance: EditorCore | null = null;
|
||||
|
||||
public readonly command: CommandManager;
|
||||
public readonly playback: PlaybackManager;
|
||||
public readonly timeline: TimelineManager;
|
||||
public readonly scenes: ScenesManager;
|
||||
public readonly project: ProjectManager;
|
||||
public readonly media: MediaManager;
|
||||
public readonly renderer: RendererManager;
|
||||
public readonly save: SaveManager;
|
||||
public readonly command: CommandManager;
|
||||
public readonly playback: PlaybackManager;
|
||||
public readonly timeline: TimelineManager;
|
||||
public readonly scenes: ScenesManager;
|
||||
public readonly project: ProjectManager;
|
||||
public readonly media: MediaManager;
|
||||
public readonly renderer: RendererManager;
|
||||
public readonly save: SaveManager;
|
||||
|
||||
private constructor() {
|
||||
this.command = new CommandManager();
|
||||
this.playback = new PlaybackManager(this);
|
||||
this.timeline = new TimelineManager(this);
|
||||
this.scenes = new ScenesManager(this);
|
||||
this.project = new ProjectManager(this);
|
||||
this.media = new MediaManager(this);
|
||||
this.renderer = new RendererManager(this);
|
||||
this.save = new SaveManager(this);
|
||||
this.save.start();
|
||||
}
|
||||
private constructor() {
|
||||
this.command = new CommandManager();
|
||||
this.playback = new PlaybackManager(this);
|
||||
this.timeline = new TimelineManager(this);
|
||||
this.scenes = new ScenesManager(this);
|
||||
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 {
|
||||
if (!EditorCore.instance) {
|
||||
EditorCore.instance = new EditorCore();
|
||||
}
|
||||
return EditorCore.instance;
|
||||
}
|
||||
static getInstance(): EditorCore {
|
||||
if (!EditorCore.instance) {
|
||||
EditorCore.instance = new EditorCore();
|
||||
}
|
||||
return EditorCore.instance;
|
||||
}
|
||||
|
||||
static reset(): void {
|
||||
EditorCore.instance = null;
|
||||
}
|
||||
static reset(): void {
|
||||
EditorCore.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import { Command } from "@/lib/commands";
|
||||
import type { Command } from "@/lib/commands";
|
||||
|
||||
export class CommandManager {
|
||||
private history: Command[] = [];
|
||||
private redoStack: Command[] = [];
|
||||
private history: Command[] = [];
|
||||
private redoStack: Command[] = [];
|
||||
|
||||
execute({ command }: { command: Command }): Command {
|
||||
command.execute();
|
||||
this.history.push(command);
|
||||
this.redoStack = [];
|
||||
return command;
|
||||
}
|
||||
execute({ command }: { command: Command }): Command {
|
||||
command.execute();
|
||||
this.history.push(command);
|
||||
this.redoStack = [];
|
||||
return command;
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (this.history.length === 0) return;
|
||||
const command = this.history.pop();
|
||||
command?.undo();
|
||||
if (command) {
|
||||
this.redoStack.push(command);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
canUndo(): boolean {
|
||||
return this.history.length > 0;
|
||||
}
|
||||
|
||||
canRedo(): boolean {
|
||||
return this.redoStack.length > 0;
|
||||
}
|
||||
canRedo(): boolean {
|
||||
return this.redoStack.length > 0;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.history = [];
|
||||
this.redoStack = [];
|
||||
}
|
||||
clear(): void {
|
||||
this.history = [];
|
||||
this.redoStack = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,157 +6,157 @@ import { videoCache } from "@/services/media/video-cache";
|
||||
import { hasMediaId } from "@/lib/timeline/element-utils";
|
||||
|
||||
export class MediaManager {
|
||||
private assets: MediaAsset[] = [];
|
||||
private isLoading = false;
|
||||
private listeners = new Set<() => void>();
|
||||
private assets: MediaAsset[] = [];
|
||||
private isLoading = false;
|
||||
private listeners = new Set<() => void>();
|
||||
|
||||
constructor(private editor: EditorCore) {}
|
||||
constructor(private editor: EditorCore) {}
|
||||
|
||||
async addMediaAsset({
|
||||
projectId,
|
||||
asset,
|
||||
}: {
|
||||
projectId: string;
|
||||
asset: Omit<MediaAsset, "id">;
|
||||
}): Promise<void> {
|
||||
const newAsset: MediaAsset = {
|
||||
...asset,
|
||||
id: generateUUID(),
|
||||
};
|
||||
async addMediaAsset({
|
||||
projectId,
|
||||
asset,
|
||||
}: {
|
||||
projectId: string;
|
||||
asset: Omit<MediaAsset, "id">;
|
||||
}): Promise<void> {
|
||||
const newAsset: MediaAsset = {
|
||||
...asset,
|
||||
id: generateUUID(),
|
||||
};
|
||||
|
||||
this.assets = [...this.assets, newAsset];
|
||||
this.notify();
|
||||
this.assets = [...this.assets, newAsset];
|
||||
this.notify();
|
||||
|
||||
try {
|
||||
await storageService.saveMediaAsset({ projectId, mediaAsset: newAsset });
|
||||
} catch (error) {
|
||||
console.error("Failed to save media asset:", error);
|
||||
this.assets = this.assets.filter((asset) => asset.id !== newAsset.id);
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
try {
|
||||
await storageService.saveMediaAsset({ projectId, mediaAsset: newAsset });
|
||||
} catch (error) {
|
||||
console.error("Failed to save media asset:", error);
|
||||
this.assets = this.assets.filter((asset) => asset.id !== newAsset.id);
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
async removeMediaAsset({
|
||||
projectId,
|
||||
id,
|
||||
}: {
|
||||
projectId: string;
|
||||
id: string;
|
||||
}): Promise<void> {
|
||||
const asset = this.assets.find((asset) => asset.id === id);
|
||||
async removeMediaAsset({
|
||||
projectId,
|
||||
id,
|
||||
}: {
|
||||
projectId: string;
|
||||
id: string;
|
||||
}): Promise<void> {
|
||||
const asset = this.assets.find((asset) => asset.id === id);
|
||||
|
||||
videoCache.clearVideo({ mediaId: id });
|
||||
videoCache.clearVideo({ mediaId: id });
|
||||
|
||||
if (asset?.url) {
|
||||
URL.revokeObjectURL(asset.url);
|
||||
if (asset.thumbnailUrl) {
|
||||
URL.revokeObjectURL(asset.thumbnailUrl);
|
||||
}
|
||||
}
|
||||
if (asset?.url) {
|
||||
URL.revokeObjectURL(asset.url);
|
||||
if (asset.thumbnailUrl) {
|
||||
URL.revokeObjectURL(asset.thumbnailUrl);
|
||||
}
|
||||
}
|
||||
|
||||
this.assets = this.assets.filter((asset) => asset.id !== id);
|
||||
this.notify();
|
||||
this.assets = this.assets.filter((asset) => asset.id !== id);
|
||||
this.notify();
|
||||
|
||||
const tracks = this.editor.timeline.getTracks();
|
||||
const elementsToRemove: Array<{ trackId: string; elementId: string }> = [];
|
||||
const tracks = this.editor.timeline.getTracks();
|
||||
const elementsToRemove: Array<{ trackId: string; elementId: string }> = [];
|
||||
|
||||
for (const track of tracks) {
|
||||
for (const element of track.elements) {
|
||||
if (hasMediaId(element) && element.mediaId === id) {
|
||||
elementsToRemove.push({ trackId: track.id, elementId: element.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const track of tracks) {
|
||||
for (const element of track.elements) {
|
||||
if (hasMediaId(element) && element.mediaId === id) {
|
||||
elementsToRemove.push({ trackId: track.id, elementId: element.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (elementsToRemove.length > 0) {
|
||||
this.editor.timeline.deleteElements({ elements: elementsToRemove });
|
||||
}
|
||||
if (elementsToRemove.length > 0) {
|
||||
this.editor.timeline.deleteElements({ elements: elementsToRemove });
|
||||
}
|
||||
|
||||
try {
|
||||
await storageService.deleteMediaAsset({ projectId, id });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete media asset:", error);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await storageService.deleteMediaAsset({ projectId, id });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete media asset:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadProjectMedia({ projectId }: { projectId: string }): Promise<void> {
|
||||
this.isLoading = true;
|
||||
this.notify();
|
||||
async loadProjectMedia({ projectId }: { projectId: string }): Promise<void> {
|
||||
this.isLoading = true;
|
||||
this.notify();
|
||||
|
||||
try {
|
||||
const mediaAssets = await storageService.loadAllMediaAssets({
|
||||
projectId,
|
||||
});
|
||||
this.assets = mediaAssets;
|
||||
this.notify();
|
||||
} catch (error) {
|
||||
console.error("Failed to load media assets:", error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
try {
|
||||
const mediaAssets = await storageService.loadAllMediaAssets({
|
||||
projectId,
|
||||
});
|
||||
this.assets = mediaAssets;
|
||||
this.notify();
|
||||
} catch (error) {
|
||||
console.error("Failed to load media assets:", error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
async clearProjectMedia({ projectId }: { projectId: string }): Promise<void> {
|
||||
this.assets.forEach((asset) => {
|
||||
if (asset.url) {
|
||||
URL.revokeObjectURL(asset.url);
|
||||
}
|
||||
if (asset.thumbnailUrl) {
|
||||
URL.revokeObjectURL(asset.thumbnailUrl);
|
||||
}
|
||||
});
|
||||
async clearProjectMedia({ projectId }: { projectId: string }): Promise<void> {
|
||||
this.assets.forEach((asset) => {
|
||||
if (asset.url) {
|
||||
URL.revokeObjectURL(asset.url);
|
||||
}
|
||||
if (asset.thumbnailUrl) {
|
||||
URL.revokeObjectURL(asset.thumbnailUrl);
|
||||
}
|
||||
});
|
||||
|
||||
const mediaIds = this.assets.map((asset) => asset.id);
|
||||
this.assets = [];
|
||||
this.notify();
|
||||
const mediaIds = this.assets.map((asset) => asset.id);
|
||||
this.assets = [];
|
||||
this.notify();
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
mediaIds.map((id) =>
|
||||
storageService.deleteMediaAsset({ projectId, id }),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to clear media assets from storage:", error);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await Promise.all(
|
||||
mediaIds.map((id) =>
|
||||
storageService.deleteMediaAsset({ projectId, id }),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to clear media assets from storage:", error);
|
||||
}
|
||||
}
|
||||
|
||||
clearAllAssets(): void {
|
||||
videoCache.clearAll();
|
||||
clearAllAssets(): void {
|
||||
videoCache.clearAll();
|
||||
|
||||
this.assets.forEach((asset) => {
|
||||
if (asset.url) {
|
||||
URL.revokeObjectURL(asset.url);
|
||||
}
|
||||
if (asset.thumbnailUrl) {
|
||||
URL.revokeObjectURL(asset.thumbnailUrl);
|
||||
}
|
||||
});
|
||||
this.assets.forEach((asset) => {
|
||||
if (asset.url) {
|
||||
URL.revokeObjectURL(asset.url);
|
||||
}
|
||||
if (asset.thumbnailUrl) {
|
||||
URL.revokeObjectURL(asset.thumbnailUrl);
|
||||
}
|
||||
});
|
||||
|
||||
this.assets = [];
|
||||
this.notify();
|
||||
}
|
||||
this.assets = [];
|
||||
this.notify();
|
||||
}
|
||||
|
||||
getAssets(): MediaAsset[] {
|
||||
return this.assets;
|
||||
}
|
||||
getAssets(): MediaAsset[] {
|
||||
return this.assets;
|
||||
}
|
||||
|
||||
setAssets({ assets }: { assets: MediaAsset[] }): void {
|
||||
this.assets = assets;
|
||||
this.notify();
|
||||
}
|
||||
setAssets({ assets }: { assets: MediaAsset[] }): void {
|
||||
this.assets = assets;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
isLoadingMedia(): boolean {
|
||||
return this.isLoading;
|
||||
}
|
||||
isLoadingMedia(): boolean {
|
||||
return this.isLoading;
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
private notify(): void {
|
||||
this.listeners.forEach((fn) => fn());
|
||||
}
|
||||
private notify(): void {
|
||||
this.listeners.forEach((fn) => fn());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,188 +1,188 @@
|
||||
import type { EditorCore } from "@/core";
|
||||
|
||||
export class PlaybackManager {
|
||||
private isPlaying = false;
|
||||
private currentTime = 0;
|
||||
private volume = 1;
|
||||
private muted = false;
|
||||
private previousVolume = 1;
|
||||
private speed = 1.0;
|
||||
private listeners = new Set<() => void>();
|
||||
private playbackTimer: number | null = null;
|
||||
private lastUpdate = 0;
|
||||
private isPlaying = false;
|
||||
private currentTime = 0;
|
||||
private volume = 1;
|
||||
private muted = false;
|
||||
private previousVolume = 1;
|
||||
private speed = 1.0;
|
||||
private listeners = new Set<() => void>();
|
||||
private playbackTimer: number | null = null;
|
||||
private lastUpdate = 0;
|
||||
|
||||
constructor(private editor: EditorCore) {}
|
||||
constructor(private editor: EditorCore) {}
|
||||
|
||||
play(): void {
|
||||
const duration = this.editor.timeline.getTotalDuration();
|
||||
play(): void {
|
||||
const duration = this.editor.timeline.getTotalDuration();
|
||||
|
||||
if (duration > 0) {
|
||||
const activeProject = this.editor.project.getActive();
|
||||
const fps = activeProject.settings.fps;
|
||||
const frameOffset = 1 / fps;
|
||||
const endThreshold = Math.max(0, duration - frameOffset);
|
||||
if (duration > 0) {
|
||||
const activeProject = this.editor.project.getActive();
|
||||
const fps = activeProject.settings.fps;
|
||||
const frameOffset = 1 / fps;
|
||||
const endThreshold = Math.max(0, duration - frameOffset);
|
||||
|
||||
if (this.currentTime >= endThreshold) {
|
||||
this.seek({ time: 0 });
|
||||
}
|
||||
}
|
||||
if (this.currentTime >= endThreshold) {
|
||||
this.seek({ time: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
this.isPlaying = true;
|
||||
this.startTimer();
|
||||
this.notify();
|
||||
}
|
||||
this.isPlaying = true;
|
||||
this.startTimer();
|
||||
this.notify();
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
this.isPlaying = false;
|
||||
this.stopTimer();
|
||||
this.notify();
|
||||
}
|
||||
pause(): void {
|
||||
this.isPlaying = false;
|
||||
this.stopTimer();
|
||||
this.notify();
|
||||
}
|
||||
|
||||
toggle(): void {
|
||||
if (this.isPlaying) {
|
||||
this.pause();
|
||||
} else {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
toggle(): void {
|
||||
if (this.isPlaying) {
|
||||
this.pause();
|
||||
} else {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
|
||||
seek({ time }: { time: number }): void {
|
||||
const duration = this.editor.timeline.getTotalDuration();
|
||||
this.currentTime = Math.max(0, Math.min(duration, time));
|
||||
this.notify();
|
||||
seek({ time }: { time: number }): void {
|
||||
const duration = this.editor.timeline.getTotalDuration();
|
||||
this.currentTime = Math.max(0, Math.min(duration, time));
|
||||
this.notify();
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("playback-seek", {
|
||||
detail: { time: this.currentTime },
|
||||
}),
|
||||
);
|
||||
}
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("playback-seek", {
|
||||
detail: { time: this.currentTime },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
setVolume({ volume }: { volume: number }): void {
|
||||
const clampedVolume = Math.max(0, Math.min(1, volume));
|
||||
this.volume = clampedVolume;
|
||||
this.muted = clampedVolume === 0;
|
||||
if (clampedVolume > 0) {
|
||||
this.previousVolume = clampedVolume;
|
||||
}
|
||||
this.notify();
|
||||
}
|
||||
setVolume({ volume }: { volume: number }): void {
|
||||
const clampedVolume = Math.max(0, Math.min(1, volume));
|
||||
this.volume = clampedVolume;
|
||||
this.muted = clampedVolume === 0;
|
||||
if (clampedVolume > 0) {
|
||||
this.previousVolume = clampedVolume;
|
||||
}
|
||||
this.notify();
|
||||
}
|
||||
|
||||
setSpeed({ speed }: { speed: number }): void {
|
||||
this.speed = Math.max(0.1, Math.min(2.0, speed));
|
||||
this.notify();
|
||||
setSpeed({ speed }: { speed: number }): void {
|
||||
this.speed = Math.max(0.1, Math.min(2.0, speed));
|
||||
this.notify();
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("playback-speed", {
|
||||
detail: { speed: this.speed },
|
||||
}),
|
||||
);
|
||||
}
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("playback-speed", {
|
||||
detail: { speed: this.speed },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
mute(): void {
|
||||
if (this.volume > 0) {
|
||||
this.previousVolume = this.volume;
|
||||
}
|
||||
this.muted = true;
|
||||
this.volume = 0;
|
||||
this.notify();
|
||||
}
|
||||
mute(): void {
|
||||
if (this.volume > 0) {
|
||||
this.previousVolume = this.volume;
|
||||
}
|
||||
this.muted = true;
|
||||
this.volume = 0;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
unmute(): void {
|
||||
this.muted = false;
|
||||
this.volume = this.previousVolume;
|
||||
this.notify();
|
||||
}
|
||||
unmute(): void {
|
||||
this.muted = false;
|
||||
this.volume = this.previousVolume;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
toggleMute(): void {
|
||||
if (this.muted) {
|
||||
this.unmute();
|
||||
} else {
|
||||
this.mute();
|
||||
}
|
||||
}
|
||||
toggleMute(): void {
|
||||
if (this.muted) {
|
||||
this.unmute();
|
||||
} else {
|
||||
this.mute();
|
||||
}
|
||||
}
|
||||
|
||||
getIsPlaying(): boolean {
|
||||
return this.isPlaying;
|
||||
}
|
||||
getIsPlaying(): boolean {
|
||||
return this.isPlaying;
|
||||
}
|
||||
|
||||
getCurrentTime(): number {
|
||||
return this.currentTime;
|
||||
}
|
||||
getCurrentTime(): number {
|
||||
return this.currentTime;
|
||||
}
|
||||
|
||||
getVolume(): number {
|
||||
return this.volume;
|
||||
}
|
||||
getVolume(): number {
|
||||
return this.volume;
|
||||
}
|
||||
|
||||
isMuted(): boolean {
|
||||
return this.muted;
|
||||
}
|
||||
isMuted(): boolean {
|
||||
return this.muted;
|
||||
}
|
||||
|
||||
getSpeed(): number {
|
||||
return this.speed;
|
||||
}
|
||||
getSpeed(): number {
|
||||
return this.speed;
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
private notify(): void {
|
||||
this.listeners.forEach((fn) => fn());
|
||||
}
|
||||
private notify(): void {
|
||||
this.listeners.forEach((fn) => fn());
|
||||
}
|
||||
|
||||
private startTimer(): void {
|
||||
if (this.playbackTimer) {
|
||||
cancelAnimationFrame(this.playbackTimer);
|
||||
}
|
||||
private startTimer(): void {
|
||||
if (this.playbackTimer) {
|
||||
cancelAnimationFrame(this.playbackTimer);
|
||||
}
|
||||
|
||||
this.lastUpdate = performance.now();
|
||||
this.updateTime();
|
||||
}
|
||||
this.lastUpdate = performance.now();
|
||||
this.updateTime();
|
||||
}
|
||||
|
||||
private stopTimer(): void {
|
||||
if (this.playbackTimer) {
|
||||
cancelAnimationFrame(this.playbackTimer);
|
||||
this.playbackTimer = null;
|
||||
}
|
||||
}
|
||||
private stopTimer(): void {
|
||||
if (this.playbackTimer) {
|
||||
cancelAnimationFrame(this.playbackTimer);
|
||||
this.playbackTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private updateTime = (): void => {
|
||||
if (!this.isPlaying) return;
|
||||
private updateTime = (): void => {
|
||||
if (!this.isPlaying) return;
|
||||
|
||||
const now = performance.now();
|
||||
const delta = (now - this.lastUpdate) / 1000;
|
||||
this.lastUpdate = now;
|
||||
const now = performance.now();
|
||||
const delta = (now - this.lastUpdate) / 1000;
|
||||
this.lastUpdate = now;
|
||||
|
||||
const newTime = this.currentTime + delta * this.speed;
|
||||
const duration = this.editor.timeline.getTotalDuration();
|
||||
const newTime = this.currentTime + delta * this.speed;
|
||||
const duration = this.editor.timeline.getTotalDuration();
|
||||
|
||||
if (duration > 0 && newTime >= duration) {
|
||||
const activeProject = this.editor.project.getActive();
|
||||
const fps = activeProject.settings.fps;
|
||||
const frameOffset = 1 / fps;
|
||||
const stopTime = Math.max(0, duration - frameOffset);
|
||||
if (duration > 0 && newTime >= duration) {
|
||||
const activeProject = this.editor.project.getActive();
|
||||
const fps = activeProject.settings.fps;
|
||||
const frameOffset = 1 / fps;
|
||||
const stopTime = Math.max(0, duration - frameOffset);
|
||||
|
||||
this.pause();
|
||||
this.currentTime = stopTime;
|
||||
this.notify();
|
||||
this.pause();
|
||||
this.currentTime = stopTime;
|
||||
this.notify();
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("playback-seek", {
|
||||
detail: { time: stopTime },
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.currentTime = newTime;
|
||||
this.notify();
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("playback-seek", {
|
||||
detail: { time: stopTime },
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.currentTime = newTime;
|
||||
this.notify();
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("playback-update", {
|
||||
detail: { time: newTime },
|
||||
}),
|
||||
);
|
||||
}
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("playback-update", {
|
||||
detail: { time: newTime },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
this.playbackTimer = requestAnimationFrame(this.updateTime);
|
||||
};
|
||||
this.playbackTimer = requestAnimationFrame(this.updateTime);
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,124 +6,124 @@ import { buildScene } from "@/services/renderer/scene-builder";
|
||||
import { createTimelineAudioBuffer } from "@/lib/media/audio";
|
||||
|
||||
export class RendererManager {
|
||||
private renderTree: RootNode | null = null;
|
||||
private listeners = new Set<() => void>();
|
||||
private renderTree: RootNode | null = null;
|
||||
private listeners = new Set<() => void>();
|
||||
|
||||
constructor(private editor: EditorCore) {}
|
||||
constructor(private editor: EditorCore) {}
|
||||
|
||||
setRenderTree({ renderTree }: { renderTree: RootNode | null }): void {
|
||||
this.renderTree = renderTree;
|
||||
this.notify();
|
||||
}
|
||||
setRenderTree({ renderTree }: { renderTree: RootNode | null }): void {
|
||||
this.renderTree = renderTree;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
getRenderTree(): RootNode | null {
|
||||
return this.renderTree;
|
||||
}
|
||||
getRenderTree(): RootNode | null {
|
||||
return this.renderTree;
|
||||
}
|
||||
|
||||
async exportProject({
|
||||
options,
|
||||
}: {
|
||||
options: ExportOptions;
|
||||
}): Promise<ExportResult> {
|
||||
const { format, quality, fps, includeAudio, onProgress, onCancel } =
|
||||
options;
|
||||
async exportProject({
|
||||
options,
|
||||
}: {
|
||||
options: ExportOptions;
|
||||
}): Promise<ExportResult> {
|
||||
const { format, quality, fps, includeAudio, onProgress, onCancel } =
|
||||
options;
|
||||
|
||||
try {
|
||||
const tracks = this.editor.timeline.getTracks();
|
||||
const mediaAssets = this.editor.media.getAssets();
|
||||
const activeProject = this.editor.project.getActive();
|
||||
try {
|
||||
const tracks = this.editor.timeline.getTracks();
|
||||
const mediaAssets = this.editor.media.getAssets();
|
||||
const activeProject = this.editor.project.getActive();
|
||||
|
||||
if (!activeProject) {
|
||||
return { success: false, error: "No active project" };
|
||||
}
|
||||
if (!activeProject) {
|
||||
return { success: false, error: "No active project" };
|
||||
}
|
||||
|
||||
const duration = this.editor.timeline.getTotalDuration();
|
||||
if (duration === 0) {
|
||||
return { success: false, error: "Project is empty" };
|
||||
}
|
||||
const duration = this.editor.timeline.getTotalDuration();
|
||||
if (duration === 0) {
|
||||
return { success: false, error: "Project is empty" };
|
||||
}
|
||||
|
||||
const exportFps = fps || activeProject.settings.fps;
|
||||
const canvasSize = activeProject.settings.canvasSize;
|
||||
const exportFps = fps || activeProject.settings.fps;
|
||||
const canvasSize = activeProject.settings.canvasSize;
|
||||
|
||||
let audioBuffer: AudioBuffer | null = null;
|
||||
if (includeAudio) {
|
||||
onProgress?.({ progress: 0.05 });
|
||||
audioBuffer = await createTimelineAudioBuffer({
|
||||
tracks,
|
||||
mediaAssets,
|
||||
duration,
|
||||
});
|
||||
}
|
||||
let audioBuffer: AudioBuffer | null = null;
|
||||
if (includeAudio) {
|
||||
onProgress?.({ progress: 0.05 });
|
||||
audioBuffer = await createTimelineAudioBuffer({
|
||||
tracks,
|
||||
mediaAssets,
|
||||
duration,
|
||||
});
|
||||
}
|
||||
|
||||
const scene = buildScene({
|
||||
tracks,
|
||||
mediaAssets,
|
||||
duration,
|
||||
canvasSize,
|
||||
background: activeProject.settings.background,
|
||||
});
|
||||
const scene = buildScene({
|
||||
tracks,
|
||||
mediaAssets,
|
||||
duration,
|
||||
canvasSize,
|
||||
background: activeProject.settings.background,
|
||||
});
|
||||
|
||||
const exporter = new SceneExporter({
|
||||
width: canvasSize.width,
|
||||
height: canvasSize.height,
|
||||
fps: exportFps,
|
||||
format,
|
||||
quality,
|
||||
includeAudio: !!includeAudio,
|
||||
audioBuffer: audioBuffer || undefined,
|
||||
});
|
||||
const exporter = new SceneExporter({
|
||||
width: canvasSize.width,
|
||||
height: canvasSize.height,
|
||||
fps: exportFps,
|
||||
format,
|
||||
quality,
|
||||
includeAudio: !!includeAudio,
|
||||
audioBuffer: audioBuffer || undefined,
|
||||
});
|
||||
|
||||
exporter.on("progress", (progress) => {
|
||||
const adjustedProgress = includeAudio
|
||||
? 0.05 + progress * 0.95
|
||||
: progress;
|
||||
onProgress?.({ progress: adjustedProgress });
|
||||
});
|
||||
exporter.on("progress", (progress) => {
|
||||
const adjustedProgress = includeAudio
|
||||
? 0.05 + progress * 0.95
|
||||
: progress;
|
||||
onProgress?.({ progress: adjustedProgress });
|
||||
});
|
||||
|
||||
let cancelled = false;
|
||||
const checkCancel = () => {
|
||||
if (onCancel?.()) {
|
||||
cancelled = true;
|
||||
exporter.cancel();
|
||||
}
|
||||
};
|
||||
let cancelled = false;
|
||||
const checkCancel = () => {
|
||||
if (onCancel?.()) {
|
||||
cancelled = true;
|
||||
exporter.cancel();
|
||||
}
|
||||
};
|
||||
|
||||
const cancelInterval = setInterval(checkCancel, 100);
|
||||
const cancelInterval = setInterval(checkCancel, 100);
|
||||
|
||||
try {
|
||||
const buffer = await exporter.export(scene);
|
||||
clearInterval(cancelInterval);
|
||||
try {
|
||||
const buffer = await exporter.export(scene);
|
||||
clearInterval(cancelInterval);
|
||||
|
||||
if (cancelled) {
|
||||
return { success: false, cancelled: true };
|
||||
}
|
||||
if (cancelled) {
|
||||
return { success: false, cancelled: true };
|
||||
}
|
||||
|
||||
if (!buffer) {
|
||||
return { success: false, error: "Export failed to produce buffer" };
|
||||
}
|
||||
if (!buffer) {
|
||||
return { success: false, error: "Export failed to produce buffer" };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
buffer,
|
||||
};
|
||||
} finally {
|
||||
clearInterval(cancelInterval);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Export failed:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown export error",
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
buffer,
|
||||
};
|
||||
} finally {
|
||||
clearInterval(cancelInterval);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Export failed:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown export error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
private notify(): void {
|
||||
this.listeners.forEach((fn) => fn());
|
||||
}
|
||||
private notify(): void {
|
||||
this.listeners.forEach((fn) => fn());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +1,103 @@
|
||||
import type { EditorCore } from "@/core";
|
||||
|
||||
type SaveManagerOptions = {
|
||||
debounceMs?: number;
|
||||
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> = [];
|
||||
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;
|
||||
}
|
||||
constructor(
|
||||
private editor: EditorCore,
|
||||
{ debounceMs = 800 }: SaveManagerOptions = {},
|
||||
) {
|
||||
this.debounceMs = debounceMs;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.unsubscribeHandlers.length > 0) return;
|
||||
start(): void {
|
||||
if (this.unsubscribeHandlers.length > 0) return;
|
||||
|
||||
this.unsubscribeHandlers = [
|
||||
this.editor.scenes.subscribe(() => {
|
||||
this.markDirty();
|
||||
}),
|
||||
this.editor.timeline.subscribe(() => {
|
||||
this.markDirty();
|
||||
}),
|
||||
];
|
||||
}
|
||||
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();
|
||||
}
|
||||
stop(): void {
|
||||
for (const unsubscribe of this.unsubscribeHandlers) {
|
||||
unsubscribe();
|
||||
}
|
||||
this.unsubscribeHandlers = [];
|
||||
this.clearTimer();
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
this.isPaused = true;
|
||||
}
|
||||
pause(): void {
|
||||
this.isPaused = true;
|
||||
}
|
||||
|
||||
resume(): void {
|
||||
this.isPaused = false;
|
||||
if (this.hasPendingSave) {
|
||||
this.queueSave();
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
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 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;
|
||||
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;
|
||||
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();
|
||||
this.isSaving = true;
|
||||
this.hasPendingSave = false;
|
||||
this.clearTimer();
|
||||
|
||||
try {
|
||||
await this.editor.project.saveCurrentProject();
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
if (this.hasPendingSave) {
|
||||
this.queueSave();
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
private clearTimer(): void {
|
||||
if (!this.saveTimer) return;
|
||||
clearTimeout(this.saveTimer);
|
||||
this.saveTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,314 +2,314 @@ import type { EditorCore } from "@/core";
|
||||
import type { TimelineTrack, TScene } from "@/types/timeline";
|
||||
import { storageService } from "@/services/storage/storage-service";
|
||||
import {
|
||||
getMainScene,
|
||||
ensureMainScene,
|
||||
canDeleteScene,
|
||||
findCurrentScene,
|
||||
getMainScene,
|
||||
ensureMainScene,
|
||||
canDeleteScene,
|
||||
findCurrentScene,
|
||||
} from "@/lib/scenes";
|
||||
import { getFrameTime, isBookmarkAtTime } from "@/lib/timeline/bookmarks";
|
||||
import { ensureMainTrack } from "@/lib/timeline/track-utils";
|
||||
import {
|
||||
CreateSceneCommand,
|
||||
DeleteSceneCommand,
|
||||
RemoveBookmarkCommand,
|
||||
RenameSceneCommand,
|
||||
ToggleBookmarkCommand,
|
||||
CreateSceneCommand,
|
||||
DeleteSceneCommand,
|
||||
RemoveBookmarkCommand,
|
||||
RenameSceneCommand,
|
||||
ToggleBookmarkCommand,
|
||||
} from "@/lib/commands/scene";
|
||||
|
||||
export class ScenesManager {
|
||||
private active: TScene | null = null;
|
||||
private list: TScene[] = [];
|
||||
private listeners = new Set<() => void>();
|
||||
private active: TScene | null = null;
|
||||
private list: TScene[] = [];
|
||||
private listeners = new Set<() => void>();
|
||||
|
||||
constructor(private editor: EditorCore) {}
|
||||
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");
|
||||
}
|
||||
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();
|
||||
}
|
||||
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);
|
||||
async deleteScene({ sceneId }: { sceneId: string }): Promise<void> {
|
||||
const sceneToDelete = this.list.find((s) => s.id === sceneId);
|
||||
|
||||
if (!sceneToDelete) {
|
||||
throw new Error("Scene not found");
|
||||
}
|
||||
if (!sceneToDelete) {
|
||||
throw new Error("Scene not found");
|
||||
}
|
||||
|
||||
const { canDelete, reason } = canDeleteScene({ scene: sceneToDelete });
|
||||
if (!canDelete) {
|
||||
throw new Error(reason);
|
||||
}
|
||||
const { canDelete, reason } = canDeleteScene({ scene: sceneToDelete });
|
||||
if (!canDelete) {
|
||||
throw new Error(reason);
|
||||
}
|
||||
|
||||
if (!this.editor.project.getActive()) {
|
||||
throw new Error("No active project");
|
||||
}
|
||||
if (!this.editor.project.getActive()) {
|
||||
throw new Error("No active project");
|
||||
}
|
||||
|
||||
const command = new DeleteSceneCommand(sceneId);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
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");
|
||||
}
|
||||
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 });
|
||||
}
|
||||
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);
|
||||
async switchToScene({ sceneId }: { sceneId: string }): Promise<void> {
|
||||
const targetScene = this.list.find((s) => s.id === sceneId);
|
||||
|
||||
if (!targetScene) {
|
||||
throw new Error("Scene not found");
|
||||
}
|
||||
if (!targetScene) {
|
||||
throw new Error("Scene not found");
|
||||
}
|
||||
|
||||
const activeProject = this.editor.project.getActive();
|
||||
const activeProject = this.editor.project.getActive();
|
||||
|
||||
if (activeProject) {
|
||||
const updatedProject = {
|
||||
...activeProject,
|
||||
currentSceneId: sceneId,
|
||||
metadata: {
|
||||
...activeProject.metadata,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
};
|
||||
if (activeProject) {
|
||||
const updatedProject = {
|
||||
...activeProject,
|
||||
currentSceneId: sceneId,
|
||||
metadata: {
|
||||
...activeProject.metadata,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
this.editor.project.setActiveProject({ project: updatedProject });
|
||||
}
|
||||
this.editor.project.setActiveProject({ project: updatedProject });
|
||||
}
|
||||
|
||||
this.active = targetScene;
|
||||
this.notify();
|
||||
}
|
||||
this.active = targetScene;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
async toggleBookmark({ time }: { time: number }): Promise<void> {
|
||||
const command = new ToggleBookmarkCommand(time);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
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();
|
||||
isBookmarked({ time }: { time: number }): boolean {
|
||||
const activeScene = this.getActiveScene();
|
||||
const activeProject = this.editor.project.getActive();
|
||||
|
||||
if (!activeScene || !this.active || !activeProject) return false;
|
||||
if (!activeScene || !this.active || !activeProject) return false;
|
||||
|
||||
const frameTime = getFrameTime({
|
||||
time,
|
||||
fps: activeProject.settings.fps,
|
||||
});
|
||||
const frameTime = getFrameTime({
|
||||
time,
|
||||
fps: activeProject.settings.fps,
|
||||
});
|
||||
|
||||
return isBookmarkAtTime({ bookmarks: activeScene.bookmarks, frameTime });
|
||||
}
|
||||
return isBookmarkAtTime({ bookmarks: activeScene.bookmarks, frameTime });
|
||||
}
|
||||
|
||||
async removeBookmark({ time }: { time: number }): Promise<void> {
|
||||
const command = new RemoveBookmarkCommand(time);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
async removeBookmark({ time }: { time: number }): Promise<void> {
|
||||
const command = new RemoveBookmarkCommand(time);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
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();
|
||||
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();
|
||||
}
|
||||
}
|
||||
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;
|
||||
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 });
|
||||
const fallbackScene = getMainScene({ scenes: scenesWithMainTracks });
|
||||
|
||||
this.list = scenesWithMainTracks;
|
||||
this.active = currentScene || fallbackScene;
|
||||
this.notify();
|
||||
this.list = scenesWithMainTracks;
|
||||
this.active = currentScene || fallbackScene;
|
||||
this.notify();
|
||||
|
||||
const hasAddedMainScene = ensuredScenes.length > scenes.length;
|
||||
if (hasAddedMainScene || hasAddedMainTrack) {
|
||||
const activeProject = this.editor.project.getActive();
|
||||
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(),
|
||||
},
|
||||
};
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
this.editor.project.setActiveProject({ project: updatedProject });
|
||||
this.editor.save.markDirty({ force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearScenes(): void {
|
||||
this.list = [];
|
||||
this.active = null;
|
||||
this.notify();
|
||||
}
|
||||
clearScenes(): void {
|
||||
this.list = [];
|
||||
this.active = null;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
getActiveScene(): TScene {
|
||||
if (!this.active) {
|
||||
throw new Error("No active scene.");
|
||||
}
|
||||
return this.active;
|
||||
}
|
||||
getActiveScene(): TScene {
|
||||
if (!this.active) {
|
||||
throw new Error("No active scene.");
|
||||
}
|
||||
return this.active;
|
||||
}
|
||||
|
||||
getScenes(): TScene[] {
|
||||
return this.list;
|
||||
}
|
||||
getScenes(): TScene[] {
|
||||
return this.list;
|
||||
}
|
||||
|
||||
setScenes({
|
||||
scenes,
|
||||
activeSceneId,
|
||||
}: {
|
||||
scenes: TScene[];
|
||||
activeSceneId?: string;
|
||||
}): void {
|
||||
this.list = scenes;
|
||||
this.active = activeSceneId
|
||||
? (scenes.find((s) => s.id === activeSceneId) ?? null)
|
||||
: this.active;
|
||||
this.notify();
|
||||
setScenes({
|
||||
scenes,
|
||||
activeSceneId,
|
||||
}: {
|
||||
scenes: TScene[];
|
||||
activeSceneId?: string;
|
||||
}): void {
|
||||
this.list = scenes;
|
||||
this.active = activeSceneId
|
||||
? (scenes.find((s) => s.id === activeSceneId) ?? null)
|
||||
: this.active;
|
||||
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 });
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
private notify(): void {
|
||||
this.listeners.forEach((fn) => fn());
|
||||
}
|
||||
private notify(): void {
|
||||
this.listeners.forEach((fn) => fn());
|
||||
}
|
||||
|
||||
updateSceneTracks({ tracks }: { tracks: TimelineTrack[] }): void {
|
||||
if (!this.active) return;
|
||||
updateSceneTracks({ tracks }: { tracks: TimelineTrack[] }): void {
|
||||
if (!this.active) return;
|
||||
|
||||
const updatedScene: TScene = {
|
||||
...this.active,
|
||||
tracks,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
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();
|
||||
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 });
|
||||
}
|
||||
}
|
||||
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[] = [];
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 };
|
||||
}
|
||||
return { scenes: ensuredScenes, hasAddedMainTrack };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,274 +1,274 @@
|
||||
import type { EditorCore } from "@/core";
|
||||
import type {
|
||||
TrackType,
|
||||
CreateTimelineElement,
|
||||
TimelineTrack,
|
||||
TextElement,
|
||||
TimelineElement,
|
||||
ClipboardItem,
|
||||
TrackType,
|
||||
CreateTimelineElement,
|
||||
TimelineTrack,
|
||||
TextElement,
|
||||
TimelineElement,
|
||||
ClipboardItem,
|
||||
} from "@/types/timeline";
|
||||
import { calculateTotalDuration } from "@/lib/timeline";
|
||||
import {
|
||||
AddTrackCommand,
|
||||
RemoveTrackCommand,
|
||||
ToggleTrackMuteCommand,
|
||||
ToggleTrackVisibilityCommand,
|
||||
InsertElementCommand,
|
||||
UpdateElementTrimCommand,
|
||||
UpdateElementDurationCommand,
|
||||
DeleteElementsCommand,
|
||||
DuplicateElementsCommand,
|
||||
ToggleElementsVisibilityCommand,
|
||||
ToggleElementsMutedCommand,
|
||||
UpdateTextElementCommand,
|
||||
SplitElementsCommand,
|
||||
PasteCommand,
|
||||
UpdateElementStartTimeCommand,
|
||||
MoveElementCommand,
|
||||
AddTrackCommand,
|
||||
RemoveTrackCommand,
|
||||
ToggleTrackMuteCommand,
|
||||
ToggleTrackVisibilityCommand,
|
||||
InsertElementCommand,
|
||||
UpdateElementTrimCommand,
|
||||
UpdateElementDurationCommand,
|
||||
DeleteElementsCommand,
|
||||
DuplicateElementsCommand,
|
||||
ToggleElementsVisibilityCommand,
|
||||
ToggleElementsMutedCommand,
|
||||
UpdateTextElementCommand,
|
||||
SplitElementsCommand,
|
||||
PasteCommand,
|
||||
UpdateElementStartTimeCommand,
|
||||
MoveElementCommand,
|
||||
} from "@/lib/commands/timeline";
|
||||
import { InsertElementParams } from "@/lib/commands/timeline/element/insert-element";
|
||||
import type { InsertElementParams } from "@/lib/commands/timeline/element/insert-element";
|
||||
|
||||
export class TimelineManager {
|
||||
private listeners = new Set<() => void>();
|
||||
private listeners = new Set<() => void>();
|
||||
|
||||
constructor(private editor: EditorCore) {}
|
||||
constructor(private editor: EditorCore) {}
|
||||
|
||||
addTrack({ type, index }: { type: TrackType; index?: number }): string {
|
||||
const command = new AddTrackCommand(type, index);
|
||||
this.editor.command.execute({ command });
|
||||
return command.getTrackId();
|
||||
}
|
||||
addTrack({ type, index }: { type: TrackType; index?: number }): string {
|
||||
const command = new AddTrackCommand(type, index);
|
||||
this.editor.command.execute({ command });
|
||||
return command.getTrackId();
|
||||
}
|
||||
|
||||
removeTrack({ trackId }: { trackId: string }): void {
|
||||
const command = new RemoveTrackCommand(trackId);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
removeTrack({ trackId }: { trackId: string }): void {
|
||||
const command = new RemoveTrackCommand(trackId);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
insertElement({ element, placement }: InsertElementParams): void {
|
||||
const command = new InsertElementCommand({ element, placement });
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
insertElement({ element, placement }: InsertElementParams): void {
|
||||
const command = new InsertElementCommand({ element, placement });
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
updateElementTrim({
|
||||
elementId,
|
||||
trimStart,
|
||||
trimEnd,
|
||||
pushHistory = true,
|
||||
}: {
|
||||
elementId: string;
|
||||
trimStart: number;
|
||||
trimEnd: number;
|
||||
pushHistory?: boolean;
|
||||
}): void {
|
||||
const command = new UpdateElementTrimCommand(elementId, trimStart, trimEnd);
|
||||
if (pushHistory) {
|
||||
this.editor.command.execute({ command });
|
||||
} else {
|
||||
command.execute();
|
||||
}
|
||||
}
|
||||
updateElementTrim({
|
||||
elementId,
|
||||
trimStart,
|
||||
trimEnd,
|
||||
pushHistory = true,
|
||||
}: {
|
||||
elementId: string;
|
||||
trimStart: number;
|
||||
trimEnd: number;
|
||||
pushHistory?: boolean;
|
||||
}): void {
|
||||
const command = new UpdateElementTrimCommand(elementId, trimStart, trimEnd);
|
||||
if (pushHistory) {
|
||||
this.editor.command.execute({ command });
|
||||
} else {
|
||||
command.execute();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementDuration({
|
||||
trackId,
|
||||
elementId,
|
||||
duration,
|
||||
pushHistory = true,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
duration: number;
|
||||
pushHistory?: boolean;
|
||||
}): void {
|
||||
const command = new UpdateElementDurationCommand(
|
||||
trackId,
|
||||
elementId,
|
||||
duration,
|
||||
);
|
||||
if (pushHistory) {
|
||||
this.editor.command.execute({ command });
|
||||
} else {
|
||||
command.execute();
|
||||
}
|
||||
}
|
||||
updateElementDuration({
|
||||
trackId,
|
||||
elementId,
|
||||
duration,
|
||||
pushHistory = true,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
duration: number;
|
||||
pushHistory?: boolean;
|
||||
}): void {
|
||||
const command = new UpdateElementDurationCommand(
|
||||
trackId,
|
||||
elementId,
|
||||
duration,
|
||||
);
|
||||
if (pushHistory) {
|
||||
this.editor.command.execute({ command });
|
||||
} else {
|
||||
command.execute();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementStartTime({
|
||||
elements,
|
||||
startTime,
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
startTime: number;
|
||||
}): void {
|
||||
const command = new UpdateElementStartTimeCommand(elements, startTime);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
updateElementStartTime({
|
||||
elements,
|
||||
startTime,
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
startTime: number;
|
||||
}): void {
|
||||
const command = new UpdateElementStartTimeCommand(elements, startTime);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
moveElement({
|
||||
sourceTrackId,
|
||||
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 });
|
||||
}
|
||||
moveElement({
|
||||
sourceTrackId,
|
||||
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 });
|
||||
}
|
||||
|
||||
toggleTrackMute({ trackId }: { trackId: string }): void {
|
||||
const command = new ToggleTrackMuteCommand(trackId);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
toggleTrackMute({ trackId }: { trackId: string }): void {
|
||||
const command = new ToggleTrackMuteCommand(trackId);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
toggleTrackVisibility({ trackId }: { trackId: string }): void {
|
||||
const command = new ToggleTrackVisibilityCommand(trackId);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
toggleTrackVisibility({ trackId }: { trackId: string }): void {
|
||||
const command = new ToggleTrackVisibilityCommand(trackId);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
splitElements({
|
||||
elements,
|
||||
splitTime,
|
||||
retainSide = "both",
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
splitTime: number;
|
||||
retainSide?: "both" | "left" | "right";
|
||||
}): string[] {
|
||||
const command = new SplitElementsCommand(elements, splitTime, retainSide);
|
||||
this.editor.command.execute({ command });
|
||||
return command.splitElementIds;
|
||||
}
|
||||
splitElements({
|
||||
elements,
|
||||
splitTime,
|
||||
retainSide = "both",
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
splitTime: number;
|
||||
retainSide?: "both" | "left" | "right";
|
||||
}): string[] {
|
||||
const command = new SplitElementsCommand(elements, splitTime, retainSide);
|
||||
this.editor.command.execute({ command });
|
||||
return command.splitElementIds;
|
||||
}
|
||||
|
||||
getTotalDuration(): number {
|
||||
return calculateTotalDuration({ tracks: this.getTracks() });
|
||||
}
|
||||
getTotalDuration(): number {
|
||||
return calculateTotalDuration({ tracks: this.getTracks() });
|
||||
}
|
||||
|
||||
getTrackById({ trackId }: { trackId: string }): TimelineTrack | null {
|
||||
return this.getTracks().find((track) => track.id === trackId) ?? null;
|
||||
}
|
||||
getTrackById({ trackId }: { trackId: string }): TimelineTrack | null {
|
||||
return this.getTracks().find((track) => track.id === trackId) ?? null;
|
||||
}
|
||||
|
||||
getElementsWithTracks({
|
||||
elements,
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
}): Array<{ track: TimelineTrack; element: TimelineElement }> {
|
||||
const result: Array<{ track: TimelineTrack; element: TimelineElement }> =
|
||||
[];
|
||||
getElementsWithTracks({
|
||||
elements,
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
}): Array<{ track: TimelineTrack; element: TimelineElement }> {
|
||||
const result: Array<{ track: TimelineTrack; element: TimelineElement }> =
|
||||
[];
|
||||
|
||||
for (const { trackId, elementId } of elements) {
|
||||
const track = this.getTrackById({ trackId });
|
||||
const element = track?.elements.find(
|
||||
(trackElement) => trackElement.id === elementId,
|
||||
);
|
||||
for (const { trackId, elementId } of elements) {
|
||||
const track = this.getTrackById({ trackId });
|
||||
const element = track?.elements.find(
|
||||
(trackElement) => trackElement.id === elementId,
|
||||
);
|
||||
|
||||
if (track && element) {
|
||||
result.push({ track, element });
|
||||
}
|
||||
}
|
||||
if (track && element) {
|
||||
result.push({ track, element });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
pasteAtTime({
|
||||
time,
|
||||
clipboardItems,
|
||||
}: {
|
||||
time: number;
|
||||
clipboardItems: ClipboardItem[];
|
||||
}): void {
|
||||
const command = new PasteCommand(time, clipboardItems);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
pasteAtTime({
|
||||
time,
|
||||
clipboardItems,
|
||||
}: {
|
||||
time: number;
|
||||
clipboardItems: ClipboardItem[];
|
||||
}): void {
|
||||
const command = new PasteCommand(time, clipboardItems);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
deleteElements({
|
||||
elements,
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
}): void {
|
||||
const command = new DeleteElementsCommand(elements);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
deleteElements({
|
||||
elements,
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
}): void {
|
||||
const command = new DeleteElementsCommand(elements);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
updateTextElement({
|
||||
trackId,
|
||||
elementId,
|
||||
updates,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
updates: Partial<
|
||||
Pick<
|
||||
TextElement,
|
||||
| "content"
|
||||
| "fontSize"
|
||||
| "fontFamily"
|
||||
| "color"
|
||||
| "backgroundColor"
|
||||
| "textAlign"
|
||||
| "fontWeight"
|
||||
| "fontStyle"
|
||||
| "textDecoration"
|
||||
| "transform"
|
||||
| "opacity"
|
||||
>
|
||||
>;
|
||||
}): void {
|
||||
const command = new UpdateTextElementCommand(trackId, elementId, updates);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
updateTextElement({
|
||||
trackId,
|
||||
elementId,
|
||||
updates,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
updates: Partial<
|
||||
Pick<
|
||||
TextElement,
|
||||
| "content"
|
||||
| "fontSize"
|
||||
| "fontFamily"
|
||||
| "color"
|
||||
| "backgroundColor"
|
||||
| "textAlign"
|
||||
| "fontWeight"
|
||||
| "fontStyle"
|
||||
| "textDecoration"
|
||||
| "transform"
|
||||
| "opacity"
|
||||
>
|
||||
>;
|
||||
}): void {
|
||||
const command = new UpdateTextElementCommand(trackId, elementId, updates);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
duplicateElements({
|
||||
elements,
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
}): void {
|
||||
const command = new DuplicateElementsCommand({ elements });
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
duplicateElements({
|
||||
elements,
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
}): void {
|
||||
const command = new DuplicateElementsCommand({ elements });
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
toggleElementsVisibility({
|
||||
elements,
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
}): void {
|
||||
const command = new ToggleElementsVisibilityCommand(elements);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
toggleElementsVisibility({
|
||||
elements,
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
}): void {
|
||||
const command = new ToggleElementsVisibilityCommand(elements);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
toggleElementsMuted({
|
||||
elements,
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
}): void {
|
||||
const command = new ToggleElementsMutedCommand(elements);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
toggleElementsMuted({
|
||||
elements,
|
||||
}: {
|
||||
elements: { trackId: string; elementId: string }[];
|
||||
}): void {
|
||||
const command = new ToggleElementsMutedCommand(elements);
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
getTracks(): TimelineTrack[] {
|
||||
return this.editor.scenes.getActiveScene()?.tracks ?? [];
|
||||
}
|
||||
getTracks(): TimelineTrack[] {
|
||||
return this.editor.scenes.getActiveScene()?.tracks ?? [];
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
private notify(): void {
|
||||
this.listeners.forEach((fn) => fn());
|
||||
}
|
||||
private notify(): void {
|
||||
this.listeners.forEach((fn) => fn());
|
||||
}
|
||||
|
||||
updateTracks(newTracks: TimelineTrack[]): void {
|
||||
this.editor.scenes.updateSceneTracks({ tracks: newTracks });
|
||||
this.notify();
|
||||
}
|
||||
updateTracks(newTracks: TimelineTrack[]): void {
|
||||
this.editor.scenes.updateSceneTracks({ tracks: newTracks });
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user