diff options
| author | cashmere <cashmere@autistici.org> | 2026-07-27 01:21:20 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-07-27 01:21:20 +0200 |
| commit | d55d2583a71ae35fa8d0d1bfbec975e67e66aafa (patch) | |
| tree | c7a612f24b12c8602d7fbf139ac2709e4f2f4d9c /apps/web/src/services/storage/service.ts | |
| parent | 2a372fdec256b8be890e181c221c2a012ead9efa (diff) | |
| download | opencut-d55d2583a71ae35fa8d0d1bfbec975e67e66aafa.tar.gz | |
feat: git-like project versioning (v1)
Linear per-project history per notes/project-versioning.md: named
checkpoints (thumbnail via renderer.captureFrame) plus auto-checkpoints
on save (60s throttle, ring of 20, named never rotate). Restore writes a
safety checkpoint first, then swaps the document outside undo/redo.
Media assets referenced by any version are pinned against deletion.
Migration v31-v32 stamps the schema; the version store lives in its own
IDB database with an in-memory fallback for tests. Versions popover in
the editor header for jump/rename/delete. 12 new tests.
Diffstat (limited to 'apps/web/src/services/storage/service.ts')
| -rw-r--r-- | apps/web/src/services/storage/service.ts | 158 |
1 files changed, 87 insertions, 71 deletions
diff --git a/apps/web/src/services/storage/service.ts b/apps/web/src/services/storage/service.ts index e8fc26c0..7b01f9e5 100644 --- a/apps/web/src/services/storage/service.ts +++ b/apps/web/src/services/storage/service.ts @@ -51,6 +51,91 @@ function normalizeBookmarks({ raw }: { raw: unknown }): Bookmark[] { .filter((b): b is Bookmark => b !== null); } +function stripAudioBuffers({ tracks }: { tracks: SceneTracks }): SceneTracks { + return { + ...tracks, + audio: tracks.audio.map((track) => ({ + ...track, + elements: track.elements.map((element) => { + const { buffer: _buffer, ...rest } = element; + return rest; + }), + })), + }; +} + +export function serializeProject({ + project, +}: { + project: TProject; +}): SerializedProject { + const duration = + project.metadata.duration ?? + getProjectDurationFromScenes({ scenes: project.scenes }); + const serializedScenes: SerializedScene[] = project.scenes.map((scene) => ({ + id: scene.id, + name: scene.name, + isMain: scene.isMain, + tracks: stripAudioBuffers({ tracks: scene.tracks }), + bookmarks: scene.bookmarks, + createdAt: scene.createdAt.toISOString(), + updatedAt: scene.updatedAt.toISOString(), + })); + + return { + metadata: { + id: project.metadata.id, + name: project.metadata.name, + thumbnail: project.metadata.thumbnail, + duration, + createdAt: project.metadata.createdAt.toISOString(), + updatedAt: project.metadata.updatedAt.toISOString(), + }, + scenes: serializedScenes, + currentSceneId: project.currentSceneId, + settings: project.settings, + version: project.version, + timelineViewState: project.timelineViewState, + }; +} + +export function deserializeProject({ + serialized, +}: { + serialized: SerializedProject; +}): TProject { + const scenes = + serialized.scenes?.map((scene) => ({ + id: scene.id, + name: scene.name, + isMain: scene.isMain, + tracks: scene.tracks, + bookmarks: normalizeBookmarks({ raw: scene.bookmarks }), + createdAt: new Date(scene.createdAt), + updatedAt: new Date(scene.updatedAt), + })) ?? []; + + return { + metadata: { + id: serialized.metadata.id, + name: serialized.metadata.name, + thumbnail: serialized.metadata.thumbnail, + duration: roundMediaTime({ + time: + serialized.metadata.duration ?? + getProjectDurationFromScenes({ scenes }), + }), + createdAt: new Date(serialized.metadata.createdAt), + updatedAt: new Date(serialized.metadata.updatedAt), + }, + scenes, + currentSceneId: serialized.currentSceneId || "", + settings: serialized.settings, + version: serialized.version, + timelineViewState: serialized.timelineViewState, + }; +} + class StorageService { private projectsAdapter: IndexedDBAdapter<SerializedProject>; private savedSoundsAdapter: IndexedDBAdapter<SavedSoundsData>; @@ -118,48 +203,8 @@ class StorageService { return isStorageQuotaExceededError({ error }); } - private stripAudioBuffers({ tracks }: { tracks: SceneTracks }): SceneTracks { - return { - ...tracks, - audio: tracks.audio.map((track) => ({ - ...track, - elements: track.elements.map((element) => { - const { buffer: _buffer, ...rest } = element; - return rest; - }), - })), - }; - } - async saveProject({ project }: { project: TProject }): Promise<void> { - const duration = - project.metadata.duration ?? - getProjectDurationFromScenes({ scenes: project.scenes }); - const serializedScenes: SerializedScene[] = project.scenes.map((scene) => ({ - id: scene.id, - name: scene.name, - isMain: scene.isMain, - tracks: this.stripAudioBuffers({ tracks: scene.tracks }), - bookmarks: scene.bookmarks, - createdAt: scene.createdAt.toISOString(), - updatedAt: scene.updatedAt.toISOString(), - })); - - const serializedProject: SerializedProject = { - metadata: { - id: project.metadata.id, - name: project.metadata.name, - thumbnail: project.metadata.thumbnail, - duration, - createdAt: project.metadata.createdAt.toISOString(), - updatedAt: project.metadata.updatedAt.toISOString(), - }, - scenes: serializedScenes, - currentSceneId: project.currentSceneId, - settings: project.settings, - version: project.version, - timelineViewState: project.timelineViewState, - }; + const serializedProject = serializeProject({ project }); await this.projectsAdapter.set({ key: project.metadata.id, @@ -190,36 +235,7 @@ class StorageService { return null; } - const scenes = - serializedProject.scenes?.map((scene) => ({ - id: scene.id, - name: scene.name, - isMain: scene.isMain, - tracks: scene.tracks, - bookmarks: normalizeBookmarks({ raw: scene.bookmarks }), - createdAt: new Date(scene.createdAt), - updatedAt: new Date(scene.updatedAt), - })) ?? []; - - const project: TProject = { - metadata: { - id: serializedProject.metadata.id, - name: serializedProject.metadata.name, - thumbnail: serializedProject.metadata.thumbnail, - duration: roundMediaTime({ - time: - serializedProject.metadata.duration ?? - getProjectDurationFromScenes({ scenes }), - }), - createdAt: new Date(serializedProject.metadata.createdAt), - updatedAt: new Date(serializedProject.metadata.updatedAt), - }, - scenes, - currentSceneId: serializedProject.currentSceneId || "", - settings: serializedProject.settings, - version: serializedProject.version, - timelineViewState: serializedProject.timelineViewState, - }; + const project = deserializeProject({ serialized: serializedProject }); return { project }; } |
