From fe020be6cea00691d6ce66e21843915defa5e944 Mon Sep 17 00:00:00 2001 From: Hyteq Date: Sun, 20 Jul 2025 10:50:17 +0300 Subject: Revert "format, cleanup, deps" This reverts commit eab8ec3ba81b86fdd4ade920d999c28fb34785b1. --- apps/web/src/lib/storage/indexeddb-adapter.ts | 178 ++++----- apps/web/src/lib/storage/opfs-adapter.ts | 114 +++--- apps/web/src/lib/storage/storage-service.ts | 530 +++++++++++++------------- apps/web/src/lib/storage/types.ts | 54 +-- 4 files changed, 438 insertions(+), 438 deletions(-) (limited to 'apps/web/src/lib/storage') diff --git a/apps/web/src/lib/storage/indexeddb-adapter.ts b/apps/web/src/lib/storage/indexeddb-adapter.ts index b599e2df..f43a8798 100644 --- a/apps/web/src/lib/storage/indexeddb-adapter.ts +++ b/apps/web/src/lib/storage/indexeddb-adapter.ts @@ -1,89 +1,89 @@ -import { StorageAdapter } from "./types"; - -export class IndexedDBAdapter implements StorageAdapter { - private dbName: string; - private storeName: string; - private version: number; - - constructor(dbName: string, storeName: string, version: number = 1) { - this.dbName = dbName; - this.storeName = storeName; - this.version = version; - } - - private async getDB(): Promise { - 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 { - 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 { - 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 { - 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 { - 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 { - 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(); - }); - } -} +import { StorageAdapter } from "./types"; + +export class IndexedDBAdapter implements StorageAdapter { + private dbName: string; + private storeName: string; + private version: number; + + constructor(dbName: string, storeName: string, version: number = 1) { + this.dbName = dbName; + this.storeName = storeName; + this.version = version; + } + + private async getDB(): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 index ccd9735d..c01954e5 100644 --- a/apps/web/src/lib/storage/opfs-adapter.ts +++ b/apps/web/src/lib/storage/opfs-adapter.ts @@ -1,73 +1,73 @@ import { StorageAdapter } from "./types"; export class OPFSAdapter implements StorageAdapter { - private directoryName: string; + private directoryName: string; - constructor(directoryName: string = "media") { - this.directoryName = directoryName; - } + constructor(directoryName: string = "media") { + this.directoryName = directoryName; + } - private async getDirectory(): Promise { - const opfsRoot = await navigator.storage.getDirectory(); - return await opfsRoot.getDirectoryHandle(this.directoryName, { - create: true, - }); - } + private async getDirectory(): Promise { + const opfsRoot = await navigator.storage.getDirectory(); + return await opfsRoot.getDirectoryHandle(this.directoryName, { + create: true, + }); + } - async get(key: string): Promise { - 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 get(key: string): Promise { + 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 { - const directory = await this.getDirectory(); - const fileHandle = await directory.getFileHandle(key, { create: true }); - const writable = await fileHandle.createWritable(); + async set(key: string, file: File): Promise { + 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(); - } + await writable.write(file); + await writable.close(); + } - async remove(key: string): Promise { - try { - const directory = await this.getDirectory(); - await directory.removeEntry(key); - } catch (error) { - if ((error as Error).name !== "NotFoundError") { - throw error; - } - } - } + async remove(key: string): Promise { + try { + const directory = await this.getDirectory(); + await directory.removeEntry(key); + } catch (error) { + if ((error as Error).name !== "NotFoundError") { + throw error; + } + } + } - async list(): Promise { - const directory = await this.getDirectory(); - const keys: string[] = []; + async list(): Promise { + const directory = await this.getDirectory(); + const keys: string[] = []; - for await (const name of directory.keys()) { - keys.push(name); - } + for await (const name of directory.keys()) { + keys.push(name); + } - return keys; - } + return keys; + } - async clear(): Promise { - const directory = await this.getDirectory(); + async clear(): Promise { + const directory = await this.getDirectory(); - for await (const name of directory.keys()) { - await directory.removeEntry(name); - } - } + 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; - } + // 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 index 044f2dff..0037dbdb 100644 --- a/apps/web/src/lib/storage/storage-service.ts +++ b/apps/web/src/lib/storage/storage-service.ts @@ -3,275 +3,275 @@ import { MediaItem } from "@/stores/media-store"; import { IndexedDBAdapter } from "./indexeddb-adapter"; import { OPFSAdapter } from "./opfs-adapter"; import { - MediaFileData, - StorageConfig, - SerializedProject, - TimelineData, + MediaFileData, + StorageConfig, + SerializedProject, + TimelineData, } from "./types"; import { TimelineTrack } from "@/types/timeline"; class StorageService { - private projectsAdapter: IndexedDBAdapter; - private config: StorageConfig; - - constructor() { - this.config = { - projectsDb: "video-editor-projects", - mediaDb: "video-editor-media", - timelineDb: "video-editor-timelines", - version: 1, - }; - - this.projectsAdapter = new IndexedDBAdapter( - this.config.projectsDb, - "projects", - this.config.version, - ); - } - - // Helper to get project-specific media adapters - private getProjectMediaAdapters(projectId: string) { - const mediaMetadataAdapter = new IndexedDBAdapter( - `${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: string) { - return new IndexedDBAdapter( - `${this.config.timelineDb}-${projectId}`, - "timeline", - this.config.version, - ); - } - - // 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(), - backgroundColor: project.backgroundColor, - backgroundType: project.backgroundType, - blurIntensity: project.blurIntensity, - }; - - 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), - backgroundColor: serializedProject.backgroundColor, - backgroundType: serializedProject.backgroundType, - blurIntensity: serializedProject.blurIntensity, - }; - } - - 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 - now project-specific - async saveMediaItem(projectId: string, mediaItem: MediaItem): Promise { - 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, - }; - - await mediaMetadataAdapter.set(mediaItem.id, metadata); - } - - async loadMediaItem( - projectId: string, - id: string, - ): Promise { - const { mediaMetadataAdapter, mediaFilesAdapter } = - this.getProjectMediaAdapters(projectId); - - const [file, metadata] = await Promise.all([ - mediaFilesAdapter.get(id), - 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, - width: metadata.width, - height: metadata.height, - duration: metadata.duration, - // thumbnailUrl would need to be regenerated or cached separately - }; - } - - async loadAllMediaItems(projectId: string): Promise { - const { mediaMetadataAdapter } = this.getProjectMediaAdapters(projectId); - - const mediaIds = await mediaMetadataAdapter.list(); - const mediaItems: MediaItem[] = []; - - for (const id of mediaIds) { - const item = await this.loadMediaItem(projectId, id); - if (item) { - mediaItems.push(item); - } - } - - return mediaItems; - } - - async deleteMediaItem(projectId: string, id: string): Promise { - const { mediaMetadataAdapter, mediaFilesAdapter } = - this.getProjectMediaAdapters(projectId); - - await Promise.all([ - mediaFilesAdapter.remove(id), - mediaMetadataAdapter.remove(id), - ]); - } - - async deleteProjectMedia(projectId: string): Promise { - const { mediaMetadataAdapter, mediaFilesAdapter } = - this.getProjectMediaAdapters(projectId); - - await Promise.all([ - mediaMetadataAdapter.clear(), - mediaFilesAdapter.clear(), - ]); - } - - // Timeline operations - now project-specific - async saveTimeline( - projectId: string, - tracks: TimelineTrack[], - ): Promise { - const timelineAdapter = this.getProjectTimelineAdapter(projectId); - const timelineData: TimelineData = { - tracks, - lastModified: new Date().toISOString(), - }; - await timelineAdapter.set("timeline", timelineData); - } - - async loadTimeline(projectId: string): Promise { - const timelineAdapter = this.getProjectTimelineAdapter(projectId); - const timelineData = await timelineAdapter.get("timeline"); - return timelineData ? timelineData.tracks : null; - } - - async deleteProjectTimeline(projectId: string): Promise { - const timelineAdapter = this.getProjectTimelineAdapter(projectId); - await timelineAdapter.remove("timeline"); - } - - // Utility methods - async clearAllData(): Promise { - // 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: 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, - }; - } - - // Check browser support - isOPFSSupported(): boolean { - return OPFSAdapter.isSupported(); - } - - isIndexedDBSupported(): boolean { - return "indexedDB" in window; - } - - isFullySupported(): boolean { - return this.isIndexedDBSupported() && this.isOPFSSupported(); - } + private projectsAdapter: IndexedDBAdapter; + private config: StorageConfig; + + constructor() { + this.config = { + projectsDb: "video-editor-projects", + mediaDb: "video-editor-media", + timelineDb: "video-editor-timelines", + version: 1, + }; + + this.projectsAdapter = new IndexedDBAdapter( + this.config.projectsDb, + "projects", + this.config.version + ); + } + + // Helper to get project-specific media adapters + private getProjectMediaAdapters(projectId: string) { + const mediaMetadataAdapter = new IndexedDBAdapter( + `${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: string) { + return new IndexedDBAdapter( + `${this.config.timelineDb}-${projectId}`, + "timeline", + this.config.version + ); + } + + // 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(), + backgroundColor: project.backgroundColor, + backgroundType: project.backgroundType, + blurIntensity: project.blurIntensity, + }; + + 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), + backgroundColor: serializedProject.backgroundColor, + backgroundType: serializedProject.backgroundType, + blurIntensity: serializedProject.blurIntensity, + }; + } + + 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 - now project-specific + async saveMediaItem(projectId: string, mediaItem: MediaItem): Promise { + 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, + }; + + await mediaMetadataAdapter.set(mediaItem.id, metadata); + } + + async loadMediaItem( + projectId: string, + id: string + ): Promise { + const { mediaMetadataAdapter, mediaFilesAdapter } = + this.getProjectMediaAdapters(projectId); + + const [file, metadata] = await Promise.all([ + mediaFilesAdapter.get(id), + 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, + width: metadata.width, + height: metadata.height, + duration: metadata.duration, + // thumbnailUrl would need to be regenerated or cached separately + }; + } + + async loadAllMediaItems(projectId: string): Promise { + const { mediaMetadataAdapter } = this.getProjectMediaAdapters(projectId); + + const mediaIds = await mediaMetadataAdapter.list(); + const mediaItems: MediaItem[] = []; + + for (const id of mediaIds) { + const item = await this.loadMediaItem(projectId, id); + if (item) { + mediaItems.push(item); + } + } + + return mediaItems; + } + + async deleteMediaItem(projectId: string, id: string): Promise { + const { mediaMetadataAdapter, mediaFilesAdapter } = + this.getProjectMediaAdapters(projectId); + + await Promise.all([ + mediaFilesAdapter.remove(id), + mediaMetadataAdapter.remove(id), + ]); + } + + async deleteProjectMedia(projectId: string): Promise { + const { mediaMetadataAdapter, mediaFilesAdapter } = + this.getProjectMediaAdapters(projectId); + + await Promise.all([ + mediaMetadataAdapter.clear(), + mediaFilesAdapter.clear(), + ]); + } + + // Timeline operations - now project-specific + async saveTimeline( + projectId: string, + tracks: TimelineTrack[] + ): Promise { + const timelineAdapter = this.getProjectTimelineAdapter(projectId); + const timelineData: TimelineData = { + tracks, + lastModified: new Date().toISOString(), + }; + await timelineAdapter.set("timeline", timelineData); + } + + async loadTimeline(projectId: string): Promise { + const timelineAdapter = this.getProjectTimelineAdapter(projectId); + const timelineData = await timelineAdapter.get("timeline"); + return timelineData ? timelineData.tracks : null; + } + + async deleteProjectTimeline(projectId: string): Promise { + const timelineAdapter = this.getProjectTimelineAdapter(projectId); + await timelineAdapter.remove("timeline"); + } + + // Utility methods + async clearAllData(): Promise { + // 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: 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, + }; + } + + // Check browser support + isOPFSSupported(): boolean { + return OPFSAdapter.isSupported(); + } + + isIndexedDBSupported(): boolean { + return "indexedDB" in window; + } + + isFullySupported(): boolean { + return this.isIndexedDBSupported() && this.isOPFSSupported(); + } } // Export singleton instance diff --git a/apps/web/src/lib/storage/types.ts b/apps/web/src/lib/storage/types.ts index 1adbb3b8..b662c164 100644 --- a/apps/web/src/lib/storage/types.ts +++ b/apps/web/src/lib/storage/types.ts @@ -2,48 +2,48 @@ import { TProject } from "@/types/project"; import { TimelineTrack } from "@/types/timeline"; export interface StorageAdapter { - get(key: string): Promise; - set(key: string, value: T): Promise; - remove(key: string): Promise; - list(): Promise; - clear(): Promise; + get(key: string): Promise; + set(key: string, value: T): Promise; + remove(key: string): Promise; + list(): Promise; + clear(): Promise; } export interface MediaFileData { - id: string; - name: string; - type: "image" | "video" | "audio"; - size: number; - lastModified: number; - width?: number; - height?: number; - duration?: number; - // File will be stored separately in OPFS + id: string; + name: string; + type: "image" | "video" | "audio"; + size: number; + lastModified: number; + width?: number; + height?: number; + duration?: number; + // File will be stored separately in OPFS } export interface TimelineData { - tracks: TimelineTrack[]; - lastModified: string; + tracks: TimelineTrack[]; + lastModified: string; } export interface StorageConfig { - projectsDb: string; - mediaDb: string; - timelineDb: string; - version: number; + projectsDb: string; + mediaDb: string; + timelineDb: string; + version: number; } // Helper type for serialization - converts Date objects to strings export type SerializedProject = Omit & { - createdAt: string; - updatedAt: string; + createdAt: string; + updatedAt: string; }; // Extend FileSystemDirectoryHandle with missing async iterator methods declare global { - interface FileSystemDirectoryHandle { - keys(): AsyncIterableIterator; - values(): AsyncIterableIterator; - entries(): AsyncIterableIterator<[string, FileSystemHandle]>; - } + interface FileSystemDirectoryHandle { + keys(): AsyncIterableIterator; + values(): AsyncIterableIterator; + entries(): AsyncIterableIterator<[string, FileSystemHandle]>; + } } -- cgit v1.3.1