refactor not done

This commit is contained in:
Maze Winther
2025-11-26 08:47:03 +01:00
commit efbebd13b8
431 changed files with 51577 additions and 0 deletions
+478
View File
@@ -0,0 +1,478 @@
import { TProject } from "@/types/project";
import { MediaFile } from "@/types/media";
import { IndexedDBAdapter } from "./indexeddb-adapter";
import { OPFSAdapter } from "./opfs-adapter";
import {
MediaFileData,
StorageConfig,
SerializedProject,
SerializedScene,
TimelineData,
} from "./types";
import { TimelineTrack } from "@/types/timeline";
import { SavedSoundsData, SavedSound, SoundEffect } from "@/types/sounds";
class StorageService {
private projectsAdapter: IndexedDBAdapter<SerializedProject>;
private savedSoundsAdapter: IndexedDBAdapter<SavedSoundsData>;
private config: StorageConfig;
constructor() {
this.config = {
projectsDb: "video-editor-projects",
mediaDb: "video-editor-media",
timelineDb: "video-editor-timelines",
savedSoundsDb: "video-editor-saved-sounds",
version: 1,
};
this.projectsAdapter = new IndexedDBAdapter<SerializedProject>(
this.config.projectsDb,
"projects",
this.config.version,
);
this.savedSoundsAdapter = new IndexedDBAdapter<SavedSoundsData>(
this.config.savedSoundsDb,
"saved-sounds",
this.config.version,
);
}
// Helper to get project-specific media adapters
private getProjectMediaAdapters({ projectId }: { projectId: string }) {
const mediaMetadataAdapter = new IndexedDBAdapter<MediaFileData>(
`${this.config.mediaDb}-${projectId}`,
"media-metadata",
this.config.version,
);
const mediaFilesAdapter = new OPFSAdapter(`media-files-${projectId}`);
return { mediaMetadataAdapter, mediaFilesAdapter };
}
// Helper to get project-specific timeline adapter
private getProjectTimelineAdapter({
projectId,
sceneId,
}: {
projectId: string;
sceneId?: string;
}) {
const dbName = sceneId
? `${this.config.timelineDb}-${projectId}-${sceneId}`
: `${this.config.timelineDb}-${projectId}`;
return new IndexedDBAdapter<TimelineData>(
dbName,
"timeline",
this.config.version,
);
}
// Project operations
async saveProject({ project }: { project: TProject }): Promise<void> {
// Convert TProject to serializable format
const serializedScenes: SerializedScene[] = project.scenes.map((scene) => ({
id: scene.id,
name: scene.name,
isMain: scene.isMain,
createdAt: scene.createdAt.toISOString(),
updatedAt: scene.updatedAt.toISOString(),
timeline: scene.timeline,
}));
const serializedProject: SerializedProject = {
id: project.id,
name: project.name,
thumbnail: project.thumbnail,
createdAt: project.createdAt.toISOString(),
updatedAt: project.updatedAt.toISOString(),
scenes: serializedScenes,
currentSceneId: project.currentSceneId,
backgroundColor: project.backgroundColor,
backgroundType: project.backgroundType,
blurIntensity: project.blurIntensity,
fps: project.fps,
canvasSize: project.canvasSize,
};
await this.projectsAdapter.set(project.id, serializedProject);
}
async loadProject({ id }: { id: string }): Promise<TProject | null> {
const serializedProject = await this.projectsAdapter.get(id);
if (!serializedProject) return null;
// Now convert serialized scenes back to Scene objects
const scenes =
serializedProject.scenes?.map((scene) => ({
id: scene.id,
name: scene.name,
isMain: scene.isMain,
createdAt: new Date(scene.createdAt),
updatedAt: new Date(scene.updatedAt),
timeline: scene.timeline,
})) || [];
// Convert back to TProject format
const project = {
id: serializedProject.id,
name: serializedProject.name,
thumbnail: serializedProject.thumbnail,
createdAt: new Date(serializedProject.createdAt),
updatedAt: new Date(serializedProject.updatedAt),
scenes,
currentSceneId: serializedProject.currentSceneId || "",
backgroundColor: serializedProject.backgroundColor,
backgroundType: serializedProject.backgroundType,
blurIntensity: serializedProject.blurIntensity,
fps: serializedProject.fps,
canvasSize: serializedProject.canvasSize,
};
return project;
}
async loadAllProjects(): Promise<TProject[]> {
const projectIds = await this.projectsAdapter.list();
const projects: TProject[] = [];
for (const id of projectIds) {
const project = await this.loadProject({ id });
if (project) {
projects.push(project);
}
}
// Sort by last updated (most recent first)
return projects.sort(
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
);
}
async deleteProject({ id }: { id: string }): Promise<void> {
await this.projectsAdapter.remove(id);
}
// Media operations
async saveMediaFile({
projectId,
mediaItem,
}: {
projectId: string;
mediaItem: MediaFile;
}): Promise<void> {
const { mediaMetadataAdapter, mediaFilesAdapter } =
this.getProjectMediaAdapters({ projectId });
// Save file to project-specific OPFS
await mediaFilesAdapter.set(mediaItem.id, mediaItem.file);
// Save metadata to project-specific IndexedDB
const metadata: MediaFileData = {
id: mediaItem.id,
name: mediaItem.name,
type: mediaItem.type,
size: mediaItem.file.size,
lastModified: mediaItem.file.lastModified,
width: mediaItem.width,
height: mediaItem.height,
duration: mediaItem.duration,
ephemeral: mediaItem.ephemeral,
};
await mediaMetadataAdapter.set(mediaItem.id, metadata);
}
async loadMediaFile({
projectId,
id,
}: {
projectId: string;
id: string;
}): Promise<MediaFile | null> {
const { mediaMetadataAdapter, mediaFilesAdapter } =
this.getProjectMediaAdapters({ projectId });
const [file, metadata] = await Promise.all([
mediaFilesAdapter.get(id),
mediaMetadataAdapter.get(id),
]);
if (!file || !metadata) return null;
let url: string;
if (metadata.type === "image" && (!file.type || file.type === "")) {
try {
const text = await file.text();
if (text.trim().startsWith("<svg")) {
const svgBlob = new Blob([text], { type: "image/svg+xml" });
url = URL.createObjectURL(svgBlob);
} else {
url = URL.createObjectURL(file);
}
} catch {
url = URL.createObjectURL(file);
}
} else {
url = URL.createObjectURL(file);
}
return {
id: metadata.id,
name: metadata.name,
type: metadata.type,
file,
url,
width: metadata.width,
height: metadata.height,
duration: metadata.duration,
ephemeral: metadata.ephemeral,
};
}
async loadAllMediaFiles({
projectId,
}: {
projectId: string;
}): Promise<MediaFile[]> {
const { mediaMetadataAdapter } = this.getProjectMediaAdapters({
projectId,
});
const mediaIds = await mediaMetadataAdapter.list();
const mediaItems: MediaFile[] = [];
for (const id of mediaIds) {
const item = await this.loadMediaFile({ projectId, id });
if (item) {
mediaItems.push(item);
}
}
return mediaItems;
}
async deleteMediaFile({
projectId,
id,
}: {
projectId: string;
id: string;
}): Promise<void> {
const { mediaMetadataAdapter, mediaFilesAdapter } =
this.getProjectMediaAdapters({ projectId });
await Promise.all([
mediaFilesAdapter.remove(id),
mediaMetadataAdapter.remove(id),
]);
}
async deleteProjectMedia({
projectId,
}: {
projectId: string;
}): Promise<void> {
const { mediaMetadataAdapter, mediaFilesAdapter } =
this.getProjectMediaAdapters({ projectId });
await Promise.all([
mediaMetadataAdapter.clear(),
mediaFilesAdapter.clear(),
]);
}
// Timeline operations - supports both legacy and scene-based storage
async saveTimeline({
projectId,
tracks,
sceneId,
}: {
projectId: string;
tracks: TimelineTrack[];
sceneId?: string;
}): Promise<void> {
const timelineAdapter = this.getProjectTimelineAdapter({
projectId,
sceneId,
});
const timelineData: TimelineData = {
tracks,
lastModified: new Date().toISOString(),
};
await timelineAdapter.set("timeline", timelineData);
}
async loadTimeline({
projectId,
sceneId,
}: {
projectId: string;
sceneId?: string;
}): Promise<TimelineTrack[] | null> {
const timelineAdapter = this.getProjectTimelineAdapter({
projectId,
sceneId,
});
const timelineData = await timelineAdapter.get("timeline");
return timelineData ? timelineData.tracks : null;
}
async deleteProjectTimeline({
projectId,
}: {
projectId: string;
}): Promise<void> {
const timelineAdapter = this.getProjectTimelineAdapter({ projectId });
await timelineAdapter.remove("timeline");
}
// Utility methods
async clearAllData(): Promise<void> {
// Clear all projects
await this.projectsAdapter.clear();
// Note: Project-specific media and timelines will be cleaned up when projects are deleted
}
async getStorageInfo(): Promise<{
projects: number;
isOPFSSupported: boolean;
isIndexedDBSupported: boolean;
}> {
const projectIds = await this.projectsAdapter.list();
return {
projects: projectIds.length,
isOPFSSupported: this.isOPFSSupported(),
isIndexedDBSupported: this.isIndexedDBSupported(),
};
}
async getProjectStorageInfo({ projectId }: { projectId: string }): Promise<{
mediaItems: number;
hasTimeline: boolean;
}> {
const { mediaMetadataAdapter } = this.getProjectMediaAdapters({
projectId,
});
const timelineAdapter = this.getProjectTimelineAdapter({ projectId });
const [mediaIds, timelineData] = await Promise.all([
mediaMetadataAdapter.list(),
timelineAdapter.get("timeline"),
]);
return {
mediaItems: mediaIds.length,
hasTimeline: !!timelineData,
};
}
async loadSavedSounds(): Promise<SavedSoundsData> {
try {
const savedSoundsData = await this.savedSoundsAdapter.get("user-sounds");
return (
savedSoundsData || {
sounds: [],
lastModified: new Date().toISOString(),
}
);
} catch (error) {
console.error("Failed to load saved sounds:", error);
return { sounds: [], lastModified: new Date().toISOString() };
}
}
async saveSoundEffect({
soundEffect,
}: {
soundEffect: SoundEffect;
}): Promise<void> {
try {
const currentData = await this.loadSavedSounds();
// Check if sound is already saved
if (currentData.sounds.some((sound) => sound.id === soundEffect.id)) {
return; // Already saved
}
const savedSound: SavedSound = {
id: soundEffect.id,
name: soundEffect.name,
username: soundEffect.username,
previewUrl: soundEffect.previewUrl,
downloadUrl: soundEffect.downloadUrl,
duration: soundEffect.duration,
tags: soundEffect.tags,
license: soundEffect.license,
savedAt: new Date().toISOString(),
};
const updatedData: SavedSoundsData = {
sounds: [...currentData.sounds, savedSound],
lastModified: new Date().toISOString(),
};
await this.savedSoundsAdapter.set("user-sounds", updatedData);
} catch (error) {
console.error("Failed to save sound effect:", error);
throw error;
}
}
async removeSavedSound({ soundId }: { soundId: number }): Promise<void> {
try {
const currentData = await this.loadSavedSounds();
const updatedData: SavedSoundsData = {
sounds: currentData.sounds.filter((sound) => sound.id !== soundId),
lastModified: new Date().toISOString(),
};
await this.savedSoundsAdapter.set("user-sounds", updatedData);
} catch (error) {
console.error("Failed to remove saved sound:", error);
throw error;
}
}
async isSoundSaved({ soundId }: { soundId: number }): Promise<boolean> {
try {
const currentData = await this.loadSavedSounds();
return currentData.sounds.some((sound) => sound.id === soundId);
} catch (error) {
console.error("Failed to check if sound is saved:", error);
return false;
}
}
async clearSavedSounds(): Promise<void> {
try {
await this.savedSoundsAdapter.remove("user-sounds");
} catch (error) {
console.error("Failed to clear saved sounds:", error);
throw error;
}
}
// Check browser support
isOPFSSupported(): boolean {
return OPFSAdapter.isSupported();
}
isIndexedDBSupported(): boolean {
return "indexedDB" in window;
}
isFullySupported(): boolean {
return this.isIndexedDBSupported() && this.isOPFSSupported();
}
}
// Export singleton instance
export const storageService = new StorageService();
export { StorageService };