diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-29 01:28:50 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-05-02 03:27:23 +0200 |
| commit | 2ee0a44735d2b930348e9774909638b2c1909738 (patch) | |
| tree | e73e60097338ed411739f3b04808dad86f7bb192 /apps/web/src/services/storage | |
| parent | a9b9cf6bf50f08723896659634f2e6afc17bcd42 (diff) | |
| download | opencut-2ee0a44735d2b930348e9774909638b2c1909738.tar.gz | |
refactor: unify element params
Diffstat (limited to 'apps/web/src/services/storage')
5 files changed, 386 insertions, 1 deletions
diff --git a/apps/web/src/services/storage/migrations/__tests__/v28-to-v29.test.ts b/apps/web/src/services/storage/migrations/__tests__/v28-to-v29.test.ts new file mode 100644 index 00000000..dc76b6fd --- /dev/null +++ b/apps/web/src/services/storage/migrations/__tests__/v28-to-v29.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, test } from "bun:test"; +import { transformProjectV28ToV29 } from "../transformers/v28-to-v29"; +import { asRecord, asRecordArray } from "./helpers"; + +describe("V28 to V29 Migration", () => { + test("moves built-in element fields into params", () => { + const result = transformProjectV28ToV29({ + project: { + id: "project-v28-builtins", + version: 28, + scenes: [ + { + id: "scene-1", + tracks: { + main: { + id: "main", + type: "video", + elements: [ + { + id: "video-1", + type: "video", + name: "Video", + transform: { + position: { x: 12, y: -4 }, + scaleX: 1.25, + scaleY: 0.75, + rotate: 9, + }, + opacity: 0.5, + blendMode: "multiply", + volume: -6, + muted: true, + }, + ], + }, + overlay: [ + { + id: "text-track", + type: "text", + elements: [ + { + id: "text-1", + type: "text", + name: "Text", + content: "Hello", + fontSize: 20, + fontFamily: "Inter", + color: "#eeeeee", + textAlign: "left", + fontWeight: "bold", + fontStyle: "italic", + textDecoration: "underline", + letterSpacing: 1.5, + lineHeight: 1.4, + background: { + enabled: true, + color: "#111111", + paddingX: 10, + paddingY: 12, + }, + transform: { + position: { x: 1, y: 2 }, + scaleX: 1, + scaleY: 1, + rotate: 0, + }, + opacity: 1, + }, + ], + }, + ], + audio: [], + }, + }, + ], + }, + }); + + expect(result.skipped).toBe(false); + expect(result.project.version).toBe(29); + + const scene = asRecordArray(result.project.scenes)[0]; + const tracks = asRecord(scene.tracks); + const main = asRecord(tracks.main); + const video = asRecordArray(main.elements)[0]; + expect(video.transform).toBeUndefined(); + expect(video.opacity).toBeUndefined(); + expect(video.volume).toBeUndefined(); + expect(video.muted).toBeUndefined(); + expect(video.params).toEqual({ + "transform.positionX": 12, + "transform.positionY": -4, + "transform.scaleX": 1.25, + "transform.scaleY": 0.75, + "transform.rotate": 9, + opacity: 0.5, + blendMode: "multiply", + volume: -6, + muted: true, + }); + + const overlay = asRecordArray(tracks.overlay)[0]; + const text = asRecordArray(asRecord(overlay).elements)[0]; + expect(text.content).toBeUndefined(); + expect(text.background).toBeUndefined(); + expect(text.params).toMatchObject({ + content: "Hello", + fontSize: 20, + fontFamily: "Inter", + color: "#eeeeee", + textAlign: "left", + fontWeight: "bold", + fontStyle: "italic", + textDecoration: "underline", + letterSpacing: 1.5, + lineHeight: 1.4, + "background.enabled": true, + "background.color": "#111111", + "background.paddingX": 10, + "background.paddingY": 12, + "transform.positionX": 1, + "transform.positionY": 2, + }); + }); +}); diff --git a/apps/web/src/services/storage/migrations/index.ts b/apps/web/src/services/storage/migrations/index.ts index 150da7c4..2e2c83bc 100644 --- a/apps/web/src/services/storage/migrations/index.ts +++ b/apps/web/src/services/storage/migrations/index.ts @@ -27,10 +27,11 @@ import { V24toV25Migration } from "./v24-to-v25"; import { V25toV26Migration } from "./v25-to-v26"; import { V26toV27Migration } from "./v26-to-v27"; import { V27toV28Migration } from "./v27-to-v28"; +import { V28toV29Migration } from "./v28-to-v29"; export { runStorageMigrations } from "./runner"; export type { MigrationProgress } from "./runner"; -export const CURRENT_PROJECT_VERSION = 28; +export const CURRENT_PROJECT_VERSION = 29; export const migrations = [ new V0toV1Migration(), @@ -61,4 +62,5 @@ export const migrations = [ new V25toV26Migration(), new V26toV27Migration(), new V27toV28Migration(), + new V28toV29Migration(), ]; diff --git a/apps/web/src/services/storage/migrations/transformers/index.ts b/apps/web/src/services/storage/migrations/transformers/index.ts index c1ae894f..ace19354 100644 --- a/apps/web/src/services/storage/migrations/transformers/index.ts +++ b/apps/web/src/services/storage/migrations/transformers/index.ts @@ -4,4 +4,5 @@ export { transformProjectV2ToV3 } from "./v2-to-v3"; export { transformProjectV3ToV4 } from "./v3-to-v4"; export { transformProjectV4ToV5 } from "./v4-to-v5"; export { transformProjectV17ToV18 } from "./v17-to-v18"; +export { transformProjectV28ToV29 } from "./v28-to-v29"; export type { MigrationResult, ProjectRecord } from "./types"; diff --git a/apps/web/src/services/storage/migrations/transformers/v28-to-v29.ts b/apps/web/src/services/storage/migrations/transformers/v28-to-v29.ts new file mode 100644 index 00000000..c7f68a34 --- /dev/null +++ b/apps/web/src/services/storage/migrations/transformers/v28-to-v29.ts @@ -0,0 +1,243 @@ +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV28ToV29({ + project, +}: { + project: ProjectRecord; +}): MigrationResult<ProjectRecord> { + if (!getProjectId({ project })) { + return { project, skipped: true, reason: "no project id" }; + } + + const version = project.version; + if (typeof version !== "number") { + return { project, skipped: true, reason: "invalid version" }; + } + if (version >= 29) { + return { project, skipped: true, reason: "already v29" }; + } + if (version !== 28) { + return { project, skipped: true, reason: "not v28" }; + } + + return { + project: { + ...migrateProject({ project }), + version: 29, + }, + skipped: false, + }; +} + +function migrateProject({ project }: { project: ProjectRecord }): ProjectRecord { + const nextProject = { ...project }; + if (Array.isArray(project.scenes)) { + nextProject.scenes = project.scenes.map((scene) => migrateScene({ scene })); + } + return nextProject; +} + +function migrateScene({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) { + return scene; + } + + const nextScene = { ...scene }; + if (isRecord(scene.tracks)) { + nextScene.tracks = migrateTracks({ tracks: scene.tracks }); + } + return nextScene; +} + +function migrateTracks({ tracks }: { tracks: ProjectRecord }): ProjectRecord { + const nextTracks = { ...tracks }; + if (isRecord(tracks.main)) { + nextTracks.main = migrateTrack({ track: tracks.main }); + } + if (Array.isArray(tracks.overlay)) { + nextTracks.overlay = tracks.overlay.map((track) => migrateTrack({ track })); + } + if (Array.isArray(tracks.audio)) { + nextTracks.audio = tracks.audio.map((track) => migrateTrack({ track })); + } + return nextTracks; +} + +function migrateTrack({ track }: { track: unknown }): unknown { + if (!isRecord(track) || !Array.isArray(track.elements)) { + return track; + } + + return { + ...track, + elements: track.elements.map((element) => migrateElement({ element })), + }; +} + +function migrateElement({ element }: { element: unknown }): unknown { + if (!isRecord(element)) { + return element; + } + + const nextElement = { ...element }; + const params: ProjectRecord = isRecord(element.params) + ? { ...element.params } + : {}; + + copyTransformParams({ source: element, params }); + copyPrimitiveParam({ source: element, params, sourceKey: "opacity", paramKey: "opacity" }); + copyPrimitiveParam({ + source: element, + params, + sourceKey: "blendMode", + paramKey: "blendMode", + }); + + if (element.type === "audio" || element.type === "video") { + copyPrimitiveParam({ + source: element, + params, + sourceKey: "volume", + paramKey: "volume", + }); + copyPrimitiveParam({ + source: element, + params, + sourceKey: "muted", + paramKey: "muted", + }); + } + + if (element.type === "text") { + copyTextParams({ source: element, params }); + } + + nextElement.params = params; + delete nextElement.transform; + delete nextElement.opacity; + delete nextElement.blendMode; + delete nextElement.volume; + delete nextElement.muted; + delete nextElement.content; + delete nextElement.fontSize; + delete nextElement.fontFamily; + delete nextElement.color; + delete nextElement.background; + delete nextElement.textAlign; + delete nextElement.fontWeight; + delete nextElement.fontStyle; + delete nextElement.textDecoration; + delete nextElement.letterSpacing; + delete nextElement.lineHeight; + + return nextElement; +} + +function copyTransformParams({ + source, + params, +}: { + source: ProjectRecord; + params: ProjectRecord; +}): void { + if (!isRecord(source.transform)) { + return; + } + if (isRecord(source.transform.position)) { + copyPrimitiveParam({ + source: source.transform.position, + params, + sourceKey: "x", + paramKey: "transform.positionX", + }); + copyPrimitiveParam({ + source: source.transform.position, + params, + sourceKey: "y", + paramKey: "transform.positionY", + }); + } + copyPrimitiveParam({ + source: source.transform, + params, + sourceKey: "scaleX", + paramKey: "transform.scaleX", + }); + copyPrimitiveParam({ + source: source.transform, + params, + sourceKey: "scaleY", + paramKey: "transform.scaleY", + }); + copyPrimitiveParam({ + source: source.transform, + params, + sourceKey: "rotate", + paramKey: "transform.rotate", + }); +} + +function copyTextParams({ + source, + params, +}: { + source: ProjectRecord; + params: ProjectRecord; +}): void { + for (const key of [ + "content", + "fontSize", + "fontFamily", + "color", + "textAlign", + "fontWeight", + "fontStyle", + "textDecoration", + "letterSpacing", + "lineHeight", + ]) { + copyPrimitiveParam({ source, params, sourceKey: key, paramKey: key }); + } + + if (!isRecord(source.background)) { + return; + } + for (const key of [ + "enabled", + "color", + "cornerRadius", + "paddingX", + "paddingY", + "offsetX", + "offsetY", + ]) { + copyPrimitiveParam({ + source: source.background, + params, + sourceKey: key, + paramKey: `background.${key}`, + }); + } +} + +function copyPrimitiveParam({ + source, + params, + sourceKey, + paramKey, +}: { + source: ProjectRecord; + params: ProjectRecord; + sourceKey: string; + paramKey: string; +}): void { + const value = source[sourceKey]; + if ( + typeof value === "number" || + typeof value === "string" || + typeof value === "boolean" + ) { + params[paramKey] = value; + } +} diff --git a/apps/web/src/services/storage/migrations/v28-to-v29.ts b/apps/web/src/services/storage/migrations/v28-to-v29.ts new file mode 100644 index 00000000..a2e75ec0 --- /dev/null +++ b/apps/web/src/services/storage/migrations/v28-to-v29.ts @@ -0,0 +1,14 @@ +import { StorageMigration, type StorageMigrationRunArgs } from "./base"; +import type { MigrationResult, ProjectRecord } from "./transformers/types"; +import { transformProjectV28ToV29 } from "./transformers/v28-to-v29"; + +export class V28toV29Migration extends StorageMigration { + from = 28; + to = 29; + + async run({ + project, + }: StorageMigrationRunArgs): Promise<MigrationResult<ProjectRecord>> { + return transformProjectV28ToV29({ project }); + } +} |
