1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core";
import { toast } from "sonner";
import type { MediaAsset } from "@/lib/media/types";
import { generateUUID } from "@/utils/id";
import { storageService } from "@/services/storage/service";
import { hasMediaId } from "@/lib/timeline/element-utils";
import { getHighestImportedVideoFps } from "@/lib/fps/utils";
import { UpdateProjectSettingsCommand } from "@/lib/commands/project";
export class AddMediaAssetCommand extends Command {
private assetId: string;
private savedAssets: MediaAsset[] | null = null;
private createdAsset: MediaAsset | null = null;
private previousProjectFps: number | null = null;
private appliedProjectFps: number | null = null;
constructor(
private projectId: string,
private asset: Omit<MediaAsset, "id">,
) {
super();
this.assetId = generateUUID();
}
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedAssets = [...editor.media.getAssets()];
this.createdAsset = {
...this.asset,
id: this.assetId,
};
editor.media.setAssets({
assets: [...this.savedAssets, this.createdAsset],
});
this.previousProjectFps = editor.project.getActiveOrNull()?.settings.fps ?? null;
this.appliedProjectFps = editor.project.ratchetFpsForImportedMedia({
importedAssets: [this.createdAsset],
});
storageService
.saveMediaAsset({
projectId: this.projectId,
mediaAsset: this.createdAsset,
})
.catch((error) => {
console.error("Failed to save media item:", error);
const currentAssets = editor.media.getAssets();
editor.media.setAssets({
assets: currentAssets.filter((asset) => asset.id !== this.assetId),
});
const currentTracks = editor.timeline.getTracks();
const orphanedElements: Array<{ trackId: string; elementId: string }> =
[];
for (const track of currentTracks) {
for (const element of track.elements) {
if (hasMediaId(element) && element.mediaId === this.assetId) {
orphanedElements.push({
trackId: track.id,
elementId: element.id,
});
}
}
}
if (orphanedElements.length > 0) {
editor.timeline.deleteElements({ elements: orphanedElements });
}
this.restoreProjectFpsAfterFailedSave({ editor });
if (storageService.isQuotaExceededError({ error })) {
toast.error("Not enough browser storage", {
description: error instanceof Error ? error.message : undefined,
});
}
});
return undefined;
}
undo(): void {
if (this.savedAssets) {
const editor = EditorCore.getInstance();
editor.media.setAssets({ assets: this.savedAssets });
if (this.createdAsset) {
storageService
.deleteMediaAsset({ projectId: this.projectId, id: this.assetId })
.catch((error) => {
console.error("Failed to delete media item on undo:", error);
});
}
}
}
getAssetId(): string {
return this.assetId;
}
private restoreProjectFpsAfterFailedSave({
editor,
}: {
editor: EditorCore;
}): void {
if (this.previousProjectFps === null || this.appliedProjectFps === null) return;
const activeProject = editor.project.getActiveOrNull();
if (!activeProject) return;
if (activeProject.settings.fps !== this.appliedProjectFps) return;
const highestRemainingVideoFps = getHighestImportedVideoFps({
mediaAssets: editor.media.getAssets(),
});
if (
highestRemainingVideoFps !== null &&
highestRemainingVideoFps >= this.appliedProjectFps
) {
return;
}
new UpdateProjectSettingsCommand({ fps: this.previousProjectFps }).execute();
}
}
|