diff options
Diffstat (limited to 'apps/web/src/lib/storage')
| -rw-r--r-- | apps/web/src/lib/storage/storage-service.ts | 467 | ||||
| -rw-r--r-- | apps/web/src/lib/storage/types.ts | 7 |
2 files changed, 280 insertions, 194 deletions
diff --git a/apps/web/src/lib/storage/storage-service.ts b/apps/web/src/lib/storage/storage-service.ts index 65b3c134..79e932bd 100644 --- a/apps/web/src/lib/storage/storage-service.ts +++ b/apps/web/src/lib/storage/storage-service.ts @@ -1,194 +1,273 @@ -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<SerializedProject>;
- private mediaMetadataAdapter: IndexedDBAdapter<MediaFileData>;
- private mediaFilesAdapter: OPFSAdapter;
- private config: StorageConfig;
-
- constructor() {
- this.config = {
- projectsDb: "video-editor-projects",
- mediaDb: "video-editor-media",
- version: 1,
- };
-
- this.projectsAdapter = new IndexedDBAdapter<SerializedProject>(
- this.config.projectsDb,
- "projects",
- this.config.version
- );
-
- this.mediaMetadataAdapter = new IndexedDBAdapter<MediaFileData>(
- this.config.mediaDb,
- "media-metadata",
- this.config.version
- );
-
- this.mediaFilesAdapter = new OPFSAdapter("media-files");
- }
-
- // Project operations
- async saveProject(project: TProject): Promise<void> {
- // 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<TProject | null> {
- 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<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: string): Promise<void> {
- await this.projectsAdapter.remove(id);
- }
-
- // Media operations
- async saveMediaItem(mediaItem: MediaItem): Promise<void> {
- // 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,
- width: mediaItem.width,
- height: mediaItem.height,
- duration: mediaItem.duration,
- };
-
- await this.mediaMetadataAdapter.set(mediaItem.id, metadata);
- }
-
- async loadMediaItem(id: string): Promise<MediaItem | null> {
- 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,
- width: metadata.width,
- height: metadata.height,
- duration: metadata.duration,
- // thumbnailUrl would need to be regenerated or cached separately
- };
- }
-
- async loadAllMediaItems(): Promise<MediaItem[]> {
- 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<void> {
- await Promise.all([
- this.mediaFilesAdapter.remove(id),
- this.mediaMetadataAdapter.remove(id),
- ]);
- }
-
- // Utility methods
- async clearAllData(): Promise<void> {
- 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 };
+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, + TimelineData, +} from "./types"; +import { TimelineTrack } from "@/types/timeline"; + +class StorageService { + private projectsAdapter: IndexedDBAdapter<SerializedProject>; + private config: StorageConfig; + + constructor() { + this.config = { + projectsDb: "video-editor-projects", + mediaDb: "video-editor-media", + timelineDb: "video-editor-timelines", + version: 1, + }; + + this.projectsAdapter = new IndexedDBAdapter<SerializedProject>( + this.config.projectsDb, + "projects", + this.config.version + ); + } + + // Helper to get project-specific media adapters + private getProjectMediaAdapters(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: string) { + return new IndexedDBAdapter<TimelineData>( + `${this.config.timelineDb}-${projectId}`, + "timeline", + this.config.version + ); + } + + // Project operations + async saveProject(project: TProject): Promise<void> { + // 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<TProject | null> { + 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<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: string): Promise<void> { + await this.projectsAdapter.remove(id); + } + + // Media operations - now project-specific + async saveMediaItem(projectId: string, mediaItem: MediaItem): 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, + }; + + await mediaMetadataAdapter.set(mediaItem.id, metadata); + } + + async loadMediaItem( + projectId: string, + id: string + ): Promise<MediaItem | null> { + 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<MediaItem[]> { + 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<void> { + const { mediaMetadataAdapter, mediaFilesAdapter } = + this.getProjectMediaAdapters(projectId); + + await Promise.all([ + mediaFilesAdapter.remove(id), + mediaMetadataAdapter.remove(id), + ]); + } + + async deleteProjectMedia(projectId: string): Promise<void> { + 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<void> { + const timelineAdapter = this.getProjectTimelineAdapter(projectId); + const timelineData: TimelineData = { + tracks, + lastModified: new Date().toISOString(), + }; + await timelineAdapter.set("timeline", timelineData); + } + + async loadTimeline(projectId: string): Promise<TimelineTrack[] | null> { + const timelineAdapter = this.getProjectTimelineAdapter(projectId); + const timelineData = await timelineAdapter.get("timeline"); + return timelineData ? timelineData.tracks : null; + } + + async deleteProjectTimeline(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: 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 +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 index 7ad18e19..b662c164 100644 --- a/apps/web/src/lib/storage/types.ts +++ b/apps/web/src/lib/storage/types.ts @@ -1,4 +1,5 @@ import { TProject } from "@/types/project"; +import { TimelineTrack } from "@/types/timeline"; export interface StorageAdapter<T> { get(key: string): Promise<T | null>; @@ -20,9 +21,15 @@ export interface MediaFileData { // File will be stored separately in OPFS } +export interface TimelineData { + tracks: TimelineTrack[]; + lastModified: string; +} + export interface StorageConfig { projectsDb: string; mediaDb: string; + timelineDb: string; version: number; } |
