mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
chore: normalize line endings
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user