From 09373eb4a3f792d96d97d81f4fb86539d76e5702 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 30 Jun 2025 19:58:36 +0200 Subject: feat: implement project management features with storage integration, including project creation, deletion, and renaming dialogs --- apps/web/src/lib/storage/storage-service.ts | 192 ++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 apps/web/src/lib/storage/storage-service.ts (limited to 'apps/web/src/lib/storage/storage-service.ts') diff --git a/apps/web/src/lib/storage/storage-service.ts b/apps/web/src/lib/storage/storage-service.ts new file mode 100644 index 00000000..5582ae29 --- /dev/null +++ b/apps/web/src/lib/storage/storage-service.ts @@ -0,0 +1,192 @@ +import { TProject } from "@/types/project"; +import { MediaItem } from "@/stores/media-store"; +import { IndexedDBAdapter } from "./indexeddb-adapter"; +import { OPFSAdapter } from "./opfs-adapter"; +import { MediaFileData, StorageConfig, SerializedProject } from "./types"; + +class StorageService { + private projectsAdapter: IndexedDBAdapter; + private mediaMetadataAdapter: IndexedDBAdapter; + private mediaFilesAdapter: OPFSAdapter; + private config: StorageConfig; + + constructor() { + this.config = { + projectsDb: "video-editor-projects", + mediaDb: "video-editor-media", + version: 1, + }; + + this.projectsAdapter = new IndexedDBAdapter( + this.config.projectsDb, + "projects", + this.config.version + ); + + this.mediaMetadataAdapter = new IndexedDBAdapter( + this.config.mediaDb, + "media-metadata", + this.config.version + ); + + this.mediaFilesAdapter = new OPFSAdapter("media-files"); + } + + // Project operations + async saveProject(project: TProject): Promise { + // Convert TProject to serializable format + const serializedProject: SerializedProject = { + id: project.id, + name: project.name, + thumbnail: project.thumbnail, + createdAt: project.createdAt.toISOString(), + updatedAt: project.updatedAt.toISOString(), + }; + + await this.projectsAdapter.set(project.id, serializedProject); + } + + async loadProject(id: string): Promise { + const serializedProject = await this.projectsAdapter.get(id); + + if (!serializedProject) return null; + + // Convert back to TProject format + return { + id: serializedProject.id, + name: serializedProject.name, + thumbnail: serializedProject.thumbnail, + createdAt: new Date(serializedProject.createdAt), + updatedAt: new Date(serializedProject.updatedAt), + }; + } + + async loadAllProjects(): Promise { + 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: string): Promise { + await this.projectsAdapter.remove(id); + } + + // Media operations + async saveMediaItem(mediaItem: MediaItem): Promise { + // Save file to OPFS + await this.mediaFilesAdapter.set(mediaItem.id, mediaItem.file); + + // Save metadata to IndexedDB + const metadata: MediaFileData = { + id: mediaItem.id, + name: mediaItem.name, + type: mediaItem.type, + size: mediaItem.file.size, + lastModified: mediaItem.file.lastModified, + aspectRatio: mediaItem.aspectRatio, + duration: mediaItem.duration, + }; + + await this.mediaMetadataAdapter.set(mediaItem.id, metadata); + } + + async loadMediaItem(id: string): Promise { + const [file, metadata] = await Promise.all([ + this.mediaFilesAdapter.get(id), + this.mediaMetadataAdapter.get(id), + ]); + + if (!file || !metadata) return null; + + // Create new object URL for the file + const url = URL.createObjectURL(file); + + return { + id: metadata.id, + name: metadata.name, + type: metadata.type, + file, + url, + aspectRatio: metadata.aspectRatio, + duration: metadata.duration, + // thumbnailUrl would need to be regenerated or cached separately + }; + } + + async loadAllMediaItems(): Promise { + const mediaIds = await this.mediaMetadataAdapter.list(); + const mediaItems: MediaItem[] = []; + + for (const id of mediaIds) { + const item = await this.loadMediaItem(id); + if (item) { + mediaItems.push(item); + } + } + + return mediaItems; + } + + async deleteMediaItem(id: string): Promise { + await Promise.all([ + this.mediaFilesAdapter.remove(id), + this.mediaMetadataAdapter.remove(id), + ]); + } + + // Utility methods + async clearAllData(): Promise { + await Promise.all([ + this.projectsAdapter.clear(), + this.mediaMetadataAdapter.clear(), + this.mediaFilesAdapter.clear(), + ]); + } + + async getStorageInfo(): Promise<{ + projects: number; + mediaItems: number; + isOPFSSupported: boolean; + isIndexedDBSupported: boolean; + }> { + const [projectIds, mediaIds] = await Promise.all([ + this.projectsAdapter.list(), + this.mediaMetadataAdapter.list(), + ]); + + return { + projects: projectIds.length, + mediaItems: mediaIds.length, + isOPFSSupported: this.isOPFSSupported(), + isIndexedDBSupported: this.isIndexedDBSupported(), + }; + } + + // 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 }; -- cgit v1.3.1