summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/storage/storage-service.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-07-07 19:06:36 +0200
committerMaze Winther <mazewinther@gmail.com>2025-07-07 19:06:36 +0200
commitbd0c7f22069d1a2e4b2a2d962becaf6b9e942414 (patch)
tree595eb9b49f655f7c30b008065c26612077474a82 /apps/web/src/lib/storage/storage-service.ts
parent11c0b89bd162fe0cb0633687fe5ad07977956ace (diff)
downloadopencut-bd0c7f22069d1a2e4b2a2d962becaf6b9e942414.tar.gz
refactor: store media relative to project, add storage for timeline data, and other things
Diffstat (limited to 'apps/web/src/lib/storage/storage-service.ts')
-rw-r--r--apps/web/src/lib/storage/storage-service.ts467
1 files changed, 273 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 };