diff options
| author | Maze <167211895+mazeincoding@users.noreply.github.com> | 2026-01-31 00:20:04 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-31 00:20:04 +0100 |
| commit | 7bf0984698e43b9aa2dd879f65951318ed410390 (patch) | |
| tree | ccf2cd590a47d550152afc07129cb55c71a9ec35 /apps/web/src/lib/storage | |
| parent | 0173db99448d1c4c6415dcf15272e17551b48b04 (diff) | |
| download | opencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz | |
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/lib/storage')
| -rw-r--r-- | apps/web/src/lib/storage/indexeddb-adapter.ts | 89 | ||||
| -rw-r--r-- | apps/web/src/lib/storage/opfs-adapter.ts | 73 | ||||
| -rw-r--r-- | apps/web/src/lib/storage/storage-service.ts | 480 | ||||
| -rw-r--r-- | apps/web/src/lib/storage/types.ts | 69 |
4 files changed, 0 insertions, 711 deletions
diff --git a/apps/web/src/lib/storage/indexeddb-adapter.ts b/apps/web/src/lib/storage/indexeddb-adapter.ts deleted file mode 100644 index 597305c7..00000000 --- a/apps/web/src/lib/storage/indexeddb-adapter.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { StorageAdapter } from "./types"; - -export class IndexedDBAdapter<T> implements StorageAdapter<T> { - private dbName: string; - private storeName: string; - private version: number; - - constructor(dbName: string, storeName: string, version = 1) { - this.dbName = dbName; - this.storeName = storeName; - this.version = version; - } - - private async getDB(): Promise<IDBDatabase> { - return new Promise((resolve, reject) => { - const request = indexedDB.open(this.dbName, this.version); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result); - - request.onupgradeneeded = (event) => { - const db = (event.target as IDBOpenDBRequest).result; - if (!db.objectStoreNames.contains(this.storeName)) { - db.createObjectStore(this.storeName, { keyPath: "id" }); - } - }; - }); - } - - async get(key: string): Promise<T | null> { - const db = await this.getDB(); - const transaction = db.transaction([this.storeName], "readonly"); - const store = transaction.objectStore(this.storeName); - - return new Promise((resolve, reject) => { - const request = store.get(key); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result || null); - }); - } - - async set(key: string, value: T): Promise<void> { - const db = await this.getDB(); - const transaction = db.transaction([this.storeName], "readwrite"); - const store = transaction.objectStore(this.storeName); - - return new Promise((resolve, reject) => { - const request = store.put({ id: key, ...value }); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(); - }); - } - - async remove(key: string): Promise<void> { - const db = await this.getDB(); - const transaction = db.transaction([this.storeName], "readwrite"); - const store = transaction.objectStore(this.storeName); - - return new Promise((resolve, reject) => { - const request = store.delete(key); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(); - }); - } - - async list(): Promise<string[]> { - const db = await this.getDB(); - const transaction = db.transaction([this.storeName], "readonly"); - const store = transaction.objectStore(this.storeName); - - return new Promise((resolve, reject) => { - const request = store.getAllKeys(); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result as string[]); - }); - } - - async clear(): Promise<void> { - const db = await this.getDB(); - const transaction = db.transaction([this.storeName], "readwrite"); - const store = transaction.objectStore(this.storeName); - - return new Promise((resolve, reject) => { - const request = store.clear(); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(); - }); - } -} diff --git a/apps/web/src/lib/storage/opfs-adapter.ts b/apps/web/src/lib/storage/opfs-adapter.ts deleted file mode 100644 index 87b5c886..00000000 --- a/apps/web/src/lib/storage/opfs-adapter.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { StorageAdapter } from "./types"; - -export class OPFSAdapter implements StorageAdapter<File> { - private directoryName: string; - - constructor(directoryName = "media") { - this.directoryName = directoryName; - } - - private async getDirectory(): Promise<FileSystemDirectoryHandle> { - const opfsRoot = await navigator.storage.getDirectory(); - return await opfsRoot.getDirectoryHandle(this.directoryName, { - create: true, - }); - } - - async get(key: string): Promise<File | null> { - try { - const directory = await this.getDirectory(); - const fileHandle = await directory.getFileHandle(key); - return await fileHandle.getFile(); - } catch (error) { - if ((error as Error).name === "NotFoundError") { - return null; - } - throw error; - } - } - - async set(key: string, file: File): Promise<void> { - const directory = await this.getDirectory(); - const fileHandle = await directory.getFileHandle(key, { create: true }); - const writable = await fileHandle.createWritable(); - - await writable.write(file); - await writable.close(); - } - - async remove(key: string): Promise<void> { - try { - const directory = await this.getDirectory(); - await directory.removeEntry(key); - } catch (error) { - if ((error as Error).name !== "NotFoundError") { - throw error; - } - } - } - - async list(): Promise<string[]> { - const directory = await this.getDirectory(); - const keys: string[] = []; - - for await (const name of directory.keys()) { - keys.push(name); - } - - return keys; - } - - async clear(): Promise<void> { - const directory = await this.getDirectory(); - - for await (const name of directory.keys()) { - await directory.removeEntry(name); - } - } - - // Helper method to check OPFS support - static isSupported(): boolean { - return "storage" in navigator && "getDirectory" in navigator.storage; - } -} diff --git a/apps/web/src/lib/storage/storage-service.ts b/apps/web/src/lib/storage/storage-service.ts deleted file mode 100644 index 2688f6e8..00000000 --- a/apps/web/src/lib/storage/storage-service.ts +++ /dev/null @@ -1,480 +0,0 @@ -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(), - })); - - 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, - bookmarks: project.bookmarks, - fps: project.fps, - canvasSize: project.canvasSize, - canvasMode: project.canvasMode, - }; - - 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), - })) || []; - - // 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, - bookmarks: serializedProject.bookmarks, - fps: serializedProject.fps, - canvasSize: serializedProject.canvasSize, - canvasMode: serializedProject.canvasMode, - }; - 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 }; diff --git a/apps/web/src/lib/storage/types.ts b/apps/web/src/lib/storage/types.ts deleted file mode 100644 index e66239cd..00000000 --- a/apps/web/src/lib/storage/types.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { TProject, Scene } from "@/types/project"; -import { TimelineTrack } from "@/types/timeline"; - -export interface StorageAdapter<T> { - get(key: string): Promise<T | null>; - set(key: string, value: T): Promise<void>; - remove(key: string): Promise<void>; - list(): Promise<string[]>; - clear(): Promise<void>; -} - -export interface MediaFileData { - id: string; - name: string; - type: "image" | "video" | "audio"; - size: number; - lastModified: number; - width?: number; - height?: number; - duration?: number; - ephemeral?: boolean; - sourceStickerIconName?: string; - // File will be stored separately in OPFS -} - -// Legacy timeline data, kept for backward compatibility -export interface TimelineData { - tracks: TimelineTrack[]; - lastModified: string; -} - -export interface SceneTimelineData { - sceneId: string; - tracks: TimelineTrack[]; - lastModified: string; -} - -export type SerializedScene = Omit<Scene, "createdAt" | "updatedAt"> & { - createdAt: string; - updatedAt: string; -}; - -export interface StorageConfig { - projectsDb: string; - mediaDb: string; - timelineDb: string; - savedSoundsDb: string; - version: number; -} - -// Helper type for serialization - converts Date objects to strings -export type SerializedProject = Omit< - TProject, - "createdAt" | "updatedAt" | "scenes" -> & { - createdAt: string; - updatedAt: string; - scenes: SerializedScene[]; - bookmarks?: number[]; -}; - -// Extend FileSystemDirectoryHandle with missing async iterator methods -declare global { - interface FileSystemDirectoryHandle { - keys(): AsyncIterableIterator<string>; - values(): AsyncIterableIterator<FileSystemHandle>; - entries(): AsyncIterableIterator<[string, FileSystemHandle]>; - } -} |
