summaryrefslogtreecommitdiff
path: root/apps/web/src/services/storage
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/services/storage')
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/fixtures/index.ts20
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/fixtures/v5.ts108
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts186
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts264
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts126
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts280
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts274
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts184
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts350
-rw-r--r--apps/web/src/services/storage/migrations/base.ts18
-rw-r--r--apps/web/src/services/storage/migrations/index.ts92
-rw-r--r--apps/web/src/services/storage/migrations/runner.ts310
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v10-to-v11.ts226
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v11-to-v12.ts392
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v12-to-v13.ts204
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v13-to-v14.ts212
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts134
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v15-to-v16.ts120
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v16-to-v17.ts214
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts210
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v18-to-v19.ts92
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v3-to-v4.ts378
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v4-to-v5.ts354
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v5-to-v6.ts176
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v6-to-v7.ts212
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v7-to-v8.ts192
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v8-to-v9.ts198
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v9-to-v10.ts42
-rw-r--r--apps/web/src/services/storage/migrations/v10-to-v11.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v11-to-v12.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v12-to-v13.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v13-to-v14.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v14-to-v15.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v15-to-v16.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v16-to-v17.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v17-to-v18.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v18-to-v19.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v3-to-v4.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v4-to-v5.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v5-to-v6.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v6-to-v7.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v7-to-v8.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v8-to-v9.ts32
-rw-r--r--apps/web/src/services/storage/migrations/v9-to-v10.ts32
-rw-r--r--apps/web/src/services/storage/quota.ts290
45 files changed, 3185 insertions, 3185 deletions
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<string, unknown> }> }> }> }>)[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<string, unknown> }> }> }> }>)[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<string, unknown>).canvasSizeMode,
- ).toBe("preset");
- expect(
- (result.project.settings as Record<string, unknown>).lastCustomCanvasSize,
- ).toBeNull();
- expect(
- (result.project.settings as Record<string, unknown>).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<string, unknown>).canvasSizeMode,
+ ).toBe("preset");
+ expect(
+ (result.project.settings as Record<string, unknown>).lastCustomCanvasSize,
+ ).toBeNull();
+ expect(
+ (result.project.settings as Record<string, unknown>).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<Record<string, unknown>>
- )[0];
- const migratedTrack = (
- migratedScene.tracks as Array<Record<string, unknown>>
- )[0];
- const migratedElement = (
- migratedTrack.elements as Array<Record<string, unknown>>
- )[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<Record<string, unknown>>
- )[0];
- const migratedTrack = (
- migratedScene.tracks as Array<Record<string, unknown>>
- )[0];
- const migratedElement = (
- migratedTrack.elements as Array<Record<string, unknown>>
- )[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<Record<string, unknown>>
+ )[0];
+ const migratedTrack = (
+ migratedScene.tracks as Array<Record<string, unknown>>
+ )[0];
+ const migratedElement = (
+ migratedTrack.elements as Array<Record<string, unknown>>
+ )[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<Record<string, unknown>>
+ )[0];
+ const migratedTrack = (
+ migratedScene.tracks as Array<Record<string, unknown>>
+ )[0];
+ const migratedElement = (
+ migratedTrack.elements as Array<Record<string, unknown>>
+ )[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<Record<string, unknown>>
- )[0];
- const migratedTrack = (
- migratedScene.tracks as Array<Record<string, unknown>>
- )[0];
- const migratedElement = (
- migratedTrack.elements as Array<Record<string, unknown>>
- )[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<Record<string, unknown>>
- )[0];
- const migratedTrack = (
- migratedScene.tracks as Array<Record<string, unknown>>
- )[0];
- const migratedElement = (
- migratedTrack.elements as Array<Record<string, unknown>>
- )[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<Record<string, unknown>>
+ )[0];
+ const migratedTrack = (
+ migratedScene.tracks as Array<Record<string, unknown>>
+ )[0];
+ const migratedElement = (
+ migratedTrack.elements as Array<Record<string, unknown>>
+ )[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<Record<string, unknown>>
+ )[0];
+ const migratedTrack = (
+ migratedScene.tracks as Array<Record<string, unknown>>
+ )[0];
+ const migratedElement = (
+ migratedTrack.elements as Array<Record<string, unknown>>
+ )[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<typeof transformProjectV5ToV6>[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<typeof transformProjectV5ToV6>[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<Record<string, unknown>> })
- .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<typeof transformProjectV5ToV6>[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<typeof transformProjectV5ToV6>[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<Record<string, unknown>> })
+ .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<typeof transformProjectV8ToV9>[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<string, unknown>[])[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<typeof transformProjectV8ToV9>[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<typeof transformProjectV8ToV9>[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<typeof transformProjectV8ToV9>[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<typeof transformProjectV8ToV9>[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<string, unknown>[])[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<typeof transformProjectV8ToV9>[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<typeof transformProjectV8ToV9>[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<typeof transformProjectV8ToV9>[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<MigrationResult<ProjectRecord>>;
-}
+import type { MigrationResult, ProjectRecord } from "./transformers/types";
+
+export abstract class StorageMigration {
+ abstract from: number;
+ abstract to: number;
+ abstract transform(
+ project: ProjectRecord,
+ ): Promise<MigrationResult<ProjectRecord>>;
+}
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<StorageMigrationResult> {
- // 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<ProjectRecord>(
- "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<StorageMigrationResult> {
+ // 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<ProjectRecord>(
+ "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<ProjectRecord> {
- 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<string, unknown>).scale;
-
- let migratedElement: ProjectRecord = {
- ...element,
- transform: migratedTransform,
- };
-
- const animations = element.animations;
- if (isRecord(animations) && isRecord(animations.channels)) {
- const channels = animations.channels as Record<string, unknown>;
- 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<ProjectRecord> {
+ 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<string, unknown>).scale;
+
+ let migratedElement: ProjectRecord = {
+ ...element,
+ transform: migratedTransform,
+ };
+
+ const animations = element.animations;
+ if (isRecord(animations) && isRecord(animations.channels)) {
+ const channels = animations.channels as Record<string, unknown>;
+ 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<ProjectRecord> {
- 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<string, unknown>;
- 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<ProjectRecord> {
+ 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<string, unknown>;
+ 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<ProjectRecord> {
- 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<string, unknown>).feather;
- delete (result as Record<string, unknown>).inverted;
- delete (result as Record<string, unknown>).stroke;
-
- return result;
-}
+import type { MigrationResult, ProjectRecord } from "./types";
+import { getProjectId, isRecord } from "./utils";
+
+export function transformProjectV12ToV13({
+ project,
+}: {
+ project: ProjectRecord;
+}): MigrationResult<ProjectRecord> {
+ 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<string, unknown>).feather;
+ delete (result as Record<string, unknown>).inverted;
+ delete (result as Record<string, unknown>).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<ProjectRecord> {
- 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<ProjectRecord> {
+ 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<ProjectRecord> {
- 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<ProjectRecord> {
+ 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<ProjectRecord> {
- 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<ProjectRecord> {
+ 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<ProjectRecord> {
- 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<ProjectRecord> {
+ 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<ProjectRecord> {
- 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<ProjectRecord> {
+ 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<ProjectRecord> {
- 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<string, unknown> = {
- ...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<ProjectRecord> {
+ 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<string, unknown> = {
+ ...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<ProjectRecord> {
- 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<ProjectRecord> {
+ 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<ProjectRecord> {
- 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<ProjectRecord> {
+ 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<ProjectRecord> {
- 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<ProjectRecord> {
+ 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<ProjectRecord> {
- 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<ProjectRecord> {
+ 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<ProjectRecord> {
- 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<ProjectRecord> {
+ 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<ProjectRecord> {
- 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<ProjectRecord> {
+ 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<ProjectRecord> {
- 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<ProjectRecord> {
+ 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<StorageQuotaStatus> {
- 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<StorageQuotaStatus> {
+ 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")
+ );
+}