From 4d127a038bf768eded68aae3cfa5c1b12241bdac Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Fri, 27 Mar 2026 00:55:15 +0100 Subject: chore: normalize line endings --- .../storage/migrations/__tests__/fixtures/index.ts | 20 +- .../storage/migrations/__tests__/fixtures/v5.ts | 108 +++--- .../migrations/__tests__/v15-to-v16.test.ts | 186 +++++----- .../migrations/__tests__/v16-to-v17.test.ts | 264 +++++++------- .../migrations/__tests__/v18-to-v19.test.ts | 126 +++---- .../storage/migrations/__tests__/v3-to-v4.test.ts | 280 +++++++-------- .../storage/migrations/__tests__/v4-to-v5.test.ts | 274 +++++++------- .../storage/migrations/__tests__/v5-to-v6.test.ts | 184 +++++----- .../storage/migrations/__tests__/v8-to-v9.test.ts | 350 +++++++++--------- apps/web/src/services/storage/migrations/base.ts | 18 +- apps/web/src/services/storage/migrations/index.ts | 92 ++--- apps/web/src/services/storage/migrations/runner.ts | 310 ++++++++-------- .../storage/migrations/transformers/v10-to-v11.ts | 226 ++++++------ .../storage/migrations/transformers/v11-to-v12.ts | 392 ++++++++++----------- .../storage/migrations/transformers/v12-to-v13.ts | 204 +++++------ .../storage/migrations/transformers/v13-to-v14.ts | 212 +++++------ .../storage/migrations/transformers/v14-to-v15.ts | 134 +++---- .../storage/migrations/transformers/v15-to-v16.ts | 120 +++---- .../storage/migrations/transformers/v16-to-v17.ts | 214 +++++------ .../storage/migrations/transformers/v17-to-v18.ts | 210 +++++------ .../storage/migrations/transformers/v18-to-v19.ts | 92 ++--- .../storage/migrations/transformers/v3-to-v4.ts | 378 ++++++++++---------- .../storage/migrations/transformers/v4-to-v5.ts | 354 +++++++++---------- .../storage/migrations/transformers/v5-to-v6.ts | 176 ++++----- .../storage/migrations/transformers/v6-to-v7.ts | 212 +++++------ .../storage/migrations/transformers/v7-to-v8.ts | 192 +++++----- .../storage/migrations/transformers/v8-to-v9.ts | 198 +++++------ .../storage/migrations/transformers/v9-to-v10.ts | 42 +-- .../src/services/storage/migrations/v10-to-v11.ts | 32 +- .../src/services/storage/migrations/v11-to-v12.ts | 32 +- .../src/services/storage/migrations/v12-to-v13.ts | 32 +- .../src/services/storage/migrations/v13-to-v14.ts | 32 +- .../src/services/storage/migrations/v14-to-v15.ts | 32 +- .../src/services/storage/migrations/v15-to-v16.ts | 32 +- .../src/services/storage/migrations/v16-to-v17.ts | 32 +- .../src/services/storage/migrations/v17-to-v18.ts | 32 +- .../src/services/storage/migrations/v18-to-v19.ts | 32 +- .../src/services/storage/migrations/v3-to-v4.ts | 32 +- .../src/services/storage/migrations/v4-to-v5.ts | 32 +- .../src/services/storage/migrations/v5-to-v6.ts | 32 +- .../src/services/storage/migrations/v6-to-v7.ts | 32 +- .../src/services/storage/migrations/v7-to-v8.ts | 32 +- .../src/services/storage/migrations/v8-to-v9.ts | 32 +- .../src/services/storage/migrations/v9-to-v10.ts | 32 +- apps/web/src/services/storage/quota.ts | 290 +++++++-------- 45 files changed, 3185 insertions(+), 3185 deletions(-) (limited to 'apps/web/src/services/storage') diff --git a/apps/web/src/services/storage/migrations/__tests__/fixtures/index.ts b/apps/web/src/services/storage/migrations/__tests__/fixtures/index.ts index 5f82e14a..a97e42d7 100644 --- a/apps/web/src/services/storage/migrations/__tests__/fixtures/index.ts +++ b/apps/web/src/services/storage/migrations/__tests__/fixtures/index.ts @@ -1,10 +1,10 @@ -/** - * Test fixtures for storage migrations. - * These represent real project data shapes from each version. - */ - -export * from "./v0"; -export * from "./v1"; -export * from "./v2"; -export * from "./v3"; -export * from "./v5"; +/** + * Test fixtures for storage migrations. + * These represent real project data shapes from each version. + */ + +export * from "./v0"; +export * from "./v1"; +export * from "./v2"; +export * from "./v3"; +export * from "./v5"; diff --git a/apps/web/src/services/storage/migrations/__tests__/fixtures/v5.ts b/apps/web/src/services/storage/migrations/__tests__/fixtures/v5.ts index 5e181c76..89bb83c3 100644 --- a/apps/web/src/services/storage/migrations/__tests__/fixtures/v5.ts +++ b/apps/web/src/services/storage/migrations/__tests__/fixtures/v5.ts @@ -1,54 +1,54 @@ -export const v5Project = { - id: "project-v5-456", - version: 5, - metadata: { - id: "project-v5-456", - name: "My V5 Project", - thumbnail: "data:image/png;base64,abc123", - duration: 30, - createdAt: "2024-06-01T10:00:00.000Z", - updatedAt: "2024-06-01T14:00:00.000Z", - }, - settings: { - fps: 30, - canvasSize: { width: 1920, height: 1080 }, - background: { type: "color", color: "#000000" }, - }, - currentSceneId: "scene-main", - scenes: [ - { - id: "scene-main", - name: "Main scene", - isMain: true, - tracks: [ - { - id: "track-1", - type: "video", - name: "Video Track", - isMain: true, - elements: [], - }, - ], - bookmarks: [2.0, 5.5, 12.0], - createdAt: "2024-06-01T10:00:00.000Z", - updatedAt: "2024-06-01T14:00:00.000Z", - }, - { - id: "scene-intro", - name: "Intro", - isMain: false, - tracks: [ - { - id: "track-2", - type: "video", - name: "Video Track", - isMain: true, - elements: [], - }, - ], - bookmarks: [], - createdAt: "2024-06-01T10:00:00.000Z", - updatedAt: "2024-06-01T14:00:00.000Z", - }, - ], -}; +export const v5Project = { + id: "project-v5-456", + version: 5, + metadata: { + id: "project-v5-456", + name: "My V5 Project", + thumbnail: "data:image/png;base64,abc123", + duration: 30, + createdAt: "2024-06-01T10:00:00.000Z", + updatedAt: "2024-06-01T14:00:00.000Z", + }, + settings: { + fps: 30, + canvasSize: { width: 1920, height: 1080 }, + background: { type: "color", color: "#000000" }, + }, + currentSceneId: "scene-main", + scenes: [ + { + id: "scene-main", + name: "Main scene", + isMain: true, + tracks: [ + { + id: "track-1", + type: "video", + name: "Video Track", + isMain: true, + elements: [], + }, + ], + bookmarks: [2.0, 5.5, 12.0], + createdAt: "2024-06-01T10:00:00.000Z", + updatedAt: "2024-06-01T14:00:00.000Z", + }, + { + id: "scene-intro", + name: "Intro", + isMain: false, + tracks: [ + { + id: "track-2", + type: "video", + name: "Video Track", + isMain: true, + elements: [], + }, + ], + bookmarks: [], + createdAt: "2024-06-01T10:00:00.000Z", + updatedAt: "2024-06-01T14:00:00.000Z", + }, + ], +}; diff --git a/apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts b/apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts index f2a4908f..122af2cb 100644 --- a/apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts +++ b/apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts @@ -1,93 +1,93 @@ -import { describe, expect, test } from "bun:test"; -import { transformProjectV15ToV16 } from "../transformers/v15-to-v16"; - -describe("V15 to V16 Migration", () => { - test("renames sticker tracks to graphic tracks", () => { - const result = transformProjectV15ToV16({ - project: { - id: "project-v15", - version: 15, - metadata: { - id: "project-v15", - name: "Project", - createdAt: "2024-01-01T00:00:00.000Z", - updatedAt: "2024-01-01T00:00:00.000Z", - }, - settings: { - fps: 30, - canvasSize: { width: 1920, height: 1080 }, - background: { type: "color", color: "#000000" }, - }, - currentSceneId: "scene-main", - scenes: [ - { - id: "scene-main", - name: "Main scene", - isMain: true, - tracks: [ - { - id: "track-graphic", - type: "sticker", - name: "Sticker Track", - hidden: false, - elements: [ - { - id: "sticker-1", - type: "sticker", - name: "Logo", - stickerId: "icons:mdi:home", - duration: 5, - startTime: 0, - trimStart: 0, - trimEnd: 0, - transform: { - scaleX: 1, - scaleY: 1, - position: { x: 0, y: 0 }, - rotate: 0, - }, - opacity: 1, - }, - ], - }, - ], - bookmarks: [], - createdAt: "2024-01-01T00:00:00.000Z", - updatedAt: "2024-01-01T00:00:00.000Z", - }, - ], - }, - }); - - expect(result.skipped).toBe(false); - expect(result.project.version).toBe(16); - expect( - (result.project.scenes as Array<{ tracks: Array<{ type: string }> }>)[0] - .tracks[0].type, - ).toBe("graphic"); - }); - - test("skips projects already on v16", () => { - const result = transformProjectV15ToV16({ - project: { - id: "project-v16", - version: 16, - }, - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("already v16"); - }); - - test("skips projects with no id", () => { - const result = transformProjectV15ToV16({ - project: { - version: 15, - scenes: [], - }, - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("no project id"); - }); -}); +import { describe, expect, test } from "bun:test"; +import { transformProjectV15ToV16 } from "../transformers/v15-to-v16"; + +describe("V15 to V16 Migration", () => { + test("renames sticker tracks to graphic tracks", () => { + const result = transformProjectV15ToV16({ + project: { + id: "project-v15", + version: 15, + metadata: { + id: "project-v15", + name: "Project", + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, + settings: { + fps: 30, + canvasSize: { width: 1920, height: 1080 }, + background: { type: "color", color: "#000000" }, + }, + currentSceneId: "scene-main", + scenes: [ + { + id: "scene-main", + name: "Main scene", + isMain: true, + tracks: [ + { + id: "track-graphic", + type: "sticker", + name: "Sticker Track", + hidden: false, + elements: [ + { + id: "sticker-1", + type: "sticker", + name: "Logo", + stickerId: "icons:mdi:home", + duration: 5, + startTime: 0, + trimStart: 0, + trimEnd: 0, + transform: { + scaleX: 1, + scaleY: 1, + position: { x: 0, y: 0 }, + rotate: 0, + }, + opacity: 1, + }, + ], + }, + ], + bookmarks: [], + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, + ], + }, + }); + + expect(result.skipped).toBe(false); + expect(result.project.version).toBe(16); + expect( + (result.project.scenes as Array<{ tracks: Array<{ type: string }> }>)[0] + .tracks[0].type, + ).toBe("graphic"); + }); + + test("skips projects already on v16", () => { + const result = transformProjectV15ToV16({ + project: { + id: "project-v16", + version: 16, + }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("already v16"); + }); + + test("skips projects with no id", () => { + const result = transformProjectV15ToV16({ + project: { + version: 15, + scenes: [], + }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("no project id"); + }); +}); diff --git a/apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts b/apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts index e246943e..352ca762 100644 --- a/apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts +++ b/apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts @@ -1,132 +1,132 @@ -import { describe, expect, test } from "bun:test"; -import { transformProjectV16ToV17 } from "../transformers/v16-to-v17"; - -describe("V16 to V17 Migration", () => { - test("adds center stroke alignment to masks that do not have it", () => { - const result = transformProjectV16ToV17({ - project: { - id: "project-v16", - version: 16, - metadata: { - id: "project-v16", - name: "Project", - createdAt: "2024-01-01T00:00:00.000Z", - updatedAt: "2024-01-01T00:00:00.000Z", - }, - settings: { - fps: 30, - canvasSize: { width: 1920, height: 1080 }, - background: { type: "color", color: "#000000" }, - }, - currentSceneId: "scene-main", - scenes: [ - { - id: "scene-main", - name: "Main scene", - isMain: true, - tracks: [ - { - id: "track-video", - type: "video", - name: "Video Track", - hidden: false, - elements: [ - { - id: "video-1", - type: "video", - name: "Clip", - mediaId: "media-1", - duration: 5, - startTime: 0, - trimStart: 0, - trimEnd: 0, - transform: { - scaleX: 1, - scaleY: 1, - position: { x: 0, y: 0 }, - rotate: 0, - }, - opacity: 1, - masks: [ - { - id: "mask-1", - type: "rectangle", - params: { - feather: 0, - inverted: false, - strokeColor: "#ffffff", - strokeWidth: 8, - centerX: 0, - centerY: 0, - width: 0.6, - height: 0.6, - rotation: 0, - scale: 1, - }, - }, - { - id: "mask-2", - type: "ellipse", - params: { - feather: 0, - inverted: false, - strokeColor: "#ffffff", - strokeWidth: 8, - strokeAlign: "outside", - centerX: 0, - centerY: 0, - width: 0.6, - height: 0.6, - rotation: 0, - scale: 1, - }, - }, - ], - }, - ], - }, - ], - bookmarks: [], - createdAt: "2024-01-01T00:00:00.000Z", - updatedAt: "2024-01-01T00:00:00.000Z", - }, - ], - }, - }); - - expect(result.skipped).toBe(false); - expect(result.project.version).toBe(17); - - const migratedMasks = ( - ((result.project.scenes as Array<{ tracks: Array<{ elements: Array<{ masks: Array<{ params: Record }> }> }> }>)[0] - .tracks[0].elements[0].masks) - ); - - expect(migratedMasks[0].params.strokeAlign).toBe("center"); - expect(migratedMasks[1].params.strokeAlign).toBe("outside"); - }); - - test("skips projects already on v17", () => { - const result = transformProjectV16ToV17({ - project: { - id: "project-v17", - version: 17, - }, - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("already v17"); - }); - - test("skips projects with no id", () => { - const result = transformProjectV16ToV17({ - project: { - version: 16, - scenes: [], - }, - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("no project id"); - }); -}); +import { describe, expect, test } from "bun:test"; +import { transformProjectV16ToV17 } from "../transformers/v16-to-v17"; + +describe("V16 to V17 Migration", () => { + test("adds center stroke alignment to masks that do not have it", () => { + const result = transformProjectV16ToV17({ + project: { + id: "project-v16", + version: 16, + metadata: { + id: "project-v16", + name: "Project", + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, + settings: { + fps: 30, + canvasSize: { width: 1920, height: 1080 }, + background: { type: "color", color: "#000000" }, + }, + currentSceneId: "scene-main", + scenes: [ + { + id: "scene-main", + name: "Main scene", + isMain: true, + tracks: [ + { + id: "track-video", + type: "video", + name: "Video Track", + hidden: false, + elements: [ + { + id: "video-1", + type: "video", + name: "Clip", + mediaId: "media-1", + duration: 5, + startTime: 0, + trimStart: 0, + trimEnd: 0, + transform: { + scaleX: 1, + scaleY: 1, + position: { x: 0, y: 0 }, + rotate: 0, + }, + opacity: 1, + masks: [ + { + id: "mask-1", + type: "rectangle", + params: { + feather: 0, + inverted: false, + strokeColor: "#ffffff", + strokeWidth: 8, + centerX: 0, + centerY: 0, + width: 0.6, + height: 0.6, + rotation: 0, + scale: 1, + }, + }, + { + id: "mask-2", + type: "ellipse", + params: { + feather: 0, + inverted: false, + strokeColor: "#ffffff", + strokeWidth: 8, + strokeAlign: "outside", + centerX: 0, + centerY: 0, + width: 0.6, + height: 0.6, + rotation: 0, + scale: 1, + }, + }, + ], + }, + ], + }, + ], + bookmarks: [], + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, + ], + }, + }); + + expect(result.skipped).toBe(false); + expect(result.project.version).toBe(17); + + const migratedMasks = ( + ((result.project.scenes as Array<{ tracks: Array<{ elements: Array<{ masks: Array<{ params: Record }> }> }> }>)[0] + .tracks[0].elements[0].masks) + ); + + expect(migratedMasks[0].params.strokeAlign).toBe("center"); + expect(migratedMasks[1].params.strokeAlign).toBe("outside"); + }); + + test("skips projects already on v17", () => { + const result = transformProjectV16ToV17({ + project: { + id: "project-v17", + version: 17, + }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("already v17"); + }); + + test("skips projects with no id", () => { + const result = transformProjectV16ToV17({ + project: { + version: 16, + scenes: [], + }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("no project id"); + }); +}); diff --git a/apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts b/apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts index 7524a8d4..0f679b6d 100644 --- a/apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts +++ b/apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts @@ -1,63 +1,63 @@ -import { describe, expect, test } from "bun:test"; -import { transformProjectV18ToV19 } from "../transformers/v18-to-v19"; - -describe("V18 to V19 Migration", () => { - test("adds canvas size mode and empty remembered custom size defaults", () => { - const result = transformProjectV18ToV19({ - project: { - id: "project-v18-defaults", - version: 18, - metadata: { - id: "project-v18-defaults", - name: "Project", - createdAt: "2024-01-01T00:00:00.000Z", - updatedAt: "2024-01-01T00:00:00.000Z", - }, - settings: { - fps: 30, - canvasSize: { width: 1920, height: 1080 }, - originalCanvasSize: { width: 1920, height: 1080 }, - background: { type: "color", color: "#000000" }, - }, - currentSceneId: "scene-main", - scenes: [], - }, - }); - - expect(result.skipped).toBe(false); - expect(result.project.version).toBe(19); - expect( - (result.project.settings as Record).canvasSizeMode, - ).toBe("preset"); - expect( - (result.project.settings as Record).lastCustomCanvasSize, - ).toBeNull(); - expect( - (result.project.settings as Record).originalCanvasSize, - ).toEqual({ width: 1920, height: 1080 }); - }); - - test("skips projects already on v19", () => { - const result = transformProjectV18ToV19({ - project: { - id: "project-v19", - version: 19, - }, - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("already v19"); - }); - - test("skips projects with no id", () => { - const result = transformProjectV18ToV19({ - project: { - version: 18, - scenes: [], - }, - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("no project id"); - }); -}); +import { describe, expect, test } from "bun:test"; +import { transformProjectV18ToV19 } from "../transformers/v18-to-v19"; + +describe("V18 to V19 Migration", () => { + test("adds canvas size mode and empty remembered custom size defaults", () => { + const result = transformProjectV18ToV19({ + project: { + id: "project-v18-defaults", + version: 18, + metadata: { + id: "project-v18-defaults", + name: "Project", + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, + settings: { + fps: 30, + canvasSize: { width: 1920, height: 1080 }, + originalCanvasSize: { width: 1920, height: 1080 }, + background: { type: "color", color: "#000000" }, + }, + currentSceneId: "scene-main", + scenes: [], + }, + }); + + expect(result.skipped).toBe(false); + expect(result.project.version).toBe(19); + expect( + (result.project.settings as Record).canvasSizeMode, + ).toBe("preset"); + expect( + (result.project.settings as Record).lastCustomCanvasSize, + ).toBeNull(); + expect( + (result.project.settings as Record).originalCanvasSize, + ).toEqual({ width: 1920, height: 1080 }); + }); + + test("skips projects already on v19", () => { + const result = transformProjectV18ToV19({ + project: { + id: "project-v19", + version: 19, + }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("already v19"); + }); + + test("skips projects with no id", () => { + const result = transformProjectV18ToV19({ + project: { + version: 18, + scenes: [], + }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("no project id"); + }); +}); diff --git a/apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts b/apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts index 81d03147..6c2d6597 100644 --- a/apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts +++ b/apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts @@ -1,140 +1,140 @@ -import { describe, expect, test } from "bun:test"; -import { transformProjectV3ToV4 } from "../transformers/v3-to-v4"; -import { v3Project } from "./fixtures"; - -describe("V3 to V4 Migration", () => { - test("normalizes legacy text fontWeight values", () => { - const projectWithLegacyTextWeight = { - ...v3Project, - scenes: [ - { - ...v3Project.scenes[0], - tracks: [ - { - id: "track-text", - type: "text", - name: "Text Track", - hidden: false, - elements: [ - { - id: "text-1", - type: "text", - name: "Title", - content: "Hello", - duration: 5, - startTime: 0, - trimStart: 0, - trimEnd: 0, - fontSize: 64, - fontFamily: "Inter", - color: "#ffffff", - backgroundColor: "transparent", - textAlign: "center", - fontWeight: "bold", - fontStyle: "normal", - textDecoration: "none", - transform: { - scale: 1, - position: { x: 0, y: 0 }, - rotate: 0, - }, - opacity: 1, - }, - ], - }, - ], - }, - ], - }; - - const result = transformProjectV3ToV4({ - project: projectWithLegacyTextWeight, - }); - - expect(result.skipped).toBe(false); - expect(result.project.version).toBe(4); - - const migratedScene = ( - result.project.scenes as Array> - )[0]; - const migratedTrack = ( - migratedScene.tracks as Array> - )[0]; - const migratedElement = ( - migratedTrack.elements as Array> - )[0]; - - expect(migratedElement.fontWeight).toBe("700"); - }); - - test("does not mutate non-text tracks", () => { - const projectWithoutTextTrack = { - ...v3Project, - scenes: [ - { - ...v3Project.scenes[0], - tracks: [ - { - id: "track-sticker", - type: "sticker", - name: "Sticker Track", - hidden: false, - elements: [ - { - id: "sticker-1", - type: "sticker", - name: "Flag", - iconName: "mdi:home", - duration: 5, - startTime: 0, - trimStart: 0, - trimEnd: 0, - transform: { - scale: 1, - position: { x: 0, y: 0 }, - rotate: 0, - }, - opacity: 1, - }, - ], - }, - ], - }, - ], - }; - - const result = transformProjectV3ToV4({ project: projectWithoutTextTrack }); - const migratedScene = ( - result.project.scenes as Array> - )[0]; - const migratedTrack = ( - migratedScene.tracks as Array> - )[0]; - const migratedElement = ( - migratedTrack.elements as Array> - )[0]; - - expect(migratedElement.iconName).toBe("mdi:home"); - }); - - test("skips projects that are already v4", () => { - const result = transformProjectV3ToV4({ - project: { ...v3Project, version: 4 }, - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("already v4"); - }); - - test("skips projects with no id", () => { - const result = transformProjectV3ToV4({ - project: { - version: 3, - scenes: [], - }, - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("no project id"); - }); -}); +import { describe, expect, test } from "bun:test"; +import { transformProjectV3ToV4 } from "../transformers/v3-to-v4"; +import { v3Project } from "./fixtures"; + +describe("V3 to V4 Migration", () => { + test("normalizes legacy text fontWeight values", () => { + const projectWithLegacyTextWeight = { + ...v3Project, + scenes: [ + { + ...v3Project.scenes[0], + tracks: [ + { + id: "track-text", + type: "text", + name: "Text Track", + hidden: false, + elements: [ + { + id: "text-1", + type: "text", + name: "Title", + content: "Hello", + duration: 5, + startTime: 0, + trimStart: 0, + trimEnd: 0, + fontSize: 64, + fontFamily: "Inter", + color: "#ffffff", + backgroundColor: "transparent", + textAlign: "center", + fontWeight: "bold", + fontStyle: "normal", + textDecoration: "none", + transform: { + scale: 1, + position: { x: 0, y: 0 }, + rotate: 0, + }, + opacity: 1, + }, + ], + }, + ], + }, + ], + }; + + const result = transformProjectV3ToV4({ + project: projectWithLegacyTextWeight, + }); + + expect(result.skipped).toBe(false); + expect(result.project.version).toBe(4); + + const migratedScene = ( + result.project.scenes as Array> + )[0]; + const migratedTrack = ( + migratedScene.tracks as Array> + )[0]; + const migratedElement = ( + migratedTrack.elements as Array> + )[0]; + + expect(migratedElement.fontWeight).toBe("700"); + }); + + test("does not mutate non-text tracks", () => { + const projectWithoutTextTrack = { + ...v3Project, + scenes: [ + { + ...v3Project.scenes[0], + tracks: [ + { + id: "track-sticker", + type: "sticker", + name: "Sticker Track", + hidden: false, + elements: [ + { + id: "sticker-1", + type: "sticker", + name: "Flag", + iconName: "mdi:home", + duration: 5, + startTime: 0, + trimStart: 0, + trimEnd: 0, + transform: { + scale: 1, + position: { x: 0, y: 0 }, + rotate: 0, + }, + opacity: 1, + }, + ], + }, + ], + }, + ], + }; + + const result = transformProjectV3ToV4({ project: projectWithoutTextTrack }); + const migratedScene = ( + result.project.scenes as Array> + )[0]; + const migratedTrack = ( + migratedScene.tracks as Array> + )[0]; + const migratedElement = ( + migratedTrack.elements as Array> + )[0]; + + expect(migratedElement.iconName).toBe("mdi:home"); + }); + + test("skips projects that are already v4", () => { + const result = transformProjectV3ToV4({ + project: { ...v3Project, version: 4 }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("already v4"); + }); + + test("skips projects with no id", () => { + const result = transformProjectV3ToV4({ + project: { + version: 3, + scenes: [], + }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("no project id"); + }); +}); diff --git a/apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts b/apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts index ce0c3019..95386137 100644 --- a/apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts +++ b/apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts @@ -1,137 +1,137 @@ -import { describe, expect, test } from "bun:test"; -import { transformProjectV4ToV5 } from "../transformers/v4-to-v5"; -import { v3Project } from "./fixtures"; - -describe("V4 to V5 Migration", () => { - test("migrates sticker iconName to stickerId and removes legacy color", () => { - const projectWithLegacySticker = { - ...v3Project, - version: 4, - scenes: [ - { - ...v3Project.scenes[0], - tracks: [ - { - id: "track-sticker", - type: "sticker", - name: "Sticker Track", - hidden: false, - elements: [ - { - id: "sticker-1", - type: "sticker", - name: "Home", - iconName: "mdi:home", - color: "#ff0000", - duration: 5, - startTime: 0, - trimStart: 0, - trimEnd: 0, - transform: { - scale: 1, - position: { x: 0, y: 0 }, - rotate: 0, - }, - opacity: 1, - }, - ], - }, - ], - }, - ], - }; - - const result = transformProjectV4ToV5({ - project: projectWithLegacySticker, - }); - - expect(result.skipped).toBe(false); - expect(result.project.version).toBe(5); - - const migratedScene = ( - result.project.scenes as Array> - )[0]; - const migratedTrack = ( - migratedScene.tracks as Array> - )[0]; - const migratedElement = ( - migratedTrack.elements as Array> - )[0]; - - expect(migratedElement.stickerId).toBe("icons:mdi:home"); - expect("iconName" in migratedElement).toBe(false); - expect("color" in migratedElement).toBe(false); - }); - - test("keeps provider-prefixed stickerId values unchanged", () => { - const projectWithStickerId = { - ...v3Project, - version: 4, - scenes: [ - { - ...v3Project.scenes[0], - tracks: [ - { - id: "track-sticker", - type: "sticker", - name: "Sticker Track", - hidden: false, - elements: [ - { - id: "sticker-1", - type: "sticker", - name: "Flag", - stickerId: "flags:AD", - duration: 5, - startTime: 0, - trimStart: 0, - trimEnd: 0, - transform: { - scale: 1, - position: { x: 0, y: 0 }, - rotate: 0, - }, - opacity: 1, - }, - ], - }, - ], - }, - ], - }; - - const result = transformProjectV4ToV5({ project: projectWithStickerId }); - const migratedScene = ( - result.project.scenes as Array> - )[0]; - const migratedTrack = ( - migratedScene.tracks as Array> - )[0]; - const migratedElement = ( - migratedTrack.elements as Array> - )[0]; - - expect(migratedElement.stickerId).toBe("flags:AD"); - }); - - test("skips projects that are already v5", () => { - const result = transformProjectV4ToV5({ - project: { ...v3Project, version: 5 }, - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("already v5"); - }); - - test("skips projects with no id", () => { - const result = transformProjectV4ToV5({ - project: { - version: 4, - scenes: [], - }, - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("no project id"); - }); -}); +import { describe, expect, test } from "bun:test"; +import { transformProjectV4ToV5 } from "../transformers/v4-to-v5"; +import { v3Project } from "./fixtures"; + +describe("V4 to V5 Migration", () => { + test("migrates sticker iconName to stickerId and removes legacy color", () => { + const projectWithLegacySticker = { + ...v3Project, + version: 4, + scenes: [ + { + ...v3Project.scenes[0], + tracks: [ + { + id: "track-sticker", + type: "sticker", + name: "Sticker Track", + hidden: false, + elements: [ + { + id: "sticker-1", + type: "sticker", + name: "Home", + iconName: "mdi:home", + color: "#ff0000", + duration: 5, + startTime: 0, + trimStart: 0, + trimEnd: 0, + transform: { + scale: 1, + position: { x: 0, y: 0 }, + rotate: 0, + }, + opacity: 1, + }, + ], + }, + ], + }, + ], + }; + + const result = transformProjectV4ToV5({ + project: projectWithLegacySticker, + }); + + expect(result.skipped).toBe(false); + expect(result.project.version).toBe(5); + + const migratedScene = ( + result.project.scenes as Array> + )[0]; + const migratedTrack = ( + migratedScene.tracks as Array> + )[0]; + const migratedElement = ( + migratedTrack.elements as Array> + )[0]; + + expect(migratedElement.stickerId).toBe("icons:mdi:home"); + expect("iconName" in migratedElement).toBe(false); + expect("color" in migratedElement).toBe(false); + }); + + test("keeps provider-prefixed stickerId values unchanged", () => { + const projectWithStickerId = { + ...v3Project, + version: 4, + scenes: [ + { + ...v3Project.scenes[0], + tracks: [ + { + id: "track-sticker", + type: "sticker", + name: "Sticker Track", + hidden: false, + elements: [ + { + id: "sticker-1", + type: "sticker", + name: "Flag", + stickerId: "flags:AD", + duration: 5, + startTime: 0, + trimStart: 0, + trimEnd: 0, + transform: { + scale: 1, + position: { x: 0, y: 0 }, + rotate: 0, + }, + opacity: 1, + }, + ], + }, + ], + }, + ], + }; + + const result = transformProjectV4ToV5({ project: projectWithStickerId }); + const migratedScene = ( + result.project.scenes as Array> + )[0]; + const migratedTrack = ( + migratedScene.tracks as Array> + )[0]; + const migratedElement = ( + migratedTrack.elements as Array> + )[0]; + + expect(migratedElement.stickerId).toBe("flags:AD"); + }); + + test("skips projects that are already v5", () => { + const result = transformProjectV4ToV5({ + project: { ...v3Project, version: 5 }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("already v5"); + }); + + test("skips projects with no id", () => { + const result = transformProjectV4ToV5({ + project: { + version: 4, + scenes: [], + }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("no project id"); + }); +}); diff --git a/apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts b/apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts index 6bbd5634..66e87713 100644 --- a/apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts +++ b/apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts @@ -1,92 +1,92 @@ -import { describe, expect, test } from "bun:test"; -import { transformProjectV5ToV6 } from "../transformers/v5-to-v6"; -import { v5Project } from "./fixtures"; - -describe("V5 to V6 Migration", () => { - test("converts number bookmarks to Bookmark objects", async () => { - const result = transformProjectV5ToV6({ - project: v5Project as Parameters< - typeof transformProjectV5ToV6 - >[0]["project"], - }); - - expect(result.skipped).toBe(false); - expect(result.project.version).toBe(6); - - const mainScene = ( - result.project.scenes as Array<{ bookmarks: unknown[] }> - )[0]; - expect(mainScene.bookmarks).toEqual([ - { time: 2.0 }, - { time: 5.5 }, - { time: 12.0 }, - ]); - - const introScene = ( - result.project.scenes as Array<{ bookmarks: unknown[] }> - )[1]; - expect(introScene.bookmarks).toEqual([]); - }); - - test("skips projects that are already v6", () => { - const result = transformProjectV5ToV6({ - project: { - ...v5Project, - version: 6, - scenes: [ - { - ...(v5Project as { scenes: unknown[] }).scenes[0], - bookmarks: [{ time: 2 }, { time: 5 }], - }, - ], - } as Parameters[0]["project"], - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("already v6"); - }); - - test("skips projects with no id", () => { - const result = transformProjectV5ToV6({ - project: { - version: 5, - scenes: [], - } as Parameters[0]["project"], - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("no project id"); - }); - - test("preserves existing Bookmark objects with note, color, duration", () => { - const projectWithRichBookmarks = { - ...v5Project, - version: 5, - scenes: [ - { - ...(v5Project as { scenes: Array> }) - .scenes[0], - bookmarks: [ - { time: 1, note: "Intro", color: "#ef4444" }, - { time: 5.5, duration: 2 }, - ], - }, - ], - }; - - const result = transformProjectV5ToV6({ - project: projectWithRichBookmarks as Parameters< - typeof transformProjectV5ToV6 - >[0]["project"], - }); - - expect(result.skipped).toBe(false); - const mainScene = ( - result.project.scenes as Array<{ bookmarks: unknown[] }> - )[0]; - expect(mainScene.bookmarks).toEqual([ - { time: 1, note: "Intro", color: "#ef4444" }, - { time: 5.5, duration: 2 }, - ]); - }); -}); +import { describe, expect, test } from "bun:test"; +import { transformProjectV5ToV6 } from "../transformers/v5-to-v6"; +import { v5Project } from "./fixtures"; + +describe("V5 to V6 Migration", () => { + test("converts number bookmarks to Bookmark objects", async () => { + const result = transformProjectV5ToV6({ + project: v5Project as Parameters< + typeof transformProjectV5ToV6 + >[0]["project"], + }); + + expect(result.skipped).toBe(false); + expect(result.project.version).toBe(6); + + const mainScene = ( + result.project.scenes as Array<{ bookmarks: unknown[] }> + )[0]; + expect(mainScene.bookmarks).toEqual([ + { time: 2.0 }, + { time: 5.5 }, + { time: 12.0 }, + ]); + + const introScene = ( + result.project.scenes as Array<{ bookmarks: unknown[] }> + )[1]; + expect(introScene.bookmarks).toEqual([]); + }); + + test("skips projects that are already v6", () => { + const result = transformProjectV5ToV6({ + project: { + ...v5Project, + version: 6, + scenes: [ + { + ...(v5Project as { scenes: unknown[] }).scenes[0], + bookmarks: [{ time: 2 }, { time: 5 }], + }, + ], + } as Parameters[0]["project"], + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("already v6"); + }); + + test("skips projects with no id", () => { + const result = transformProjectV5ToV6({ + project: { + version: 5, + scenes: [], + } as Parameters[0]["project"], + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("no project id"); + }); + + test("preserves existing Bookmark objects with note, color, duration", () => { + const projectWithRichBookmarks = { + ...v5Project, + version: 5, + scenes: [ + { + ...(v5Project as { scenes: Array> }) + .scenes[0], + bookmarks: [ + { time: 1, note: "Intro", color: "#ef4444" }, + { time: 5.5, duration: 2 }, + ], + }, + ], + }; + + const result = transformProjectV5ToV6({ + project: projectWithRichBookmarks as Parameters< + typeof transformProjectV5ToV6 + >[0]["project"], + }); + + expect(result.skipped).toBe(false); + const mainScene = ( + result.project.scenes as Array<{ bookmarks: unknown[] }> + )[0]; + expect(mainScene.bookmarks).toEqual([ + { time: 1, note: "Intro", color: "#ef4444" }, + { time: 5.5, duration: 2 }, + ]); + }); +}); diff --git a/apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts b/apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts index 51e91f78..aa79a6ea 100644 --- a/apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts +++ b/apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts @@ -1,175 +1,175 @@ -import { describe, expect, test } from "bun:test"; -import { transformProjectV8ToV9 } from "../transformers/v8-to-v9"; - -const v8ProjectWithText = { - id: "project-v8-text", - version: 8, - metadata: { - id: "project-v8-text", - name: "V8 Project with Text", - createdAt: "2024-01-01T00:00:00.000Z", - updatedAt: "2024-01-01T00:00:00.000Z", - }, - settings: { - fps: 30, - canvasSize: { width: 1920, height: 1080 }, - background: { type: "color", color: "#000000" }, - }, - currentSceneId: "scene-main", - scenes: [ - { - id: "scene-main", - name: "Main scene", - isMain: true, - tracks: [ - { - id: "track-text", - type: "text", - name: "Text Track", - hidden: false, - elements: [ - { - id: "el-1", - type: "text", - content: "With color", - startTime: 0, - duration: 5, - background: { - color: "#ff0000", - cornerRadius: 0, - paddingX: 8, - paddingY: 4, - }, - }, - { - id: "el-2", - type: "text", - content: "Transparent", - startTime: 5, - duration: 5, - background: { - color: "transparent", - paddingX: 30, - paddingY: 42, - }, - }, - ], - }, - ], - bookmarks: [], - createdAt: "2024-01-01T00:00:00.000Z", - updatedAt: "2024-01-01T00:00:00.000Z", - }, - ], -} as Parameters[0]["project"]; - -describe("V8 to V9 Migration", () => { - test("adds background.enabled from color (transparent => false, otherwise true)", () => { - const result = transformProjectV8ToV9({ project: v8ProjectWithText }); - - expect(result.skipped).toBe(false); - expect(result.project.version).toBe(9); - - const track = ( - result.project.scenes as Array<{ tracks: Array<{ elements: unknown[] }> }> - )[0].tracks[0]; - const elements = track.elements as Array<{ background: { enabled: boolean; color: string } }>; - - expect(elements[0].background.enabled).toBe(true); - expect(elements[0].background.color).toBe("#ff0000"); - - expect(elements[1].background.enabled).toBe(false); - expect(elements[1].background.color).toBe("transparent"); - }); - - test("preserves existing background.enabled if already present", () => { - const projectWithEnabled = { - ...v8ProjectWithText, - scenes: [ - { - ...(v8ProjectWithText.scenes as Record[])[0], - tracks: [ - { - id: "track-text", - type: "text", - name: "Text Track", - hidden: false, - elements: [ - { - id: "el-1", - type: "text", - content: "Already has enabled", - startTime: 0, - duration: 5, - background: { - enabled: false, - color: "#00ff00", - }, - }, - ], - }, - ], - }, - ], - } as Parameters[0]["project"]; - - const result = transformProjectV8ToV9({ project: projectWithEnabled }); - - expect(result.skipped).toBe(false); - const elements = ( - result.project.scenes as Array<{ tracks: Array<{ elements: unknown[] }> }> - )[0].tracks[0].elements as Array<{ background: { enabled: boolean } }>; - expect(elements[0].background.enabled).toBe(false); - }); - - test("skips non-text elements and tracks", () => { - const projectWithVideoOnly = { - ...v8ProjectWithText, - scenes: [ - { - id: "scene-main", - name: "Main scene", - isMain: true, - tracks: [ - { - id: "track-video", - type: "video", - name: "Video Track", - isMain: true, - elements: [], - }, - ], - bookmarks: [], - createdAt: "2024-01-01T00:00:00.000Z", - updatedAt: "2024-01-01T00:00:00.000Z", - }, - ], - } as Parameters[0]["project"]; - - const result = transformProjectV8ToV9({ project: projectWithVideoOnly }); - - expect(result.skipped).toBe(false); - expect(result.project.version).toBe(9); - }); - - test("skips projects that are already v9", () => { - const result = transformProjectV8ToV9({ - project: { ...v8ProjectWithText, version: 9 }, - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("already v9"); - }); - - test("skips projects with no id", () => { - const result = transformProjectV8ToV9({ - project: { - version: 8, - scenes: [], - } as Parameters[0]["project"], - }); - - expect(result.skipped).toBe(true); - expect(result.reason).toBe("no project id"); - }); -}); +import { describe, expect, test } from "bun:test"; +import { transformProjectV8ToV9 } from "../transformers/v8-to-v9"; + +const v8ProjectWithText = { + id: "project-v8-text", + version: 8, + metadata: { + id: "project-v8-text", + name: "V8 Project with Text", + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, + settings: { + fps: 30, + canvasSize: { width: 1920, height: 1080 }, + background: { type: "color", color: "#000000" }, + }, + currentSceneId: "scene-main", + scenes: [ + { + id: "scene-main", + name: "Main scene", + isMain: true, + tracks: [ + { + id: "track-text", + type: "text", + name: "Text Track", + hidden: false, + elements: [ + { + id: "el-1", + type: "text", + content: "With color", + startTime: 0, + duration: 5, + background: { + color: "#ff0000", + cornerRadius: 0, + paddingX: 8, + paddingY: 4, + }, + }, + { + id: "el-2", + type: "text", + content: "Transparent", + startTime: 5, + duration: 5, + background: { + color: "transparent", + paddingX: 30, + paddingY: 42, + }, + }, + ], + }, + ], + bookmarks: [], + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, + ], +} as Parameters[0]["project"]; + +describe("V8 to V9 Migration", () => { + test("adds background.enabled from color (transparent => false, otherwise true)", () => { + const result = transformProjectV8ToV9({ project: v8ProjectWithText }); + + expect(result.skipped).toBe(false); + expect(result.project.version).toBe(9); + + const track = ( + result.project.scenes as Array<{ tracks: Array<{ elements: unknown[] }> }> + )[0].tracks[0]; + const elements = track.elements as Array<{ background: { enabled: boolean; color: string } }>; + + expect(elements[0].background.enabled).toBe(true); + expect(elements[0].background.color).toBe("#ff0000"); + + expect(elements[1].background.enabled).toBe(false); + expect(elements[1].background.color).toBe("transparent"); + }); + + test("preserves existing background.enabled if already present", () => { + const projectWithEnabled = { + ...v8ProjectWithText, + scenes: [ + { + ...(v8ProjectWithText.scenes as Record[])[0], + tracks: [ + { + id: "track-text", + type: "text", + name: "Text Track", + hidden: false, + elements: [ + { + id: "el-1", + type: "text", + content: "Already has enabled", + startTime: 0, + duration: 5, + background: { + enabled: false, + color: "#00ff00", + }, + }, + ], + }, + ], + }, + ], + } as Parameters[0]["project"]; + + const result = transformProjectV8ToV9({ project: projectWithEnabled }); + + expect(result.skipped).toBe(false); + const elements = ( + result.project.scenes as Array<{ tracks: Array<{ elements: unknown[] }> }> + )[0].tracks[0].elements as Array<{ background: { enabled: boolean } }>; + expect(elements[0].background.enabled).toBe(false); + }); + + test("skips non-text elements and tracks", () => { + const projectWithVideoOnly = { + ...v8ProjectWithText, + scenes: [ + { + id: "scene-main", + name: "Main scene", + isMain: true, + tracks: [ + { + id: "track-video", + type: "video", + name: "Video Track", + isMain: true, + elements: [], + }, + ], + bookmarks: [], + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, + ], + } as Parameters[0]["project"]; + + const result = transformProjectV8ToV9({ project: projectWithVideoOnly }); + + expect(result.skipped).toBe(false); + expect(result.project.version).toBe(9); + }); + + test("skips projects that are already v9", () => { + const result = transformProjectV8ToV9({ + project: { ...v8ProjectWithText, version: 9 }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("already v9"); + }); + + test("skips projects with no id", () => { + const result = transformProjectV8ToV9({ + project: { + version: 8, + scenes: [], + } as Parameters[0]["project"], + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("no project id"); + }); +}); diff --git a/apps/web/src/services/storage/migrations/base.ts b/apps/web/src/services/storage/migrations/base.ts index 5b7e9db0..13418f98 100644 --- a/apps/web/src/services/storage/migrations/base.ts +++ b/apps/web/src/services/storage/migrations/base.ts @@ -1,9 +1,9 @@ -import type { MigrationResult, ProjectRecord } from "./transformers/types"; - -export abstract class StorageMigration { - abstract from: number; - abstract to: number; - abstract transform( - project: ProjectRecord, - ): Promise>; -} +import type { MigrationResult, ProjectRecord } from "./transformers/types"; + +export abstract class StorageMigration { + abstract from: number; + abstract to: number; + abstract transform( + project: ProjectRecord, + ): Promise>; +} diff --git a/apps/web/src/services/storage/migrations/index.ts b/apps/web/src/services/storage/migrations/index.ts index 1a855287..e87d1cb9 100644 --- a/apps/web/src/services/storage/migrations/index.ts +++ b/apps/web/src/services/storage/migrations/index.ts @@ -1,46 +1,46 @@ -export { StorageMigration } from "./base"; -import { V0toV1Migration } from "./v0-to-v1"; -import { V1toV2Migration } from "./v1-to-v2"; -import { V2toV3Migration } from "./v2-to-v3"; -import { V3toV4Migration } from "./v3-to-v4"; -import { V4toV5Migration } from "./v4-to-v5"; -import { V5toV6Migration } from "./v5-to-v6"; -import { V6toV7Migration } from "./v6-to-v7"; -import { V7toV8Migration } from "./v7-to-v8"; -import { V8toV9Migration } from "./v8-to-v9"; -import { V9toV10Migration } from "./v9-to-v10"; -import { V10toV11Migration } from "./v10-to-v11"; -import { V11toV12Migration } from "./v11-to-v12"; -import { V12toV13Migration } from "./v12-to-v13"; -import { V13toV14Migration } from "./v13-to-v14"; -import { V14toV15Migration } from "./v14-to-v15"; -import { V15toV16Migration } from "./v15-to-v16"; -import { V16toV17Migration } from "./v16-to-v17"; -import { V17toV18Migration } from "./v17-to-v18"; -import { V18toV19Migration } from "./v18-to-v19"; -export { runStorageMigrations } from "./runner"; -export type { MigrationProgress } from "./runner"; - -export const CURRENT_PROJECT_VERSION = 19; - -export const migrations = [ - new V0toV1Migration(), - new V1toV2Migration(), - new V2toV3Migration(), - new V3toV4Migration(), - new V4toV5Migration(), - new V5toV6Migration(), - new V6toV7Migration(), - new V7toV8Migration(), - new V8toV9Migration(), - new V9toV10Migration(), - new V10toV11Migration(), - new V11toV12Migration(), - new V12toV13Migration(), - new V13toV14Migration(), - new V14toV15Migration(), - new V15toV16Migration(), - new V16toV17Migration(), - new V17toV18Migration(), - new V18toV19Migration(), -]; +export { StorageMigration } from "./base"; +import { V0toV1Migration } from "./v0-to-v1"; +import { V1toV2Migration } from "./v1-to-v2"; +import { V2toV3Migration } from "./v2-to-v3"; +import { V3toV4Migration } from "./v3-to-v4"; +import { V4toV5Migration } from "./v4-to-v5"; +import { V5toV6Migration } from "./v5-to-v6"; +import { V6toV7Migration } from "./v6-to-v7"; +import { V7toV8Migration } from "./v7-to-v8"; +import { V8toV9Migration } from "./v8-to-v9"; +import { V9toV10Migration } from "./v9-to-v10"; +import { V10toV11Migration } from "./v10-to-v11"; +import { V11toV12Migration } from "./v11-to-v12"; +import { V12toV13Migration } from "./v12-to-v13"; +import { V13toV14Migration } from "./v13-to-v14"; +import { V14toV15Migration } from "./v14-to-v15"; +import { V15toV16Migration } from "./v15-to-v16"; +import { V16toV17Migration } from "./v16-to-v17"; +import { V17toV18Migration } from "./v17-to-v18"; +import { V18toV19Migration } from "./v18-to-v19"; +export { runStorageMigrations } from "./runner"; +export type { MigrationProgress } from "./runner"; + +export const CURRENT_PROJECT_VERSION = 19; + +export const migrations = [ + new V0toV1Migration(), + new V1toV2Migration(), + new V2toV3Migration(), + new V3toV4Migration(), + new V4toV5Migration(), + new V5toV6Migration(), + new V6toV7Migration(), + new V7toV8Migration(), + new V8toV9Migration(), + new V9toV10Migration(), + new V10toV11Migration(), + new V11toV12Migration(), + new V12toV13Migration(), + new V13toV14Migration(), + new V14toV15Migration(), + new V15toV16Migration(), + new V16toV17Migration(), + new V17toV18Migration(), + new V18toV19Migration(), +]; diff --git a/apps/web/src/services/storage/migrations/runner.ts b/apps/web/src/services/storage/migrations/runner.ts index e1c2d09a..409a8695 100644 --- a/apps/web/src/services/storage/migrations/runner.ts +++ b/apps/web/src/services/storage/migrations/runner.ts @@ -1,155 +1,155 @@ -import { - IndexedDBAdapter, - deleteDatabase, -} from "@/services/storage/indexeddb-adapter"; -import type { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { getProjectId, isRecord } from "./transformers/utils"; - -export interface StorageMigrationResult { - migratedCount: number; -} - -export interface MigrationProgress { - isMigrating: boolean; - fromVersion: number | null; - toVersion: number | null; - projectName: string | null; -} - -let hasCleanedUpMetaDb = false; - -const MIN_MIGRATION_DISPLAY_MS = 1000; - -export async function runStorageMigrations({ - migrations, - onProgress, -}: { - migrations: StorageMigration[]; - onProgress?: (progress: MigrationProgress) => void; -}): Promise { - // One-time cleanup: delete the old global version database - if (!hasCleanedUpMetaDb) { - try { - await deleteDatabase({ dbName: "video-editor-meta" }); - } catch { - // Ignore errors - DB might not exist - } - hasCleanedUpMetaDb = true; - } - - const projectsAdapter = new IndexedDBAdapter( - "video-editor-projects", - "projects", - 1, - ); - const projects = await projectsAdapter.getAll(); - - const orderedMigrations = [...migrations].sort((a, b) => a.from - b.from); - let migratedCount = 0; - let migrationStartTime: number | null = null; - - for (const project of projects) { - if (typeof project !== "object" || project === null) { - continue; - } - - let projectRecord = project as ProjectRecord; - let currentVersion = getProjectVersion({ project: projectRecord }); - const targetVersion = orderedMigrations.at(-1)?.to ?? currentVersion; - - if (currentVersion >= targetVersion) { - continue; - } - - // Track when we first showed the migration dialog - if (migrationStartTime === null) { - migrationStartTime = Date.now(); - } - - const projectName = getProjectName({ project: projectRecord }); - onProgress?.({ - isMigrating: true, - fromVersion: currentVersion, - toVersion: targetVersion, - projectName, - }); - - for (const migration of orderedMigrations) { - if (migration.from !== currentVersion) { - continue; - } - - const result = await migration.transform(projectRecord); - - if (result.skipped) { - break; - } - - const projectId = getProjectId({ project: result.project }); - if (!projectId) { - break; - } - - await projectsAdapter.set(projectId, result.project); - migratedCount++; - currentVersion = migration.to; - projectRecord = result.project; - } - } - - // Ensure dialog is visible for minimum time so users can see it - if (migrationStartTime !== null) { - const elapsed = Date.now() - migrationStartTime; - if (elapsed < MIN_MIGRATION_DISPLAY_MS) { - await new Promise((resolve) => - setTimeout(resolve, MIN_MIGRATION_DISPLAY_MS - elapsed), - ); - } - } - - onProgress?.({ - isMigrating: false, - fromVersion: null, - toVersion: null, - projectName: null, - }); - - return { migratedCount }; -} - -function getProjectVersion({ project }: { project: ProjectRecord }): number { - const versionValue = project.version; - - // v2 and up - has explicit version field - if (typeof versionValue === "number") { - return versionValue; - } - - // v1 - has scenes array - const scenesValue = project.scenes; - if (Array.isArray(scenesValue) && scenesValue.length > 0) { - return 1; - } - - // v0 - no scenes - return 0; -} - -function getProjectName({ - project, -}: { - project: ProjectRecord; -}): string | null { - const metadata = project.metadata; - if (isRecord(metadata) && typeof metadata.name === "string") { - return metadata.name; - } - - // v0 had name directly on project - if (typeof project.name === "string") { - return project.name; - } - - return null; -} +import { + IndexedDBAdapter, + deleteDatabase, +} from "@/services/storage/indexeddb-adapter"; +import type { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { getProjectId, isRecord } from "./transformers/utils"; + +export interface StorageMigrationResult { + migratedCount: number; +} + +export interface MigrationProgress { + isMigrating: boolean; + fromVersion: number | null; + toVersion: number | null; + projectName: string | null; +} + +let hasCleanedUpMetaDb = false; + +const MIN_MIGRATION_DISPLAY_MS = 1000; + +export async function runStorageMigrations({ + migrations, + onProgress, +}: { + migrations: StorageMigration[]; + onProgress?: (progress: MigrationProgress) => void; +}): Promise { + // One-time cleanup: delete the old global version database + if (!hasCleanedUpMetaDb) { + try { + await deleteDatabase({ dbName: "video-editor-meta" }); + } catch { + // Ignore errors - DB might not exist + } + hasCleanedUpMetaDb = true; + } + + const projectsAdapter = new IndexedDBAdapter( + "video-editor-projects", + "projects", + 1, + ); + const projects = await projectsAdapter.getAll(); + + const orderedMigrations = [...migrations].sort((a, b) => a.from - b.from); + let migratedCount = 0; + let migrationStartTime: number | null = null; + + for (const project of projects) { + if (typeof project !== "object" || project === null) { + continue; + } + + let projectRecord = project as ProjectRecord; + let currentVersion = getProjectVersion({ project: projectRecord }); + const targetVersion = orderedMigrations.at(-1)?.to ?? currentVersion; + + if (currentVersion >= targetVersion) { + continue; + } + + // Track when we first showed the migration dialog + if (migrationStartTime === null) { + migrationStartTime = Date.now(); + } + + const projectName = getProjectName({ project: projectRecord }); + onProgress?.({ + isMigrating: true, + fromVersion: currentVersion, + toVersion: targetVersion, + projectName, + }); + + for (const migration of orderedMigrations) { + if (migration.from !== currentVersion) { + continue; + } + + const result = await migration.transform(projectRecord); + + if (result.skipped) { + break; + } + + const projectId = getProjectId({ project: result.project }); + if (!projectId) { + break; + } + + await projectsAdapter.set(projectId, result.project); + migratedCount++; + currentVersion = migration.to; + projectRecord = result.project; + } + } + + // Ensure dialog is visible for minimum time so users can see it + if (migrationStartTime !== null) { + const elapsed = Date.now() - migrationStartTime; + if (elapsed < MIN_MIGRATION_DISPLAY_MS) { + await new Promise((resolve) => + setTimeout(resolve, MIN_MIGRATION_DISPLAY_MS - elapsed), + ); + } + } + + onProgress?.({ + isMigrating: false, + fromVersion: null, + toVersion: null, + projectName: null, + }); + + return { migratedCount }; +} + +function getProjectVersion({ project }: { project: ProjectRecord }): number { + const versionValue = project.version; + + // v2 and up - has explicit version field + if (typeof versionValue === "number") { + return versionValue; + } + + // v1 - has scenes array + const scenesValue = project.scenes; + if (Array.isArray(scenesValue) && scenesValue.length > 0) { + return 1; + } + + // v0 - no scenes + return 0; +} + +function getProjectName({ + project, +}: { + project: ProjectRecord; +}): string | null { + const metadata = project.metadata; + if (isRecord(metadata) && typeof metadata.name === "string") { + return metadata.name; + } + + // v0 had name directly on project + if (typeof project.name === "string") { + return project.name; + } + + return null; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v10-to-v11.ts b/apps/web/src/services/storage/migrations/transformers/v10-to-v11.ts index b48f5e88..2f803d67 100644 --- a/apps/web/src/services/storage/migrations/transformers/v10-to-v11.ts +++ b/apps/web/src/services/storage/migrations/transformers/v10-to-v11.ts @@ -1,113 +1,113 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV10ToV11({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - if (!getProjectId({ project })) { - return { project, skipped: true, reason: "no project id" }; - } - - if (typeof project.version === "number" && project.version >= 11) { - return { project, skipped: true, reason: "already v11" }; - } - - const migratedProject = migrateProjectScale({ project }); - - return { - project: { ...migratedProject, version: 11 }, - skipped: false, - }; -} - -function migrateProjectScale({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) return project; - - const migratedScenes = scenesValue.map((scene) => - migrateSceneScale({ scene }), - ); - - return { ...project, scenes: migratedScenes }; -} - -function migrateSceneScale({ scene }: { scene: unknown }): unknown { - if (!isRecord(scene)) return scene; - - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) return scene; - - const migratedTracks = tracksValue.map((track) => - migrateTrackScale({ track }), - ); - - return { ...scene, tracks: migratedTracks }; -} - -function migrateTrackScale({ track }: { track: unknown }): unknown { - if (!isRecord(track)) return track; - - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) return track; - - const migratedElements = elementsValue.map((element) => - migrateElementScale({ element }), - ); - - return { ...track, elements: migratedElements }; -} - -function migrateElementScale({ element }: { element: unknown }): unknown { - if (!isRecord(element)) return element; - - const transform = element.transform; - if (!isRecord(transform)) return element; - - const scale = transform.scale; - if (typeof scale !== "number") return element; - - const migratedTransform = { - ...transform, - scaleX: scale, - scaleY: scale, - }; - delete (migratedTransform as Record).scale; - - let migratedElement: ProjectRecord = { - ...element, - transform: migratedTransform, - }; - - const animations = element.animations; - if (isRecord(animations) && isRecord(animations.channels)) { - const channels = animations.channels as Record; - const scaleChannel = channels["transform.scale"]; - if (scaleChannel && isRecord(scaleChannel)) { - const keyframes = (scaleChannel as { keyframes?: unknown[] }).keyframes; - if (Array.isArray(keyframes)) { - const newChannels = { ...channels }; - delete newChannels["transform.scale"]; - newChannels["transform.scaleX"] = { - ...scaleChannel, - keyframes: [...keyframes], - }; - newChannels["transform.scaleY"] = { - ...scaleChannel, - keyframes: keyframes.map((kf) => (isRecord(kf) ? { ...kf } : kf)), - }; - migratedElement = { - ...migratedElement, - animations: { ...animations, channels: newChannels }, - }; - } - } - } - - return migratedElement; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV10ToV11({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 11) { + return { project, skipped: true, reason: "already v11" }; + } + + const migratedProject = migrateProjectScale({ project }); + + return { + project: { ...migratedProject, version: 11 }, + skipped: false, + }; +} + +function migrateProjectScale({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) return project; + + const migratedScenes = scenesValue.map((scene) => + migrateSceneScale({ scene }), + ); + + return { ...project, scenes: migratedScenes }; +} + +function migrateSceneScale({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) return scene; + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) return scene; + + const migratedTracks = tracksValue.map((track) => + migrateTrackScale({ track }), + ); + + return { ...scene, tracks: migratedTracks }; +} + +function migrateTrackScale({ track }: { track: unknown }): unknown { + if (!isRecord(track)) return track; + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) return track; + + const migratedElements = elementsValue.map((element) => + migrateElementScale({ element }), + ); + + return { ...track, elements: migratedElements }; +} + +function migrateElementScale({ element }: { element: unknown }): unknown { + if (!isRecord(element)) return element; + + const transform = element.transform; + if (!isRecord(transform)) return element; + + const scale = transform.scale; + if (typeof scale !== "number") return element; + + const migratedTransform = { + ...transform, + scaleX: scale, + scaleY: scale, + }; + delete (migratedTransform as Record).scale; + + let migratedElement: ProjectRecord = { + ...element, + transform: migratedTransform, + }; + + const animations = element.animations; + if (isRecord(animations) && isRecord(animations.channels)) { + const channels = animations.channels as Record; + const scaleChannel = channels["transform.scale"]; + if (scaleChannel && isRecord(scaleChannel)) { + const keyframes = (scaleChannel as { keyframes?: unknown[] }).keyframes; + if (Array.isArray(keyframes)) { + const newChannels = { ...channels }; + delete newChannels["transform.scale"]; + newChannels["transform.scaleX"] = { + ...scaleChannel, + keyframes: [...keyframes], + }; + newChannels["transform.scaleY"] = { + ...scaleChannel, + keyframes: keyframes.map((kf) => (isRecord(kf) ? { ...kf } : kf)), + }; + migratedElement = { + ...migratedElement, + animations: { ...animations, channels: newChannels }, + }; + } + } + } + + return migratedElement; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v11-to-v12.ts b/apps/web/src/services/storage/migrations/transformers/v11-to-v12.ts index fa1bd371..4efc5b25 100644 --- a/apps/web/src/services/storage/migrations/transformers/v11-to-v12.ts +++ b/apps/web/src/services/storage/migrations/transformers/v11-to-v12.ts @@ -1,196 +1,196 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV11ToV12({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - if (!getProjectId({ project })) { - return { project, skipped: true, reason: "no project id" }; - } - - if (typeof project.version === "number" && project.version >= 12) { - return { project, skipped: true, reason: "already v12" }; - } - - const migratedProject = migrateProjectPosition({ project }); - - return { - project: { ...migratedProject, version: 12 }, - skipped: false, - }; -} - -function migrateProjectPosition({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) return project; - - const migratedScenes = scenesValue.map((scene) => - migrateScenePosition({ scene }), - ); - - return { ...project, scenes: migratedScenes }; -} - -function migrateScenePosition({ scene }: { scene: unknown }): unknown { - if (!isRecord(scene)) return scene; - - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) return scene; - - const migratedTracks = tracksValue.map((track) => - migrateTrackPosition({ track }), - ); - - return { ...scene, tracks: migratedTracks }; -} - -function migrateTrackPosition({ track }: { track: unknown }): unknown { - if (!isRecord(track)) return track; - - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) return track; - - const migratedElements = elementsValue.map((element) => - migrateElementPosition({ element }), - ); - - return { ...track, elements: migratedElements }; -} - -function migrateElementPosition({ element }: { element: unknown }): unknown { - if (!isRecord(element)) return element; - - const animations = element.animations; - if (!isRecord(animations) || !isRecord(animations.channels)) return element; - - const channels = animations.channels as Record; - const xChannel = channels["transform.position.x"]; - const yChannel = channels["transform.position.y"]; - - if (!xChannel && !yChannel) return element; - - const baseTransform = isRecord(element.transform) ? element.transform : {}; - const basePosition = isRecord(baseTransform.position) - ? baseTransform.position - : {}; - const baseX = typeof basePosition.x === "number" ? basePosition.x : 0; - const baseY = typeof basePosition.y === "number" ? basePosition.y : 0; - - const xKeyframes = getKeyframes({ channel: xChannel }); - const yKeyframes = getKeyframes({ channel: yChannel }); - - const allTimes = Array.from( - new Set([ - ...xKeyframes.map((kf) => kf.time), - ...yKeyframes.map((kf) => kf.time), - ]), - ).sort((a, b) => a - b); - - const vectorKeyframes = allTimes.map((time) => { - const xKf = xKeyframes.find((kf) => Math.abs(kf.time - time) < 0.001); - const yKf = yKeyframes.find((kf) => Math.abs(kf.time - time) < 0.001); - const x = - typeof xKf?.value === "number" - ? xKf.value - : interpolateScalarAtTime({ - keyframes: xKeyframes, - time, - fallback: baseX, - }); - const y = - typeof yKf?.value === "number" - ? yKf.value - : interpolateScalarAtTime({ - keyframes: yKeyframes, - time, - fallback: baseY, - }); - const interpolation = xKf?.interpolation ?? yKf?.interpolation ?? "linear"; - return { - id: xKf?.id ?? yKf?.id ?? crypto.randomUUID(), - time, - value: { x, y }, - interpolation, - }; - }); - - const newChannels = { ...channels }; - delete newChannels["transform.position.x"]; - delete newChannels["transform.position.y"]; - newChannels["transform.position"] = { - valueKind: "vector", - keyframes: vectorKeyframes, - }; - - return { - ...element, - animations: { ...animations, channels: newChannels }, - }; -} - -interface ScalarKeyframe { - id: string; - time: number; - value: number; - interpolation: string; -} - -function getKeyframes({ channel }: { channel: unknown }): ScalarKeyframe[] { - if (!isRecord(channel)) return []; - if (!Array.isArray(channel.keyframes)) return []; - return channel.keyframes.flatMap((kf) => { - if (!isRecord(kf)) return []; - if ( - typeof kf.id !== "string" || - typeof kf.time !== "number" || - typeof kf.value !== "number" - ) - return []; - return [ - { - id: kf.id, - time: kf.time, - value: kf.value, - interpolation: - typeof kf.interpolation === "string" ? kf.interpolation : "linear", - }, - ]; - }); -} - -function interpolateScalarAtTime({ - keyframes, - time, - fallback, -}: { - keyframes: ScalarKeyframe[]; - time: number; - fallback: number; -}): number { - if (keyframes.length === 0) return fallback; - - const sorted = [...keyframes].sort((a, b) => a.time - b.time); - const first = sorted[0]; - const last = sorted[sorted.length - 1]; - - if (!first || !last) return fallback; - if (time <= first.time) return first.value; - if (time >= last.time) return last.value; - - for (let i = 0; i < sorted.length - 1; i++) { - const left = sorted[i]; - const right = sorted[i + 1]; - if (time < left.time || time > right.time) continue; - if (left.interpolation === "hold") return left.value; - const t = (time - left.time) / (right.time - left.time); - return left.value + (right.value - left.value) * t; - } - - return last.value; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV11ToV12({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 12) { + return { project, skipped: true, reason: "already v12" }; + } + + const migratedProject = migrateProjectPosition({ project }); + + return { + project: { ...migratedProject, version: 12 }, + skipped: false, + }; +} + +function migrateProjectPosition({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) return project; + + const migratedScenes = scenesValue.map((scene) => + migrateScenePosition({ scene }), + ); + + return { ...project, scenes: migratedScenes }; +} + +function migrateScenePosition({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) return scene; + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) return scene; + + const migratedTracks = tracksValue.map((track) => + migrateTrackPosition({ track }), + ); + + return { ...scene, tracks: migratedTracks }; +} + +function migrateTrackPosition({ track }: { track: unknown }): unknown { + if (!isRecord(track)) return track; + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) return track; + + const migratedElements = elementsValue.map((element) => + migrateElementPosition({ element }), + ); + + return { ...track, elements: migratedElements }; +} + +function migrateElementPosition({ element }: { element: unknown }): unknown { + if (!isRecord(element)) return element; + + const animations = element.animations; + if (!isRecord(animations) || !isRecord(animations.channels)) return element; + + const channels = animations.channels as Record; + const xChannel = channels["transform.position.x"]; + const yChannel = channels["transform.position.y"]; + + if (!xChannel && !yChannel) return element; + + const baseTransform = isRecord(element.transform) ? element.transform : {}; + const basePosition = isRecord(baseTransform.position) + ? baseTransform.position + : {}; + const baseX = typeof basePosition.x === "number" ? basePosition.x : 0; + const baseY = typeof basePosition.y === "number" ? basePosition.y : 0; + + const xKeyframes = getKeyframes({ channel: xChannel }); + const yKeyframes = getKeyframes({ channel: yChannel }); + + const allTimes = Array.from( + new Set([ + ...xKeyframes.map((kf) => kf.time), + ...yKeyframes.map((kf) => kf.time), + ]), + ).sort((a, b) => a - b); + + const vectorKeyframes = allTimes.map((time) => { + const xKf = xKeyframes.find((kf) => Math.abs(kf.time - time) < 0.001); + const yKf = yKeyframes.find((kf) => Math.abs(kf.time - time) < 0.001); + const x = + typeof xKf?.value === "number" + ? xKf.value + : interpolateScalarAtTime({ + keyframes: xKeyframes, + time, + fallback: baseX, + }); + const y = + typeof yKf?.value === "number" + ? yKf.value + : interpolateScalarAtTime({ + keyframes: yKeyframes, + time, + fallback: baseY, + }); + const interpolation = xKf?.interpolation ?? yKf?.interpolation ?? "linear"; + return { + id: xKf?.id ?? yKf?.id ?? crypto.randomUUID(), + time, + value: { x, y }, + interpolation, + }; + }); + + const newChannels = { ...channels }; + delete newChannels["transform.position.x"]; + delete newChannels["transform.position.y"]; + newChannels["transform.position"] = { + valueKind: "vector", + keyframes: vectorKeyframes, + }; + + return { + ...element, + animations: { ...animations, channels: newChannels }, + }; +} + +interface ScalarKeyframe { + id: string; + time: number; + value: number; + interpolation: string; +} + +function getKeyframes({ channel }: { channel: unknown }): ScalarKeyframe[] { + if (!isRecord(channel)) return []; + if (!Array.isArray(channel.keyframes)) return []; + return channel.keyframes.flatMap((kf) => { + if (!isRecord(kf)) return []; + if ( + typeof kf.id !== "string" || + typeof kf.time !== "number" || + typeof kf.value !== "number" + ) + return []; + return [ + { + id: kf.id, + time: kf.time, + value: kf.value, + interpolation: + typeof kf.interpolation === "string" ? kf.interpolation : "linear", + }, + ]; + }); +} + +function interpolateScalarAtTime({ + keyframes, + time, + fallback, +}: { + keyframes: ScalarKeyframe[]; + time: number; + fallback: number; +}): number { + if (keyframes.length === 0) return fallback; + + const sorted = [...keyframes].sort((a, b) => a.time - b.time); + const first = sorted[0]; + const last = sorted[sorted.length - 1]; + + if (!first || !last) return fallback; + if (time <= first.time) return first.value; + if (time >= last.time) return last.value; + + for (let i = 0; i < sorted.length - 1; i++) { + const left = sorted[i]; + const right = sorted[i + 1]; + if (time < left.time || time > right.time) continue; + if (left.interpolation === "hold") return left.value; + const t = (time - left.time) / (right.time - left.time); + return left.value + (right.value - left.value) * t; + } + + return last.value; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v12-to-v13.ts b/apps/web/src/services/storage/migrations/transformers/v12-to-v13.ts index 59b29732..518de59a 100644 --- a/apps/web/src/services/storage/migrations/transformers/v12-to-v13.ts +++ b/apps/web/src/services/storage/migrations/transformers/v12-to-v13.ts @@ -1,102 +1,102 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV12ToV13({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - if (!getProjectId({ project })) { - return { project, skipped: true, reason: "no project id" }; - } - - if (typeof project.version === "number" && project.version >= 13) { - return { project, skipped: true, reason: "already v13" }; - } - - const migratedProject = migrateProjectMasks({ project }); - - return { - project: { ...migratedProject, version: 13 }, - skipped: false, - }; -} - -function migrateProjectMasks({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) return project; - - const migratedScenes = scenesValue.map((scene) => - migrateSceneMasks({ scene }), - ); - - return { ...project, scenes: migratedScenes }; -} - -function migrateSceneMasks({ scene }: { scene: unknown }): unknown { - if (!isRecord(scene)) return scene; - - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) return scene; - - const migratedTracks = tracksValue.map((track) => - migrateTrackMasks({ track }), - ); - - return { ...scene, tracks: migratedTracks }; -} - -function migrateTrackMasks({ track }: { track: unknown }): unknown { - if (!isRecord(track)) return track; - - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) return track; - - const migratedElements = elementsValue.map((element) => - migrateElementMasks({ element }), - ); - - return { ...track, elements: migratedElements }; -} - -function migrateElementMasks({ element }: { element: unknown }): unknown { - if (!isRecord(element)) return element; - - const masksValue = element.masks; - if (!Array.isArray(masksValue)) return element; - - const migratedMasks = masksValue.map((mask) => migrateMask({ mask })); - - return { ...element, masks: migratedMasks }; -} - -function migrateMask({ mask }: { mask: unknown }): unknown { - if (!isRecord(mask)) return mask; - - const params = isRecord(mask.params) ? { ...mask.params } : {}; - const result = { ...mask }; - - result.params = { - ...params, - feather: typeof mask.feather === "number" ? mask.feather : 0, - inverted: typeof mask.inverted === "boolean" ? mask.inverted : false, - strokeColor: - isRecord(mask.stroke) && typeof mask.stroke.color === "string" - ? mask.stroke.color - : "#ffffff", - strokeWidth: - isRecord(mask.stroke) && typeof mask.stroke.width === "number" - ? mask.stroke.width - : 0, - }; - - delete (result as Record).feather; - delete (result as Record).inverted; - delete (result as Record).stroke; - - return result; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV12ToV13({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 13) { + return { project, skipped: true, reason: "already v13" }; + } + + const migratedProject = migrateProjectMasks({ project }); + + return { + project: { ...migratedProject, version: 13 }, + skipped: false, + }; +} + +function migrateProjectMasks({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) return project; + + const migratedScenes = scenesValue.map((scene) => + migrateSceneMasks({ scene }), + ); + + return { ...project, scenes: migratedScenes }; +} + +function migrateSceneMasks({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) return scene; + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) return scene; + + const migratedTracks = tracksValue.map((track) => + migrateTrackMasks({ track }), + ); + + return { ...scene, tracks: migratedTracks }; +} + +function migrateTrackMasks({ track }: { track: unknown }): unknown { + if (!isRecord(track)) return track; + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) return track; + + const migratedElements = elementsValue.map((element) => + migrateElementMasks({ element }), + ); + + return { ...track, elements: migratedElements }; +} + +function migrateElementMasks({ element }: { element: unknown }): unknown { + if (!isRecord(element)) return element; + + const masksValue = element.masks; + if (!Array.isArray(masksValue)) return element; + + const migratedMasks = masksValue.map((mask) => migrateMask({ mask })); + + return { ...element, masks: migratedMasks }; +} + +function migrateMask({ mask }: { mask: unknown }): unknown { + if (!isRecord(mask)) return mask; + + const params = isRecord(mask.params) ? { ...mask.params } : {}; + const result = { ...mask }; + + result.params = { + ...params, + feather: typeof mask.feather === "number" ? mask.feather : 0, + inverted: typeof mask.inverted === "boolean" ? mask.inverted : false, + strokeColor: + isRecord(mask.stroke) && typeof mask.stroke.color === "string" + ? mask.stroke.color + : "#ffffff", + strokeWidth: + isRecord(mask.stroke) && typeof mask.stroke.width === "number" + ? mask.stroke.width + : 0, + }; + + delete (result as Record).feather; + delete (result as Record).inverted; + delete (result as Record).stroke; + + return result; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v13-to-v14.ts b/apps/web/src/services/storage/migrations/transformers/v13-to-v14.ts index 01a3cc3e..89dde38e 100644 --- a/apps/web/src/services/storage/migrations/transformers/v13-to-v14.ts +++ b/apps/web/src/services/storage/migrations/transformers/v13-to-v14.ts @@ -1,106 +1,106 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV13ToV14({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - if (!getProjectId({ project })) { - return { project, skipped: true, reason: "no project id" }; - } - - if (typeof project.version === "number" && project.version >= 14) { - return { project, skipped: true, reason: "already v14" }; - } - - const migratedProject = migrateProjectSplitMasks({ project }); - - return { - project: { ...migratedProject, version: 14 }, - skipped: false, - }; -} - -function migrateProjectSplitMasks({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) return project; - - const migratedScenes = scenesValue.map((scene) => - migrateSceneSplitMasks({ scene }), - ); - - return { ...project, scenes: migratedScenes }; -} - -function migrateSceneSplitMasks({ scene }: { scene: unknown }): unknown { - if (!isRecord(scene)) return scene; - - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) return scene; - - const migratedTracks = tracksValue.map((track) => - migrateTrackSplitMasks({ track }), - ); - - return { ...scene, tracks: migratedTracks }; -} - -function migrateTrackSplitMasks({ track }: { track: unknown }): unknown { - if (!isRecord(track)) return track; - - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) return track; - - const migratedElements = elementsValue.map((element) => - migrateElementSplitMasks({ element }), - ); - - return { ...track, elements: migratedElements }; -} - -function migrateElementSplitMasks({ element }: { element: unknown }): unknown { - if (!isRecord(element)) return element; - - const masksValue = element.masks; - if (!Array.isArray(masksValue)) return element; - - const migratedMasks = masksValue.map((mask) => migrateSplitMask({ mask })); - - return { ...element, masks: migratedMasks }; -} - -/** - * Converts split mask params from position-along-normal to explicit (x, y) anchor. - * - * Old: center = (cx + (position-0.5)*W*cos(rot), cy + (position-0.5)*W*sin(rot)) - * New: center = (cx + x*W, cy + y*W) - * - * Conversion: x = (position-0.5)*cos(rot), y = (position-0.5)*sin(rot) - */ -function migrateSplitMask({ mask }: { mask: unknown }): unknown { - if (!isRecord(mask)) return mask; - if (mask.type !== "split") return mask; - - const params = isRecord(mask.params) ? mask.params : {}; - const position = typeof params.position === "number" ? params.position : 0.5; - const rotation = typeof params.rotation === "number" ? params.rotation : 0; - - const angleRad = (rotation * Math.PI) / 180; - const x = (position - 0.5) * Math.cos(angleRad); - const y = (position - 0.5) * Math.sin(angleRad); - - const { position: _removed, ...restParams } = params as Record< - string, - unknown - > & { position: unknown }; - - return { - ...mask, - params: { ...restParams, x, y }, - }; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV13ToV14({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 14) { + return { project, skipped: true, reason: "already v14" }; + } + + const migratedProject = migrateProjectSplitMasks({ project }); + + return { + project: { ...migratedProject, version: 14 }, + skipped: false, + }; +} + +function migrateProjectSplitMasks({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) return project; + + const migratedScenes = scenesValue.map((scene) => + migrateSceneSplitMasks({ scene }), + ); + + return { ...project, scenes: migratedScenes }; +} + +function migrateSceneSplitMasks({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) return scene; + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) return scene; + + const migratedTracks = tracksValue.map((track) => + migrateTrackSplitMasks({ track }), + ); + + return { ...scene, tracks: migratedTracks }; +} + +function migrateTrackSplitMasks({ track }: { track: unknown }): unknown { + if (!isRecord(track)) return track; + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) return track; + + const migratedElements = elementsValue.map((element) => + migrateElementSplitMasks({ element }), + ); + + return { ...track, elements: migratedElements }; +} + +function migrateElementSplitMasks({ element }: { element: unknown }): unknown { + if (!isRecord(element)) return element; + + const masksValue = element.masks; + if (!Array.isArray(masksValue)) return element; + + const migratedMasks = masksValue.map((mask) => migrateSplitMask({ mask })); + + return { ...element, masks: migratedMasks }; +} + +/** + * Converts split mask params from position-along-normal to explicit (x, y) anchor. + * + * Old: center = (cx + (position-0.5)*W*cos(rot), cy + (position-0.5)*W*sin(rot)) + * New: center = (cx + x*W, cy + y*W) + * + * Conversion: x = (position-0.5)*cos(rot), y = (position-0.5)*sin(rot) + */ +function migrateSplitMask({ mask }: { mask: unknown }): unknown { + if (!isRecord(mask)) return mask; + if (mask.type !== "split") return mask; + + const params = isRecord(mask.params) ? mask.params : {}; + const position = typeof params.position === "number" ? params.position : 0.5; + const rotation = typeof params.rotation === "number" ? params.rotation : 0; + + const angleRad = (rotation * Math.PI) / 180; + const x = (position - 0.5) * Math.cos(angleRad); + const y = (position - 0.5) * Math.sin(angleRad); + + const { position: _removed, ...restParams } = params as Record< + string, + unknown + > & { position: unknown }; + + return { + ...mask, + params: { ...restParams, x, y }, + }; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts index beef05d4..e75c541b 100644 --- a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts +++ b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts @@ -1,67 +1,67 @@ -import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/constants/sticker-constants"; -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV14ToV15({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - if (!getProjectId({ project })) { - return { project, skipped: true, reason: "no project id" }; - } - - if (typeof project.version === "number" && project.version >= 15) { - return { project, skipped: true, reason: "already v15" }; - } - - const migratedProject = backfillStickerIntrinsicDimensions({ project }); - - return { - project: { ...migratedProject, version: 15 }, - skipped: false, - }; -} - -function backfillStickerIntrinsicDimensions({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) return project; - - const migratedScenes = scenesValue.map((scene) => { - if (!isRecord(scene)) return scene; - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) return scene; - - const migratedTracks = tracksValue.map((track) => { - if (!isRecord(track)) return track; - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) return track; - - const migratedElements = elementsValue.map((element) => { - if (!isRecord(element)) return element; - if (element.type !== "sticker") return element; - if ( - typeof element.intrinsicWidth === "number" && - typeof element.intrinsicHeight === "number" - ) { - return element; - } - return { - ...element, - intrinsicWidth: STICKER_INTRINSIC_SIZE_FALLBACK, - intrinsicHeight: STICKER_INTRINSIC_SIZE_FALLBACK, - }; - }); - - return { ...track, elements: migratedElements }; - }); - - return { ...scene, tracks: migratedTracks }; - }); - - return { ...project, scenes: migratedScenes }; -} +import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/constants/sticker-constants"; +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV14ToV15({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 15) { + return { project, skipped: true, reason: "already v15" }; + } + + const migratedProject = backfillStickerIntrinsicDimensions({ project }); + + return { + project: { ...migratedProject, version: 15 }, + skipped: false, + }; +} + +function backfillStickerIntrinsicDimensions({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) return project; + + const migratedScenes = scenesValue.map((scene) => { + if (!isRecord(scene)) return scene; + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) return scene; + + const migratedTracks = tracksValue.map((track) => { + if (!isRecord(track)) return track; + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) return track; + + const migratedElements = elementsValue.map((element) => { + if (!isRecord(element)) return element; + if (element.type !== "sticker") return element; + if ( + typeof element.intrinsicWidth === "number" && + typeof element.intrinsicHeight === "number" + ) { + return element; + } + return { + ...element, + intrinsicWidth: STICKER_INTRINSIC_SIZE_FALLBACK, + intrinsicHeight: STICKER_INTRINSIC_SIZE_FALLBACK, + }; + }); + + return { ...track, elements: migratedElements }; + }); + + return { ...scene, tracks: migratedTracks }; + }); + + return { ...project, scenes: migratedScenes }; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v15-to-v16.ts b/apps/web/src/services/storage/migrations/transformers/v15-to-v16.ts index 15e2f3bb..746938d0 100644 --- a/apps/web/src/services/storage/migrations/transformers/v15-to-v16.ts +++ b/apps/web/src/services/storage/migrations/transformers/v15-to-v16.ts @@ -1,60 +1,60 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV15ToV16({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - if (!getProjectId({ project })) { - return { project, skipped: true, reason: "no project id" }; - } - - if (typeof project.version === "number" && project.version >= 16) { - return { project, skipped: true, reason: "already v16" }; - } - - return { - project: { - ...renameStickerTracksToGraphic({ project }), - version: 16, - }, - skipped: false, - }; -} - -function renameStickerTracksToGraphic({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) { - return project; - } - - const migratedScenes = scenesValue.map((scene) => { - if (!isRecord(scene) || !Array.isArray(scene.tracks)) { - return scene; - } - - return { - ...scene, - tracks: scene.tracks.map((track) => { - if (!isRecord(track) || track.type !== "sticker") { - return track; - } - - return { - ...track, - type: "graphic", - }; - }), - }; - }); - - return { - ...project, - scenes: migratedScenes, - }; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV15ToV16({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 16) { + return { project, skipped: true, reason: "already v16" }; + } + + return { + project: { + ...renameStickerTracksToGraphic({ project }), + version: 16, + }, + skipped: false, + }; +} + +function renameStickerTracksToGraphic({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) { + return project; + } + + const migratedScenes = scenesValue.map((scene) => { + if (!isRecord(scene) || !Array.isArray(scene.tracks)) { + return scene; + } + + return { + ...scene, + tracks: scene.tracks.map((track) => { + if (!isRecord(track) || track.type !== "sticker") { + return track; + } + + return { + ...track, + type: "graphic", + }; + }), + }; + }); + + return { + ...project, + scenes: migratedScenes, + }; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v16-to-v17.ts b/apps/web/src/services/storage/migrations/transformers/v16-to-v17.ts index 7fec70e9..00474188 100644 --- a/apps/web/src/services/storage/migrations/transformers/v16-to-v17.ts +++ b/apps/web/src/services/storage/migrations/transformers/v16-to-v17.ts @@ -1,107 +1,107 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV16ToV17({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - if (!getProjectId({ project })) { - return { project, skipped: true, reason: "no project id" }; - } - - if (typeof project.version === "number" && project.version >= 17) { - return { project, skipped: true, reason: "already v17" }; - } - - return { - project: { - ...backfillMaskStrokeAlign({ project }), - version: 17, - }, - skipped: false, - }; -} - -function backfillMaskStrokeAlign({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) { - return project; - } - - const migratedScenes = scenesValue.map((scene) => { - if (!isRecord(scene)) { - return scene; - } - - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) { - return scene; - } - - const migratedTracks = tracksValue.map((track) => { - if (!isRecord(track)) { - return track; - } - - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) { - return track; - } - - const migratedElements = elementsValue.map((element) => { - if (!isRecord(element)) { - return element; - } - - const masksValue = element.masks; - if (!Array.isArray(masksValue)) { - return element; - } - - const migratedMasks = masksValue.map((mask) => { - if (!isRecord(mask)) { - return mask; - } - - const paramsValue = mask.params; - if (!isRecord(paramsValue) || typeof paramsValue.strokeAlign === "string") { - return mask; - } - - return { - ...mask, - params: { - ...paramsValue, - strokeAlign: "center", - }, - }; - }); - - return { - ...element, - masks: migratedMasks, - }; - }); - - return { - ...track, - elements: migratedElements, - }; - }); - - return { - ...scene, - tracks: migratedTracks, - }; - }); - - return { - ...project, - scenes: migratedScenes, - }; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV16ToV17({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 17) { + return { project, skipped: true, reason: "already v17" }; + } + + return { + project: { + ...backfillMaskStrokeAlign({ project }), + version: 17, + }, + skipped: false, + }; +} + +function backfillMaskStrokeAlign({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) { + return project; + } + + const migratedScenes = scenesValue.map((scene) => { + if (!isRecord(scene)) { + return scene; + } + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) { + return scene; + } + + const migratedTracks = tracksValue.map((track) => { + if (!isRecord(track)) { + return track; + } + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) { + return track; + } + + const migratedElements = elementsValue.map((element) => { + if (!isRecord(element)) { + return element; + } + + const masksValue = element.masks; + if (!Array.isArray(masksValue)) { + return element; + } + + const migratedMasks = masksValue.map((mask) => { + if (!isRecord(mask)) { + return mask; + } + + const paramsValue = mask.params; + if (!isRecord(paramsValue) || typeof paramsValue.strokeAlign === "string") { + return mask; + } + + return { + ...mask, + params: { + ...paramsValue, + strokeAlign: "center", + }, + }; + }); + + return { + ...element, + masks: migratedMasks, + }; + }); + + return { + ...track, + elements: migratedElements, + }; + }); + + return { + ...scene, + tracks: migratedTracks, + }; + }); + + return { + ...project, + scenes: migratedScenes, + }; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts index 31a60121..5dcf6feb 100644 --- a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts +++ b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts @@ -1,105 +1,105 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { VOLUME_DB_MIN } from "@/lib/timeline/audio-constants"; -import { clampDb } from "@/lib/timeline/audio-state"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV17ToV18({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - if (!getProjectId({ project })) { - return { project, skipped: true, reason: "no project id" }; - } - - if (typeof project.version === "number" && project.version >= 18) { - return { project, skipped: true, reason: "already v18" }; - } - - return { - project: { - ...migrateElementVolumes({ project }), - version: 18, - }, - skipped: false, - }; -} - -function migrateElementVolumes({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) { - return project; - } - - const migratedScenes = scenesValue.map((scene) => { - if (!isRecord(scene)) { - return scene; - } - - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) { - return scene; - } - - const migratedTracks = tracksValue.map((track) => { - if (!isRecord(track)) { - return track; - } - - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) { - return track; - } - - const migratedElements = elementsValue.map((element) => { - if (!isRecord(element)) { - return element; - } - - if (element.type !== "audio" && element.type !== "video") { - return element; - } - - const legacyVolume = element.volume; - return { - ...element, - volume: - typeof legacyVolume === "number" - ? linearGainToDb(legacyVolume) - : 0, - }; - }); - - return { - ...track, - elements: migratedElements, - }; - }); - - return { - ...scene, - tracks: migratedTracks, - }; - }); - - return { - ...project, - scenes: migratedScenes, - }; -} - -function linearGainToDb(gain: number): number { - if (!Number.isFinite(gain)) { - return 0; - } - - if (gain <= 0) { - return VOLUME_DB_MIN; - } - - return clampDb(20 * Math.log10(gain)); -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { VOLUME_DB_MIN } from "@/lib/timeline/audio-constants"; +import { clampDb } from "@/lib/timeline/audio-state"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV17ToV18({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 18) { + return { project, skipped: true, reason: "already v18" }; + } + + return { + project: { + ...migrateElementVolumes({ project }), + version: 18, + }, + skipped: false, + }; +} + +function migrateElementVolumes({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) { + return project; + } + + const migratedScenes = scenesValue.map((scene) => { + if (!isRecord(scene)) { + return scene; + } + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) { + return scene; + } + + const migratedTracks = tracksValue.map((track) => { + if (!isRecord(track)) { + return track; + } + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) { + return track; + } + + const migratedElements = elementsValue.map((element) => { + if (!isRecord(element)) { + return element; + } + + if (element.type !== "audio" && element.type !== "video") { + return element; + } + + const legacyVolume = element.volume; + return { + ...element, + volume: + typeof legacyVolume === "number" + ? linearGainToDb(legacyVolume) + : 0, + }; + }); + + return { + ...track, + elements: migratedElements, + }; + }); + + return { + ...scene, + tracks: migratedTracks, + }; + }); + + return { + ...project, + scenes: migratedScenes, + }; +} + +function linearGainToDb(gain: number): number { + if (!Number.isFinite(gain)) { + return 0; + } + + if (gain <= 0) { + return VOLUME_DB_MIN; + } + + return clampDb(20 * Math.log10(gain)); +} diff --git a/apps/web/src/services/storage/migrations/transformers/v18-to-v19.ts b/apps/web/src/services/storage/migrations/transformers/v18-to-v19.ts index 1946b829..3afa5203 100644 --- a/apps/web/src/services/storage/migrations/transformers/v18-to-v19.ts +++ b/apps/web/src/services/storage/migrations/transformers/v18-to-v19.ts @@ -1,46 +1,46 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV18ToV19({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - if (!getProjectId({ project })) { - return { project, skipped: true, reason: "no project id" }; - } - - if (typeof project.version === "number" && project.version >= 19) { - return { project, skipped: true, reason: "already v19" }; - } - - return { - project: { - ...migrateCanvasSizeSettings({ project }), - version: 19, - }, - skipped: false, - }; -} - -function migrateCanvasSizeSettings({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const settingsValue = project.settings; - if (!isRecord(settingsValue)) { - return project; - } - - const migratedSettings: Record = { - ...settingsValue, - canvasSizeMode: "preset", - lastCustomCanvasSize: null, - }; - - return { - ...project, - settings: migratedSettings, - }; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV18ToV19({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 19) { + return { project, skipped: true, reason: "already v19" }; + } + + return { + project: { + ...migrateCanvasSizeSettings({ project }), + version: 19, + }, + skipped: false, + }; +} + +function migrateCanvasSizeSettings({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const settingsValue = project.settings; + if (!isRecord(settingsValue)) { + return project; + } + + const migratedSettings: Record = { + ...settingsValue, + canvasSizeMode: "preset", + lastCustomCanvasSize: null, + }; + + return { + ...project, + settings: migratedSettings, + }; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v3-to-v4.ts b/apps/web/src/services/storage/migrations/transformers/v3-to-v4.ts index d6e1efee..84d97fd8 100644 --- a/apps/web/src/services/storage/migrations/transformers/v3-to-v4.ts +++ b/apps/web/src/services/storage/migrations/transformers/v3-to-v4.ts @@ -1,189 +1,189 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -const LEGACY_FONT_WEIGHT_MAP = { - normal: "400", - bold: "700", -} as const; - -const VALID_NUMERIC_FONT_WEIGHTS = new Set([ - "100", - "200", - "300", - "400", - "500", - "600", - "700", - "800", - "900", -]); - -export function transformProjectV3ToV4({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - const projectId = getProjectId({ project }); - if (!projectId) { - return { project, skipped: true, reason: "no project id" }; - } - - if (isV4Project({ project })) { - return { project, skipped: true, reason: "already v4" }; - } - - const migratedProject = normalizeProjectTextFontWeights({ project }); - - return { - project: { - ...migratedProject, - version: 4, - }, - skipped: false, - }; -} - -function normalizeProjectTextFontWeights({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) { - return project; - } - - let hasSceneChanges = false; - const normalizedScenes = scenesValue.map((scene) => { - const normalizedScene = normalizeSceneTextFontWeights({ scene }); - if (normalizedScene !== scene) { - hasSceneChanges = true; - } - return normalizedScene; - }); - - if (!hasSceneChanges) { - return project; - } - - return { - ...project, - scenes: normalizedScenes, - }; -} - -function normalizeSceneTextFontWeights({ scene }: { scene: unknown }): unknown { - if (!isRecord(scene)) { - return scene; - } - - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) { - return scene; - } - - let hasTrackChanges = false; - const normalizedTracks = tracksValue.map((track) => { - const normalizedTrack = normalizeTrackTextFontWeights({ track }); - if (normalizedTrack !== track) { - hasTrackChanges = true; - } - return normalizedTrack; - }); - - if (!hasTrackChanges) { - return scene; - } - - return { - ...scene, - tracks: normalizedTracks, - }; -} - -function normalizeTrackTextFontWeights({ track }: { track: unknown }): unknown { - if (!isRecord(track)) { - return track; - } - - if (track.type !== "text") { - return track; - } - - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) { - return track; - } - - let hasElementChanges = false; - const normalizedElements = elementsValue.map((element) => { - const normalizedElement = normalizeTextElementFontWeight({ element }); - if (normalizedElement !== element) { - hasElementChanges = true; - } - return normalizedElement; - }); - - if (!hasElementChanges) { - return track; - } - - return { - ...track, - elements: normalizedElements, - }; -} - -function normalizeTextElementFontWeight({ - element, -}: { - element: unknown; -}): unknown { - if (!isRecord(element) || element.type !== "text") { - return element; - } - - const normalizedWeight = normalizeFontWeight({ value: element.fontWeight }); - if (normalizedWeight === element.fontWeight) { - return element; - } - - return { - ...element, - fontWeight: normalizedWeight, - }; -} - -function normalizeFontWeight({ value }: { value: unknown }): unknown { - if (typeof value === "number") { - const numericWeight = String(value); - if (VALID_NUMERIC_FONT_WEIGHTS.has(numericWeight)) { - return numericWeight; - } - return value; - } - - if (typeof value !== "string") { - return value; - } - - const normalized = value.trim().toLowerCase(); - if (normalized in LEGACY_FONT_WEIGHT_MAP) { - return LEGACY_FONT_WEIGHT_MAP[ - normalized as keyof typeof LEGACY_FONT_WEIGHT_MAP - ]; - } - - if (VALID_NUMERIC_FONT_WEIGHTS.has(normalized)) { - return normalized; - } - - return value; -} - -export { getProjectId } from "./utils"; - -function isV4Project({ project }: { project: ProjectRecord }): boolean { - const versionValue = project.version; - return typeof versionValue === "number" && versionValue >= 4; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +const LEGACY_FONT_WEIGHT_MAP = { + normal: "400", + bold: "700", +} as const; + +const VALID_NUMERIC_FONT_WEIGHTS = new Set([ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", +]); + +export function transformProjectV3ToV4({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + const projectId = getProjectId({ project }); + if (!projectId) { + return { project, skipped: true, reason: "no project id" }; + } + + if (isV4Project({ project })) { + return { project, skipped: true, reason: "already v4" }; + } + + const migratedProject = normalizeProjectTextFontWeights({ project }); + + return { + project: { + ...migratedProject, + version: 4, + }, + skipped: false, + }; +} + +function normalizeProjectTextFontWeights({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) { + return project; + } + + let hasSceneChanges = false; + const normalizedScenes = scenesValue.map((scene) => { + const normalizedScene = normalizeSceneTextFontWeights({ scene }); + if (normalizedScene !== scene) { + hasSceneChanges = true; + } + return normalizedScene; + }); + + if (!hasSceneChanges) { + return project; + } + + return { + ...project, + scenes: normalizedScenes, + }; +} + +function normalizeSceneTextFontWeights({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) { + return scene; + } + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) { + return scene; + } + + let hasTrackChanges = false; + const normalizedTracks = tracksValue.map((track) => { + const normalizedTrack = normalizeTrackTextFontWeights({ track }); + if (normalizedTrack !== track) { + hasTrackChanges = true; + } + return normalizedTrack; + }); + + if (!hasTrackChanges) { + return scene; + } + + return { + ...scene, + tracks: normalizedTracks, + }; +} + +function normalizeTrackTextFontWeights({ track }: { track: unknown }): unknown { + if (!isRecord(track)) { + return track; + } + + if (track.type !== "text") { + return track; + } + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) { + return track; + } + + let hasElementChanges = false; + const normalizedElements = elementsValue.map((element) => { + const normalizedElement = normalizeTextElementFontWeight({ element }); + if (normalizedElement !== element) { + hasElementChanges = true; + } + return normalizedElement; + }); + + if (!hasElementChanges) { + return track; + } + + return { + ...track, + elements: normalizedElements, + }; +} + +function normalizeTextElementFontWeight({ + element, +}: { + element: unknown; +}): unknown { + if (!isRecord(element) || element.type !== "text") { + return element; + } + + const normalizedWeight = normalizeFontWeight({ value: element.fontWeight }); + if (normalizedWeight === element.fontWeight) { + return element; + } + + return { + ...element, + fontWeight: normalizedWeight, + }; +} + +function normalizeFontWeight({ value }: { value: unknown }): unknown { + if (typeof value === "number") { + const numericWeight = String(value); + if (VALID_NUMERIC_FONT_WEIGHTS.has(numericWeight)) { + return numericWeight; + } + return value; + } + + if (typeof value !== "string") { + return value; + } + + const normalized = value.trim().toLowerCase(); + if (normalized in LEGACY_FONT_WEIGHT_MAP) { + return LEGACY_FONT_WEIGHT_MAP[ + normalized as keyof typeof LEGACY_FONT_WEIGHT_MAP + ]; + } + + if (VALID_NUMERIC_FONT_WEIGHTS.has(normalized)) { + return normalized; + } + + return value; +} + +export { getProjectId } from "./utils"; + +function isV4Project({ project }: { project: ProjectRecord }): boolean { + const versionValue = project.version; + return typeof versionValue === "number" && versionValue >= 4; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v4-to-v5.ts b/apps/web/src/services/storage/migrations/transformers/v4-to-v5.ts index 971e3510..243b130c 100644 --- a/apps/web/src/services/storage/migrations/transformers/v4-to-v5.ts +++ b/apps/web/src/services/storage/migrations/transformers/v4-to-v5.ts @@ -1,177 +1,177 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -const KNOWN_STICKER_PROVIDER_IDS = new Set([ - "icons", - "emoji", - "flags", - "shapes", -]); - -export function transformProjectV4ToV5({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - const projectId = getProjectId({ project }); - if (!projectId) { - return { project, skipped: true, reason: "no project id" }; - } - - if (isV5Project({ project })) { - return { project, skipped: true, reason: "already v5" }; - } - - const migratedProject = migrateProjectStickerElements({ project }); - - return { - project: { - ...migratedProject, - version: 5, - }, - skipped: false, - }; -} - -function migrateProjectStickerElements({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) { - return project; - } - - let hasSceneChanges = false; - const migratedScenes = scenesValue.map((scene) => { - const migratedScene = migrateSceneStickerElements({ scene }); - if (migratedScene !== scene) { - hasSceneChanges = true; - } - return migratedScene; - }); - - if (!hasSceneChanges) { - return project; - } - - return { - ...project, - scenes: migratedScenes, - }; -} - -function migrateSceneStickerElements({ scene }: { scene: unknown }): unknown { - if (!isRecord(scene)) { - return scene; - } - - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) { - return scene; - } - - let hasTrackChanges = false; - const migratedTracks = tracksValue.map((track) => { - const migratedTrack = migrateTrackStickerElements({ track }); - if (migratedTrack !== track) { - hasTrackChanges = true; - } - return migratedTrack; - }); - - if (!hasTrackChanges) { - return scene; - } - - return { - ...scene, - tracks: migratedTracks, - }; -} - -function migrateTrackStickerElements({ track }: { track: unknown }): unknown { - if (!isRecord(track)) { - return track; - } - - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) { - return track; - } - - let hasElementChanges = false; - const migratedElements = elementsValue.map((element) => { - const migratedElement = migrateStickerElement({ element }); - if (migratedElement !== element) { - hasElementChanges = true; - } - return migratedElement; - }); - - if (!hasElementChanges) { - return track; - } - - return { - ...track, - elements: migratedElements, - }; -} - -function migrateStickerElement({ element }: { element: unknown }): unknown { - if (!isRecord(element) || element.type !== "sticker") { - return element; - } - - const existingStickerId = - typeof element.stickerId === "string" ? element.stickerId : null; - const legacyIconName = - typeof element.iconName === "string" ? element.iconName : null; - - const normalizedStickerId = - normalizeStickerId({ - value: existingStickerId ?? legacyIconName, - }) ?? null; - - const hasLegacyIconName = "iconName" in element; - const hasLegacyColor = "color" in element; - const shouldUpdateStickerId = normalizedStickerId !== existingStickerId; - - if (!hasLegacyIconName && !hasLegacyColor && !shouldUpdateStickerId) { - return element; - } - - const { - iconName: _legacyIconName, - color: _legacyColor, - ...remaining - } = element; - return normalizedStickerId - ? { ...remaining, stickerId: normalizedStickerId } - : remaining; -} - -function normalizeStickerId({ value }: { value: unknown }): string | null { - if (typeof value !== "string" || value.length === 0) { - return null; - } - - const separatorIndex = value.indexOf(":"); - if (separatorIndex === -1) { - return `icons:${value}`; - } - - const maybeProvider = value.slice(0, separatorIndex); - if (KNOWN_STICKER_PROVIDER_IDS.has(maybeProvider)) { - return value; - } - - return `icons:${value}`; -} - -function isV5Project({ project }: { project: ProjectRecord }): boolean { - const versionValue = project.version; - return typeof versionValue === "number" && versionValue >= 5; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +const KNOWN_STICKER_PROVIDER_IDS = new Set([ + "icons", + "emoji", + "flags", + "shapes", +]); + +export function transformProjectV4ToV5({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + const projectId = getProjectId({ project }); + if (!projectId) { + return { project, skipped: true, reason: "no project id" }; + } + + if (isV5Project({ project })) { + return { project, skipped: true, reason: "already v5" }; + } + + const migratedProject = migrateProjectStickerElements({ project }); + + return { + project: { + ...migratedProject, + version: 5, + }, + skipped: false, + }; +} + +function migrateProjectStickerElements({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) { + return project; + } + + let hasSceneChanges = false; + const migratedScenes = scenesValue.map((scene) => { + const migratedScene = migrateSceneStickerElements({ scene }); + if (migratedScene !== scene) { + hasSceneChanges = true; + } + return migratedScene; + }); + + if (!hasSceneChanges) { + return project; + } + + return { + ...project, + scenes: migratedScenes, + }; +} + +function migrateSceneStickerElements({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) { + return scene; + } + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) { + return scene; + } + + let hasTrackChanges = false; + const migratedTracks = tracksValue.map((track) => { + const migratedTrack = migrateTrackStickerElements({ track }); + if (migratedTrack !== track) { + hasTrackChanges = true; + } + return migratedTrack; + }); + + if (!hasTrackChanges) { + return scene; + } + + return { + ...scene, + tracks: migratedTracks, + }; +} + +function migrateTrackStickerElements({ track }: { track: unknown }): unknown { + if (!isRecord(track)) { + return track; + } + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) { + return track; + } + + let hasElementChanges = false; + const migratedElements = elementsValue.map((element) => { + const migratedElement = migrateStickerElement({ element }); + if (migratedElement !== element) { + hasElementChanges = true; + } + return migratedElement; + }); + + if (!hasElementChanges) { + return track; + } + + return { + ...track, + elements: migratedElements, + }; +} + +function migrateStickerElement({ element }: { element: unknown }): unknown { + if (!isRecord(element) || element.type !== "sticker") { + return element; + } + + const existingStickerId = + typeof element.stickerId === "string" ? element.stickerId : null; + const legacyIconName = + typeof element.iconName === "string" ? element.iconName : null; + + const normalizedStickerId = + normalizeStickerId({ + value: existingStickerId ?? legacyIconName, + }) ?? null; + + const hasLegacyIconName = "iconName" in element; + const hasLegacyColor = "color" in element; + const shouldUpdateStickerId = normalizedStickerId !== existingStickerId; + + if (!hasLegacyIconName && !hasLegacyColor && !shouldUpdateStickerId) { + return element; + } + + const { + iconName: _legacyIconName, + color: _legacyColor, + ...remaining + } = element; + return normalizedStickerId + ? { ...remaining, stickerId: normalizedStickerId } + : remaining; +} + +function normalizeStickerId({ value }: { value: unknown }): string | null { + if (typeof value !== "string" || value.length === 0) { + return null; + } + + const separatorIndex = value.indexOf(":"); + if (separatorIndex === -1) { + return `icons:${value}`; + } + + const maybeProvider = value.slice(0, separatorIndex); + if (KNOWN_STICKER_PROVIDER_IDS.has(maybeProvider)) { + return value; + } + + return `icons:${value}`; +} + +function isV5Project({ project }: { project: ProjectRecord }): boolean { + const versionValue = project.version; + return typeof versionValue === "number" && versionValue >= 5; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v5-to-v6.ts b/apps/web/src/services/storage/migrations/transformers/v5-to-v6.ts index ca855149..f3890f24 100644 --- a/apps/web/src/services/storage/migrations/transformers/v5-to-v6.ts +++ b/apps/web/src/services/storage/migrations/transformers/v5-to-v6.ts @@ -1,88 +1,88 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV5ToV6({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - const projectId = getProjectId({ project }); - if (!projectId) { - return { project, skipped: true, reason: "no project id" }; - } - - if (isV6Project({ project })) { - return { project, skipped: true, reason: "already v6" }; - } - - const migratedProject = migrateProjectBookmarks({ project }); - - return { - project: { - ...migratedProject, - version: 6, - }, - skipped: false, - }; -} - -function migrateProjectBookmarks({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) { - return project; - } - - let hasSceneChanges = false; - const migratedScenes = scenesValue.map((scene) => { - const migratedScene = migrateSceneBookmarks({ scene }); - if (migratedScene !== scene) { - hasSceneChanges = true; - } - return migratedScene; - }); - - if (!hasSceneChanges) { - return project; - } - - return { - ...project, - scenes: migratedScenes, - }; -} - -function migrateSceneBookmarks({ scene }: { scene: unknown }): unknown { - if (!isRecord(scene)) { - return scene; - } - - const bookmarksValue = scene.bookmarks; - if (!Array.isArray(bookmarksValue)) { - return scene; - } - - const needsMigration = bookmarksValue.some( - (bookmark) => typeof bookmark === "number", - ); - if (!needsMigration) { - return scene; - } - - const migratedBookmarks = bookmarksValue.map((bookmark) => - typeof bookmark === "number" ? { time: bookmark } : bookmark, - ); - - return { - ...scene, - bookmarks: migratedBookmarks, - }; -} - -function isV6Project({ project }: { project: ProjectRecord }): boolean { - const versionValue = project.version; - return typeof versionValue === "number" && versionValue >= 6; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV5ToV6({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + const projectId = getProjectId({ project }); + if (!projectId) { + return { project, skipped: true, reason: "no project id" }; + } + + if (isV6Project({ project })) { + return { project, skipped: true, reason: "already v6" }; + } + + const migratedProject = migrateProjectBookmarks({ project }); + + return { + project: { + ...migratedProject, + version: 6, + }, + skipped: false, + }; +} + +function migrateProjectBookmarks({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) { + return project; + } + + let hasSceneChanges = false; + const migratedScenes = scenesValue.map((scene) => { + const migratedScene = migrateSceneBookmarks({ scene }); + if (migratedScene !== scene) { + hasSceneChanges = true; + } + return migratedScene; + }); + + if (!hasSceneChanges) { + return project; + } + + return { + ...project, + scenes: migratedScenes, + }; +} + +function migrateSceneBookmarks({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) { + return scene; + } + + const bookmarksValue = scene.bookmarks; + if (!Array.isArray(bookmarksValue)) { + return scene; + } + + const needsMigration = bookmarksValue.some( + (bookmark) => typeof bookmark === "number", + ); + if (!needsMigration) { + return scene; + } + + const migratedBookmarks = bookmarksValue.map((bookmark) => + typeof bookmark === "number" ? { time: bookmark } : bookmark, + ); + + return { + ...scene, + bookmarks: migratedBookmarks, + }; +} + +function isV6Project({ project }: { project: ProjectRecord }): boolean { + const versionValue = project.version; + return typeof versionValue === "number" && versionValue >= 6; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v6-to-v7.ts b/apps/web/src/services/storage/migrations/transformers/v6-to-v7.ts index 6f4848d4..fe3b75fd 100644 --- a/apps/web/src/services/storage/migrations/transformers/v6-to-v7.ts +++ b/apps/web/src/services/storage/migrations/transformers/v6-to-v7.ts @@ -1,106 +1,106 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV6ToV7({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - const projectId = getProjectId({ project }); - if (!projectId) { - return { project, skipped: true, reason: "no project id" }; - } - - if (isV7Project({ project })) { - return { project, skipped: true, reason: "already v7" }; - } - - const migratedProject = migrateProjectTextElements({ project }); - - return { - project: { ...migratedProject, version: 7 }, - skipped: false, - }; -} - -function migrateProjectTextElements({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) return project; - - let hasChanges = false; - const migratedScenes = scenesValue.map((scene) => { - const migrated = migrateSceneTextElements({ scene }); - if (migrated !== scene) hasChanges = true; - return migrated; - }); - - if (!hasChanges) return project; - return { ...project, scenes: migratedScenes }; -} - -function migrateSceneTextElements({ scene }: { scene: unknown }): unknown { - if (!isRecord(scene)) return scene; - - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) return scene; - - let hasChanges = false; - const migratedTracks = tracksValue.map((track) => { - const migrated = migrateTrackTextElements({ track }); - if (migrated !== track) hasChanges = true; - return migrated; - }); - - if (!hasChanges) return scene; - return { ...scene, tracks: migratedTracks }; -} - -function migrateTrackTextElements({ track }: { track: unknown }): unknown { - if (!isRecord(track)) return track; - - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) return track; - - let hasChanges = false; - const migratedElements = elementsValue.map((element) => { - const migrated = migrateTextElement({ element }); - if (migrated !== element) hasChanges = true; - return migrated; - }); - - if (!hasChanges) return track; - return { ...track, elements: migratedElements }; -} - -function migrateTextElement({ element }: { element: unknown }): unknown { - if (!isRecord(element)) return element; - if (element.type !== "text") return element; - if (isRecord(element.background)) return element; - - const backgroundColor = - typeof element.backgroundColor === "string" - ? element.backgroundColor - : "transparent"; - - const { backgroundColor: _removed, ...rest } = element; - - return { - ...rest, - background: { - color: backgroundColor, - cornerRadius: 0, - paddingX: 8, - paddingY: 4, - offsetX: 0, - offsetY: 0, - }, - }; -} - -function isV7Project({ project }: { project: ProjectRecord }): boolean { - return typeof project.version === "number" && project.version >= 7; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV6ToV7({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + const projectId = getProjectId({ project }); + if (!projectId) { + return { project, skipped: true, reason: "no project id" }; + } + + if (isV7Project({ project })) { + return { project, skipped: true, reason: "already v7" }; + } + + const migratedProject = migrateProjectTextElements({ project }); + + return { + project: { ...migratedProject, version: 7 }, + skipped: false, + }; +} + +function migrateProjectTextElements({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) return project; + + let hasChanges = false; + const migratedScenes = scenesValue.map((scene) => { + const migrated = migrateSceneTextElements({ scene }); + if (migrated !== scene) hasChanges = true; + return migrated; + }); + + if (!hasChanges) return project; + return { ...project, scenes: migratedScenes }; +} + +function migrateSceneTextElements({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) return scene; + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) return scene; + + let hasChanges = false; + const migratedTracks = tracksValue.map((track) => { + const migrated = migrateTrackTextElements({ track }); + if (migrated !== track) hasChanges = true; + return migrated; + }); + + if (!hasChanges) return scene; + return { ...scene, tracks: migratedTracks }; +} + +function migrateTrackTextElements({ track }: { track: unknown }): unknown { + if (!isRecord(track)) return track; + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) return track; + + let hasChanges = false; + const migratedElements = elementsValue.map((element) => { + const migrated = migrateTextElement({ element }); + if (migrated !== element) hasChanges = true; + return migrated; + }); + + if (!hasChanges) return track; + return { ...track, elements: migratedElements }; +} + +function migrateTextElement({ element }: { element: unknown }): unknown { + if (!isRecord(element)) return element; + if (element.type !== "text") return element; + if (isRecord(element.background)) return element; + + const backgroundColor = + typeof element.backgroundColor === "string" + ? element.backgroundColor + : "transparent"; + + const { backgroundColor: _removed, ...rest } = element; + + return { + ...rest, + background: { + color: backgroundColor, + cornerRadius: 0, + paddingX: 8, + paddingY: 4, + offsetX: 0, + offsetY: 0, + }, + }; +} + +function isV7Project({ project }: { project: ProjectRecord }): boolean { + return typeof project.version === "number" && project.version >= 7; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v7-to-v8.ts b/apps/web/src/services/storage/migrations/transformers/v7-to-v8.ts index 7393f776..e592cde9 100644 --- a/apps/web/src/services/storage/migrations/transformers/v7-to-v8.ts +++ b/apps/web/src/services/storage/migrations/transformers/v7-to-v8.ts @@ -1,96 +1,96 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV7ToV8({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - const projectId = getProjectId({ project }); - if (!projectId) { - return { project, skipped: true, reason: "no project id" }; - } - - if (isV8Project({ project })) { - return { project, skipped: true, reason: "already v8" }; - } - - const migratedProject = migrateProjectElements({ project }); - - return { - project: { ...migratedProject, version: 8 }, - skipped: false, - }; -} - -function migrateProjectElements({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) return project; - - let hasChanges = false; - const migratedScenes = scenesValue.map((scene) => { - const migrated = migrateSceneElements({ scene }); - if (migrated !== scene) hasChanges = true; - return migrated; - }); - - if (!hasChanges) return project; - return { ...project, scenes: migratedScenes }; -} - -function migrateSceneElements({ scene }: { scene: unknown }): unknown { - if (!isRecord(scene)) return scene; - - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) return scene; - - let hasChanges = false; - const migratedTracks = tracksValue.map((track) => { - const migrated = migrateTrackElements({ track }); - if (migrated !== track) hasChanges = true; - return migrated; - }); - - if (!hasChanges) return scene; - return { ...scene, tracks: migratedTracks }; -} - -function migrateTrackElements({ track }: { track: unknown }): unknown { - if (!isRecord(track)) return track; - - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) return track; - - let hasChanges = false; - const migratedElements = elementsValue.map((element) => { - const migrated = migrateElement({ element }); - if (migrated !== element) hasChanges = true; - return migrated; - }); - - if (!hasChanges) return track; - return { ...track, elements: migratedElements }; -} - -function migrateElement({ element }: { element: unknown }): unknown { - if (!isRecord(element)) return element; - if (element.type !== "video" && element.type !== "audio") return element; - if (typeof element.sourceDuration === "number") return element; - - const trimStart = typeof element.trimStart === "number" ? element.trimStart : 0; - const duration = typeof element.duration === "number" ? element.duration : 0; - const trimEnd = typeof element.trimEnd === "number" ? element.trimEnd : 0; - - return { - ...element, - sourceDuration: trimStart + duration + trimEnd, - }; -} - -function isV8Project({ project }: { project: ProjectRecord }): boolean { - return typeof project.version === "number" && project.version >= 8; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV7ToV8({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + const projectId = getProjectId({ project }); + if (!projectId) { + return { project, skipped: true, reason: "no project id" }; + } + + if (isV8Project({ project })) { + return { project, skipped: true, reason: "already v8" }; + } + + const migratedProject = migrateProjectElements({ project }); + + return { + project: { ...migratedProject, version: 8 }, + skipped: false, + }; +} + +function migrateProjectElements({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) return project; + + let hasChanges = false; + const migratedScenes = scenesValue.map((scene) => { + const migrated = migrateSceneElements({ scene }); + if (migrated !== scene) hasChanges = true; + return migrated; + }); + + if (!hasChanges) return project; + return { ...project, scenes: migratedScenes }; +} + +function migrateSceneElements({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) return scene; + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) return scene; + + let hasChanges = false; + const migratedTracks = tracksValue.map((track) => { + const migrated = migrateTrackElements({ track }); + if (migrated !== track) hasChanges = true; + return migrated; + }); + + if (!hasChanges) return scene; + return { ...scene, tracks: migratedTracks }; +} + +function migrateTrackElements({ track }: { track: unknown }): unknown { + if (!isRecord(track)) return track; + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) return track; + + let hasChanges = false; + const migratedElements = elementsValue.map((element) => { + const migrated = migrateElement({ element }); + if (migrated !== element) hasChanges = true; + return migrated; + }); + + if (!hasChanges) return track; + return { ...track, elements: migratedElements }; +} + +function migrateElement({ element }: { element: unknown }): unknown { + if (!isRecord(element)) return element; + if (element.type !== "video" && element.type !== "audio") return element; + if (typeof element.sourceDuration === "number") return element; + + const trimStart = typeof element.trimStart === "number" ? element.trimStart : 0; + const duration = typeof element.duration === "number" ? element.duration : 0; + const trimEnd = typeof element.trimEnd === "number" ? element.trimEnd : 0; + + return { + ...element, + sourceDuration: trimStart + duration + trimEnd, + }; +} + +function isV8Project({ project }: { project: ProjectRecord }): boolean { + return typeof project.version === "number" && project.version >= 8; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v8-to-v9.ts b/apps/web/src/services/storage/migrations/transformers/v8-to-v9.ts index 1dc895bc..628eebb6 100644 --- a/apps/web/src/services/storage/migrations/transformers/v8-to-v9.ts +++ b/apps/web/src/services/storage/migrations/transformers/v8-to-v9.ts @@ -1,99 +1,99 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId, isRecord } from "./utils"; - -export function transformProjectV8ToV9({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - const projectId = getProjectId({ project }); - if (!projectId) { - return { project, skipped: true, reason: "no project id" }; - } - - if (isV9Project({ project })) { - return { project, skipped: true, reason: "already v9" }; - } - - const migratedProject = migrateProjectTextElements({ project }); - - return { - project: { ...migratedProject, version: 9 }, - skipped: false, - }; -} - -function migrateProjectTextElements({ - project, -}: { - project: ProjectRecord; -}): ProjectRecord { - const scenesValue = project.scenes; - if (!Array.isArray(scenesValue)) return project; - - let hasChanges = false; - const migratedScenes = scenesValue.map((scene) => { - const migrated = migrateSceneTextElements({ scene }); - if (migrated !== scene) hasChanges = true; - return migrated; - }); - - if (!hasChanges) return project; - return { ...project, scenes: migratedScenes }; -} - -function migrateSceneTextElements({ scene }: { scene: unknown }): unknown { - if (!isRecord(scene)) return scene; - - const tracksValue = scene.tracks; - if (!Array.isArray(tracksValue)) return scene; - - let hasChanges = false; - const migratedTracks = tracksValue.map((track) => { - const migrated = migrateTrackTextElements({ track }); - if (migrated !== track) hasChanges = true; - return migrated; - }); - - if (!hasChanges) return scene; - return { ...scene, tracks: migratedTracks }; -} - -function migrateTrackTextElements({ track }: { track: unknown }): unknown { - if (!isRecord(track)) return track; - if (track.type !== "text") return track; - - const elementsValue = track.elements; - if (!Array.isArray(elementsValue)) return track; - - let hasChanges = false; - const migratedElements = elementsValue.map((element) => { - const migrated = migrateTextElement({ element }); - if (migrated !== element) hasChanges = true; - return migrated; - }); - - if (!hasChanges) return track; - return { ...track, elements: migratedElements }; -} - -function migrateTextElement({ element }: { element: unknown }): unknown { - if (!isRecord(element)) return element; - if (element.type !== "text") return element; - - const bg = element.background; - if (!isRecord(bg)) return element; - if (typeof bg.enabled === "boolean") return element; - - const color = typeof bg.color === "string" ? bg.color : "transparent"; - const enabled = color !== "transparent"; - - return { - ...element, - background: { ...bg, enabled }, - }; -} - -function isV9Project({ project }: { project: ProjectRecord }): boolean { - return typeof project.version === "number" && project.version >= 9; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV8ToV9({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + const projectId = getProjectId({ project }); + if (!projectId) { + return { project, skipped: true, reason: "no project id" }; + } + + if (isV9Project({ project })) { + return { project, skipped: true, reason: "already v9" }; + } + + const migratedProject = migrateProjectTextElements({ project }); + + return { + project: { ...migratedProject, version: 9 }, + skipped: false, + }; +} + +function migrateProjectTextElements({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const scenesValue = project.scenes; + if (!Array.isArray(scenesValue)) return project; + + let hasChanges = false; + const migratedScenes = scenesValue.map((scene) => { + const migrated = migrateSceneTextElements({ scene }); + if (migrated !== scene) hasChanges = true; + return migrated; + }); + + if (!hasChanges) return project; + return { ...project, scenes: migratedScenes }; +} + +function migrateSceneTextElements({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) return scene; + + const tracksValue = scene.tracks; + if (!Array.isArray(tracksValue)) return scene; + + let hasChanges = false; + const migratedTracks = tracksValue.map((track) => { + const migrated = migrateTrackTextElements({ track }); + if (migrated !== track) hasChanges = true; + return migrated; + }); + + if (!hasChanges) return scene; + return { ...scene, tracks: migratedTracks }; +} + +function migrateTrackTextElements({ track }: { track: unknown }): unknown { + if (!isRecord(track)) return track; + if (track.type !== "text") return track; + + const elementsValue = track.elements; + if (!Array.isArray(elementsValue)) return track; + + let hasChanges = false; + const migratedElements = elementsValue.map((element) => { + const migrated = migrateTextElement({ element }); + if (migrated !== element) hasChanges = true; + return migrated; + }); + + if (!hasChanges) return track; + return { ...track, elements: migratedElements }; +} + +function migrateTextElement({ element }: { element: unknown }): unknown { + if (!isRecord(element)) return element; + if (element.type !== "text") return element; + + const bg = element.background; + if (!isRecord(bg)) return element; + if (typeof bg.enabled === "boolean") return element; + + const color = typeof bg.color === "string" ? bg.color : "transparent"; + const enabled = color !== "transparent"; + + return { + ...element, + background: { ...bg, enabled }, + }; +} + +function isV9Project({ project }: { project: ProjectRecord }): boolean { + return typeof project.version === "number" && project.version >= 9; +} diff --git a/apps/web/src/services/storage/migrations/transformers/v9-to-v10.ts b/apps/web/src/services/storage/migrations/transformers/v9-to-v10.ts index 52d13295..18885b62 100644 --- a/apps/web/src/services/storage/migrations/transformers/v9-to-v10.ts +++ b/apps/web/src/services/storage/migrations/transformers/v9-to-v10.ts @@ -1,21 +1,21 @@ -import type { MigrationResult, ProjectRecord } from "./types"; -import { getProjectId } from "./utils"; - -export function transformProjectV9ToV10({ - project, -}: { - project: ProjectRecord; -}): MigrationResult { - if (!getProjectId({ project })) { - return { project, skipped: true, reason: "no project id" }; - } - - if (typeof project.version === "number" && project.version >= 10) { - return { project, skipped: true, reason: "already v10" }; - } - - return { - project: { ...project, version: 10 }, - skipped: false, - }; -} +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId } from "./utils"; + +export function transformProjectV9ToV10({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + if (typeof project.version === "number" && project.version >= 10) { + return { project, skipped: true, reason: "already v10" }; + } + + return { + project: { ...project, version: 10 }, + skipped: false, + }; +} diff --git a/apps/web/src/services/storage/migrations/v10-to-v11.ts b/apps/web/src/services/storage/migrations/v10-to-v11.ts index 68439fbe..345dbeb3 100644 --- a/apps/web/src/services/storage/migrations/v10-to-v11.ts +++ b/apps/web/src/services/storage/migrations/v10-to-v11.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV10ToV11 } from "./transformers/v10-to-v11"; - -export class V10toV11Migration extends StorageMigration { - from = 10; - to = 11; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV10ToV11({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV10ToV11 } from "./transformers/v10-to-v11"; + +export class V10toV11Migration extends StorageMigration { + from = 10; + to = 11; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV10ToV11({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v11-to-v12.ts b/apps/web/src/services/storage/migrations/v11-to-v12.ts index 5efedbef..b3154ca8 100644 --- a/apps/web/src/services/storage/migrations/v11-to-v12.ts +++ b/apps/web/src/services/storage/migrations/v11-to-v12.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV11ToV12 } from "./transformers/v11-to-v12"; - -export class V11toV12Migration extends StorageMigration { - from = 11; - to = 12; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV11ToV12({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV11ToV12 } from "./transformers/v11-to-v12"; + +export class V11toV12Migration extends StorageMigration { + from = 11; + to = 12; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV11ToV12({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v12-to-v13.ts b/apps/web/src/services/storage/migrations/v12-to-v13.ts index 5c6e05fc..34b8de60 100644 --- a/apps/web/src/services/storage/migrations/v12-to-v13.ts +++ b/apps/web/src/services/storage/migrations/v12-to-v13.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV12ToV13 } from "./transformers/v12-to-v13"; - -export class V12toV13Migration extends StorageMigration { - from = 12; - to = 13; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV12ToV13({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV12ToV13 } from "./transformers/v12-to-v13"; + +export class V12toV13Migration extends StorageMigration { + from = 12; + to = 13; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV12ToV13({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v13-to-v14.ts b/apps/web/src/services/storage/migrations/v13-to-v14.ts index fbc9fc5f..b8bde59c 100644 --- a/apps/web/src/services/storage/migrations/v13-to-v14.ts +++ b/apps/web/src/services/storage/migrations/v13-to-v14.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV13ToV14 } from "./transformers/v13-to-v14"; - -export class V13toV14Migration extends StorageMigration { - from = 13; - to = 14; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV13ToV14({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV13ToV14 } from "./transformers/v13-to-v14"; + +export class V13toV14Migration extends StorageMigration { + from = 13; + to = 14; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV13ToV14({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v14-to-v15.ts b/apps/web/src/services/storage/migrations/v14-to-v15.ts index 1d18423f..5c3a60cf 100644 --- a/apps/web/src/services/storage/migrations/v14-to-v15.ts +++ b/apps/web/src/services/storage/migrations/v14-to-v15.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV14ToV15 } from "./transformers/v14-to-v15"; - -export class V14toV15Migration extends StorageMigration { - from = 14; - to = 15; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV14ToV15({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV14ToV15 } from "./transformers/v14-to-v15"; + +export class V14toV15Migration extends StorageMigration { + from = 14; + to = 15; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV14ToV15({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v15-to-v16.ts b/apps/web/src/services/storage/migrations/v15-to-v16.ts index 800c9c07..07afe148 100644 --- a/apps/web/src/services/storage/migrations/v15-to-v16.ts +++ b/apps/web/src/services/storage/migrations/v15-to-v16.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV15ToV16 } from "./transformers/v15-to-v16"; - -export class V15toV16Migration extends StorageMigration { - from = 15; - to = 16; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV15ToV16({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV15ToV16 } from "./transformers/v15-to-v16"; + +export class V15toV16Migration extends StorageMigration { + from = 15; + to = 16; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV15ToV16({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v16-to-v17.ts b/apps/web/src/services/storage/migrations/v16-to-v17.ts index f4029db6..053e8d07 100644 --- a/apps/web/src/services/storage/migrations/v16-to-v17.ts +++ b/apps/web/src/services/storage/migrations/v16-to-v17.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV16ToV17 } from "./transformers/v16-to-v17"; - -export class V16toV17Migration extends StorageMigration { - from = 16; - to = 17; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV16ToV17({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV16ToV17 } from "./transformers/v16-to-v17"; + +export class V16toV17Migration extends StorageMigration { + from = 16; + to = 17; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV16ToV17({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v17-to-v18.ts b/apps/web/src/services/storage/migrations/v17-to-v18.ts index 75beb36b..b1591907 100644 --- a/apps/web/src/services/storage/migrations/v17-to-v18.ts +++ b/apps/web/src/services/storage/migrations/v17-to-v18.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV17ToV18 } from "./transformers/v17-to-v18"; - -export class V17toV18Migration extends StorageMigration { - from = 17; - to = 18; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV17ToV18({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV17ToV18 } from "./transformers/v17-to-v18"; + +export class V17toV18Migration extends StorageMigration { + from = 17; + to = 18; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV17ToV18({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v18-to-v19.ts b/apps/web/src/services/storage/migrations/v18-to-v19.ts index 166d9855..d8fde783 100644 --- a/apps/web/src/services/storage/migrations/v18-to-v19.ts +++ b/apps/web/src/services/storage/migrations/v18-to-v19.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV18ToV19 } from "./transformers/v18-to-v19"; - -export class V18toV19Migration extends StorageMigration { - from = 18; - to = 19; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV18ToV19({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV18ToV19 } from "./transformers/v18-to-v19"; + +export class V18toV19Migration extends StorageMigration { + from = 18; + to = 19; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV18ToV19({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v3-to-v4.ts b/apps/web/src/services/storage/migrations/v3-to-v4.ts index 67b2fdf7..a3738076 100644 --- a/apps/web/src/services/storage/migrations/v3-to-v4.ts +++ b/apps/web/src/services/storage/migrations/v3-to-v4.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV3ToV4 } from "./transformers/v3-to-v4"; - -export class V3toV4Migration extends StorageMigration { - from = 3; - to = 4; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV3ToV4({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV3ToV4 } from "./transformers/v3-to-v4"; + +export class V3toV4Migration extends StorageMigration { + from = 3; + to = 4; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV3ToV4({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v4-to-v5.ts b/apps/web/src/services/storage/migrations/v4-to-v5.ts index 829a746d..394406d1 100644 --- a/apps/web/src/services/storage/migrations/v4-to-v5.ts +++ b/apps/web/src/services/storage/migrations/v4-to-v5.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV4ToV5 } from "./transformers/v4-to-v5"; - -export class V4toV5Migration extends StorageMigration { - from = 4; - to = 5; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV4ToV5({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV4ToV5 } from "./transformers/v4-to-v5"; + +export class V4toV5Migration extends StorageMigration { + from = 4; + to = 5; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV4ToV5({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v5-to-v6.ts b/apps/web/src/services/storage/migrations/v5-to-v6.ts index b621bd72..e6be4f9b 100644 --- a/apps/web/src/services/storage/migrations/v5-to-v6.ts +++ b/apps/web/src/services/storage/migrations/v5-to-v6.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV5ToV6 } from "./transformers/v5-to-v6"; - -export class V5toV6Migration extends StorageMigration { - from = 5; - to = 6; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV5ToV6({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV5ToV6 } from "./transformers/v5-to-v6"; + +export class V5toV6Migration extends StorageMigration { + from = 5; + to = 6; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV5ToV6({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v6-to-v7.ts b/apps/web/src/services/storage/migrations/v6-to-v7.ts index 0556b4d5..3f63770b 100644 --- a/apps/web/src/services/storage/migrations/v6-to-v7.ts +++ b/apps/web/src/services/storage/migrations/v6-to-v7.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV6ToV7 } from "./transformers/v6-to-v7"; - -export class V6toV7Migration extends StorageMigration { - from = 6; - to = 7; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV6ToV7({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV6ToV7 } from "./transformers/v6-to-v7"; + +export class V6toV7Migration extends StorageMigration { + from = 6; + to = 7; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV6ToV7({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v7-to-v8.ts b/apps/web/src/services/storage/migrations/v7-to-v8.ts index 33372c0a..5a3302f7 100644 --- a/apps/web/src/services/storage/migrations/v7-to-v8.ts +++ b/apps/web/src/services/storage/migrations/v7-to-v8.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV7ToV8 } from "./transformers/v7-to-v8"; - -export class V7toV8Migration extends StorageMigration { - from = 7; - to = 8; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV7ToV8({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV7ToV8 } from "./transformers/v7-to-v8"; + +export class V7toV8Migration extends StorageMigration { + from = 7; + to = 8; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV7ToV8({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v8-to-v9.ts b/apps/web/src/services/storage/migrations/v8-to-v9.ts index 6222eeac..bdbed910 100644 --- a/apps/web/src/services/storage/migrations/v8-to-v9.ts +++ b/apps/web/src/services/storage/migrations/v8-to-v9.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV8ToV9 } from "./transformers/v8-to-v9"; - -export class V8toV9Migration extends StorageMigration { - from = 8; - to = 9; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV8ToV9({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV8ToV9 } from "./transformers/v8-to-v9"; + +export class V8toV9Migration extends StorageMigration { + from = 8; + to = 9; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV8ToV9({ project }); + } +} diff --git a/apps/web/src/services/storage/migrations/v9-to-v10.ts b/apps/web/src/services/storage/migrations/v9-to-v10.ts index 801096fe..d61116f0 100644 --- a/apps/web/src/services/storage/migrations/v9-to-v10.ts +++ b/apps/web/src/services/storage/migrations/v9-to-v10.ts @@ -1,16 +1,16 @@ -import { StorageMigration } from "./base"; -import type { ProjectRecord } from "./transformers/types"; -import { transformProjectV9ToV10 } from "./transformers/v9-to-v10"; - -export class V9toV10Migration extends StorageMigration { - from = 9; - to = 10; - - async transform(project: ProjectRecord): Promise<{ - project: ProjectRecord; - skipped: boolean; - reason?: string; - }> { - return transformProjectV9ToV10({ project }); - } -} +import { StorageMigration } from "./base"; +import type { ProjectRecord } from "./transformers/types"; +import { transformProjectV9ToV10 } from "./transformers/v9-to-v10"; + +export class V9toV10Migration extends StorageMigration { + from = 9; + to = 10; + + async transform(project: ProjectRecord): Promise<{ + project: ProjectRecord; + skipped: boolean; + reason?: string; + }> { + return transformProjectV9ToV10({ project }); + } +} diff --git a/apps/web/src/services/storage/quota.ts b/apps/web/src/services/storage/quota.ts index 1eb6028e..4d1adad5 100644 --- a/apps/web/src/services/storage/quota.ts +++ b/apps/web/src/services/storage/quota.ts @@ -1,145 +1,145 @@ -const BYTE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const; - -export const STORAGE_HEADROOM_RESERVE_BYTES = 50 * 1024 * 1024; - -export interface StorageQuotaStatus { - quotaBytes: number | null; - usageBytes: number | null; - headroomBytes: number | null; - availableBytes: number | null; -} - -export interface StorageCapacityCheckResult { - canStore: boolean; - reason: "enough-space" | "insufficient-space" | "estimate-unavailable"; - availableBytes: number | null; -} - -function normalizeByteValue({ value }: { value: unknown }): number | null { - if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { - return null; - } - - return value; -} - -export function formatStorageBytes({ bytes }: { bytes: number }): string { - if (!Number.isFinite(bytes) || bytes <= 0) { - return "0 B"; - } - - let value = bytes; - let unitIndex = 0; - - while (value >= 1024 && unitIndex < BYTE_UNITS.length - 1) { - value /= 1024; - unitIndex += 1; - } - - const precision = value >= 10 || unitIndex === 0 ? 0 : 1; - return `${value.toFixed(precision)} ${BYTE_UNITS[unitIndex]}`; -} - -export async function readStorageQuotaStatus(): Promise { - if ( - typeof navigator === "undefined" || - !navigator.storage || - typeof navigator.storage.estimate !== "function" - ) { - return { - quotaBytes: null, - usageBytes: null, - headroomBytes: null, - availableBytes: null, - }; - } - - const estimate = await navigator.storage.estimate(); - const quotaBytes = normalizeByteValue({ value: estimate.quota }); - const usageBytes = normalizeByteValue({ value: estimate.usage }); - - if (quotaBytes === null || usageBytes === null) { - return { - quotaBytes, - usageBytes, - headroomBytes: null, - availableBytes: null, - }; - } - - const headroomBytes = Math.max(quotaBytes - usageBytes, 0); - const availableBytes = Math.max( - headroomBytes - STORAGE_HEADROOM_RESERVE_BYTES, - 0, - ); - - return { - quotaBytes, - usageBytes, - headroomBytes, - availableBytes, - }; -} - -export function evaluateStorageCapacity({ - requiredBytes, - quotaStatus, -}: { - requiredBytes: number; - quotaStatus: StorageQuotaStatus; -}): StorageCapacityCheckResult { - if (quotaStatus.availableBytes === null) { - return { - canStore: true, - reason: "estimate-unavailable", - availableBytes: null, - }; - } - - if (requiredBytes > quotaStatus.availableBytes) { - return { - canStore: false, - reason: "insufficient-space", - availableBytes: quotaStatus.availableBytes, - }; - } - - return { - canStore: true, - reason: "enough-space", - availableBytes: quotaStatus.availableBytes, - }; -} - -export class StorageQuotaExceededError extends Error { - requiredBytes: number; - - constructor({ requiredBytes }: { requiredBytes: number }) { - super( - `Not enough browser storage to save a ${formatStorageBytes({ bytes: requiredBytes })} file.`, - ); - - this.name = "StorageQuotaExceededError"; - this.requiredBytes = requiredBytes; - } -} - -export function isStorageQuotaExceededError({ - error, -}: { - error: unknown; -}): boolean { - if (error instanceof StorageQuotaExceededError) { - return true; - } - - if (!(error instanceof Error)) { - return false; - } - - return ( - error.name === "QuotaExceededError" || - error.name === "NS_ERROR_DOM_QUOTA_REACHED" || - error.message.toLowerCase().includes("quota") - ); -} +const BYTE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const; + +export const STORAGE_HEADROOM_RESERVE_BYTES = 50 * 1024 * 1024; + +export interface StorageQuotaStatus { + quotaBytes: number | null; + usageBytes: number | null; + headroomBytes: number | null; + availableBytes: number | null; +} + +export interface StorageCapacityCheckResult { + canStore: boolean; + reason: "enough-space" | "insufficient-space" | "estimate-unavailable"; + availableBytes: number | null; +} + +function normalizeByteValue({ value }: { value: unknown }): number | null { + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { + return null; + } + + return value; +} + +export function formatStorageBytes({ bytes }: { bytes: number }): string { + if (!Number.isFinite(bytes) || bytes <= 0) { + return "0 B"; + } + + let value = bytes; + let unitIndex = 0; + + while (value >= 1024 && unitIndex < BYTE_UNITS.length - 1) { + value /= 1024; + unitIndex += 1; + } + + const precision = value >= 10 || unitIndex === 0 ? 0 : 1; + return `${value.toFixed(precision)} ${BYTE_UNITS[unitIndex]}`; +} + +export async function readStorageQuotaStatus(): Promise { + if ( + typeof navigator === "undefined" || + !navigator.storage || + typeof navigator.storage.estimate !== "function" + ) { + return { + quotaBytes: null, + usageBytes: null, + headroomBytes: null, + availableBytes: null, + }; + } + + const estimate = await navigator.storage.estimate(); + const quotaBytes = normalizeByteValue({ value: estimate.quota }); + const usageBytes = normalizeByteValue({ value: estimate.usage }); + + if (quotaBytes === null || usageBytes === null) { + return { + quotaBytes, + usageBytes, + headroomBytes: null, + availableBytes: null, + }; + } + + const headroomBytes = Math.max(quotaBytes - usageBytes, 0); + const availableBytes = Math.max( + headroomBytes - STORAGE_HEADROOM_RESERVE_BYTES, + 0, + ); + + return { + quotaBytes, + usageBytes, + headroomBytes, + availableBytes, + }; +} + +export function evaluateStorageCapacity({ + requiredBytes, + quotaStatus, +}: { + requiredBytes: number; + quotaStatus: StorageQuotaStatus; +}): StorageCapacityCheckResult { + if (quotaStatus.availableBytes === null) { + return { + canStore: true, + reason: "estimate-unavailable", + availableBytes: null, + }; + } + + if (requiredBytes > quotaStatus.availableBytes) { + return { + canStore: false, + reason: "insufficient-space", + availableBytes: quotaStatus.availableBytes, + }; + } + + return { + canStore: true, + reason: "enough-space", + availableBytes: quotaStatus.availableBytes, + }; +} + +export class StorageQuotaExceededError extends Error { + requiredBytes: number; + + constructor({ requiredBytes }: { requiredBytes: number }) { + super( + `Not enough browser storage to save a ${formatStorageBytes({ bytes: requiredBytes })} file.`, + ); + + this.name = "StorageQuotaExceededError"; + this.requiredBytes = requiredBytes; + } +} + +export function isStorageQuotaExceededError({ + error, +}: { + error: unknown; +}): boolean { + if (error instanceof StorageQuotaExceededError) { + return true; + } + + if (!(error instanceof Error)) { + return false; + } + + return ( + error.name === "QuotaExceededError" || + error.name === "NS_ERROR_DOM_QUOTA_REACHED" || + error.message.toLowerCase().includes("quota") + ); +} -- cgit v1.3.1