diff options
| author | Maze Winther <mazewinther@gmail.com> | 2025-09-01 16:17:06 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2025-09-01 16:17:06 +0200 |
| commit | 350b55081cde3a4332c982ea5c53b7d359029f6b (patch) | |
| tree | f63b66635d2a13a0a43c622a9bea25d9b9822960 /apps/web/src/lib | |
| parent | 2b700b7474ffc728f5318c178244e035c93716b5 (diff) | |
| download | opencut-350b55081cde3a4332c982ea5c53b7d359029f6b.tar.gz | |
refactor(core): switch to scene-based architecture
Diffstat (limited to 'apps/web/src/lib')
| -rw-r--r-- | apps/web/src/lib/fetch-github-stars.ts | 29 | ||||
| -rw-r--r-- | apps/web/src/lib/storage/storage-service.ts | 159 | ||||
| -rw-r--r-- | apps/web/src/lib/storage/types.ts | 20 |
3 files changed, 140 insertions, 68 deletions
diff --git a/apps/web/src/lib/fetch-github-stars.ts b/apps/web/src/lib/fetch-github-stars.ts deleted file mode 100644 index 0372f25b..00000000 --- a/apps/web/src/lib/fetch-github-stars.ts +++ /dev/null @@ -1,29 +0,0 @@ -export async function getStars(): Promise<string> { - try { - const res = await fetch( - "https://api.github.com/repos/OpenCut-app/OpenCut", - { - next: { revalidate: 3600 }, - } - ); - - if (!res.ok) { - throw new Error(`GitHub API error: ${res.status} ${res.statusText}`); - } - const data = (await res.json()) as { stargazers_count: number }; - const count = data.stargazers_count; - - if (typeof count !== "number") { - throw new Error("Invalid stargazers_count from GitHub API"); - } - - if (count >= 1_000_000) - return (count / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M"; - if (count >= 1000) - return (count / 1000).toFixed(1).replace(/\.0$/, "") + "k"; - return count.toString(); - } catch (error) { - console.error("Failed to fetch GitHub stars:", error); - return "1.5k"; - } -} diff --git a/apps/web/src/lib/storage/storage-service.ts b/apps/web/src/lib/storage/storage-service.ts index 3aadf883..80f10d3d 100644 --- a/apps/web/src/lib/storage/storage-service.ts +++ b/apps/web/src/lib/storage/storage-service.ts @@ -6,6 +6,7 @@ import { MediaFileData, StorageConfig, SerializedProject, + SerializedScene, TimelineData, } from "./types"; import { TimelineTrack } from "@/types/timeline"; @@ -39,7 +40,7 @@ class StorageService { } // Helper to get project-specific media adapters - private getProjectMediaAdapters(projectId: string) { + private getProjectMediaAdapters({ projectId }: { projectId: string }) { const mediaMetadataAdapter = new IndexedDBAdapter<MediaFileData>( `${this.config.mediaDb}-${projectId}`, "media-metadata", @@ -52,23 +53,43 @@ class StorageService { } // Helper to get project-specific timeline adapter - private getProjectTimelineAdapter(projectId: string) { + private getProjectTimelineAdapter({ + projectId, + sceneId, + }: { + projectId: string; + sceneId?: string; + }) { + const dbName = sceneId + ? `${this.config.timelineDb}-${projectId}-${sceneId}` + : `${this.config.timelineDb}-${projectId}`; + return new IndexedDBAdapter<TimelineData>( - `${this.config.timelineDb}-${projectId}`, + dbName, "timeline", this.config.version ); } // Project operations - async saveProject(project: TProject): Promise<void> { + 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, @@ -81,11 +102,21 @@ class StorageService { await this.projectsAdapter.set(project.id, serializedProject); } - async loadProject(id: string): Promise<TProject | null> { + 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 return { id: serializedProject.id, @@ -93,6 +124,8 @@ class StorageService { 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, @@ -108,7 +141,7 @@ class StorageService { const projects: TProject[] = []; for (const id of projectIds) { - const project = await this.loadProject(id); + const project = await this.loadProject({ id }); if (project) { projects.push(project); } @@ -120,14 +153,20 @@ class StorageService { ); } - async deleteProject(id: string): Promise<void> { + async deleteProject({ id }: { id: string }): Promise<void> { await this.projectsAdapter.remove(id); } // Media operations - async saveMediaFile(projectId: string, mediaItem: MediaFile): Promise<void> { + async saveMediaFile({ + projectId, + mediaItem, + }: { + projectId: string; + mediaItem: MediaFile; + }): Promise<void> { const { mediaMetadataAdapter, mediaFilesAdapter } = - this.getProjectMediaAdapters(projectId); + this.getProjectMediaAdapters({ projectId }); // Save file to project-specific OPFS await mediaFilesAdapter.set(mediaItem.id, mediaItem.file); @@ -148,12 +187,15 @@ class StorageService { await mediaMetadataAdapter.set(mediaItem.id, metadata); } - async loadMediaFile( - projectId: string, - id: string - ): Promise<MediaFile | null> { + async loadMediaFile({ + projectId, + id, + }: { + projectId: string; + id: string; + }): Promise<MediaFile | null> { const { mediaMetadataAdapter, mediaFilesAdapter } = - this.getProjectMediaAdapters(projectId); + this.getProjectMediaAdapters({ projectId }); const [file, metadata] = await Promise.all([ mediaFilesAdapter.get(id), @@ -192,14 +234,20 @@ class StorageService { }; } - async loadAllMediaFiles(projectId: string): Promise<MediaFile[]> { - const { mediaMetadataAdapter } = this.getProjectMediaAdapters(projectId); + 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); + const item = await this.loadMediaFile({ projectId, id }); if (item) { mediaItems.push(item); } @@ -208,9 +256,15 @@ class StorageService { return mediaItems; } - async deleteMediaFile(projectId: string, id: string): Promise<void> { + async deleteMediaFile({ + projectId, + id, + }: { + projectId: string; + id: string; + }): Promise<void> { const { mediaMetadataAdapter, mediaFilesAdapter } = - this.getProjectMediaAdapters(projectId); + this.getProjectMediaAdapters({ projectId }); await Promise.all([ mediaFilesAdapter.remove(id), @@ -218,9 +272,13 @@ class StorageService { ]); } - async deleteProjectMedia(projectId: string): Promise<void> { + async deleteProjectMedia({ + projectId, + }: { + projectId: string; + }): Promise<void> { const { mediaMetadataAdapter, mediaFilesAdapter } = - this.getProjectMediaAdapters(projectId); + this.getProjectMediaAdapters({ projectId }); await Promise.all([ mediaMetadataAdapter.clear(), @@ -228,12 +286,20 @@ class StorageService { ]); } - // Timeline operations - now project-specific - async saveTimeline( - projectId: string, - tracks: TimelineTrack[] - ): Promise<void> { - const timelineAdapter = this.getProjectTimelineAdapter(projectId); + // 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(), @@ -241,14 +307,27 @@ class StorageService { await timelineAdapter.set("timeline", timelineData); } - async loadTimeline(projectId: string): Promise<TimelineTrack[] | null> { - const timelineAdapter = this.getProjectTimelineAdapter(projectId); + 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: string): Promise<void> { - const timelineAdapter = this.getProjectTimelineAdapter(projectId); + async deleteProjectTimeline({ + projectId, + }: { + projectId: string; + }): Promise<void> { + const timelineAdapter = this.getProjectTimelineAdapter({ projectId }); await timelineAdapter.remove("timeline"); } @@ -274,12 +353,14 @@ class StorageService { }; } - async getProjectStorageInfo(projectId: string): Promise<{ + async getProjectStorageInfo({ projectId }: { projectId: string }): Promise<{ mediaItems: number; hasTimeline: boolean; }> { - const { mediaMetadataAdapter } = this.getProjectMediaAdapters(projectId); - const timelineAdapter = this.getProjectTimelineAdapter(projectId); + const { mediaMetadataAdapter } = this.getProjectMediaAdapters({ + projectId, + }); + const timelineAdapter = this.getProjectTimelineAdapter({ projectId }); const [mediaIds, timelineData] = await Promise.all([ mediaMetadataAdapter.list(), @@ -307,7 +388,11 @@ class StorageService { } } - async saveSoundEffect(soundEffect: SoundEffect): Promise<void> { + async saveSoundEffect({ + soundEffect, + }: { + soundEffect: SoundEffect; + }): Promise<void> { try { const currentData = await this.loadSavedSounds(); @@ -340,7 +425,7 @@ class StorageService { } } - async removeSavedSound(soundId: number): Promise<void> { + async removeSavedSound({ soundId }: { soundId: number }): Promise<void> { try { const currentData = await this.loadSavedSounds(); @@ -356,7 +441,7 @@ class StorageService { } } - async isSoundSaved(soundId: number): Promise<boolean> { + async isSoundSaved({ soundId }: { soundId: number }): Promise<boolean> { try { const currentData = await this.loadSavedSounds(); return currentData.sounds.some((sound) => sound.id === soundId); diff --git a/apps/web/src/lib/storage/types.ts b/apps/web/src/lib/storage/types.ts index 8f593867..e66239cd 100644 --- a/apps/web/src/lib/storage/types.ts +++ b/apps/web/src/lib/storage/types.ts @@ -1,4 +1,4 @@ -import { TProject } from "@/types/project"; +import { TProject, Scene } from "@/types/project"; import { TimelineTrack } from "@/types/timeline"; export interface StorageAdapter<T> { @@ -23,11 +23,23 @@ export interface MediaFileData { // 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; @@ -37,9 +49,13 @@ export interface StorageConfig { } // Helper type for serialization - converts Date objects to strings -export type SerializedProject = Omit<TProject, "createdAt" | "updatedAt"> & { +export type SerializedProject = Omit< + TProject, + "createdAt" | "updatedAt" | "scenes" +> & { createdAt: string; updatedAt: string; + scenes: SerializedScene[]; bookmarks?: number[]; }; |
