mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
chore: switch from biome to eslint + prettier; fix ton of lint issues
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { FrameRate } from "opencut-wasm";
|
||||
import type { AnyBaseNode } from "./nodes/base-node";
|
||||
import { createCanvasSurface } from "./canvas-utils";
|
||||
import { buildFrameDescriptor } from "./compositor/frame-descriptor";
|
||||
import { wasmCompositor } from "./compositor/wasm-compositor";
|
||||
import { resolveRenderTree } from "./resolve";
|
||||
@@ -16,8 +17,8 @@ export type CanvasRendererParams = {
|
||||
};
|
||||
|
||||
export class CanvasRenderer {
|
||||
canvas: OffscreenCanvas | HTMLCanvasElement;
|
||||
context: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;
|
||||
canvas: OffscreenCanvas;
|
||||
context: OffscreenCanvasRenderingContext2D;
|
||||
width: number;
|
||||
height: number;
|
||||
fps: FrameRate;
|
||||
@@ -27,22 +28,9 @@ export class CanvasRenderer {
|
||||
this.height = height;
|
||||
this.fps = fps;
|
||||
|
||||
try {
|
||||
this.canvas = new OffscreenCanvas(width, height);
|
||||
} catch {
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = height;
|
||||
}
|
||||
|
||||
const context = this.canvas.getContext("2d");
|
||||
if (!context) {
|
||||
throw new Error("Failed to get canvas context");
|
||||
}
|
||||
|
||||
this.context = context as
|
||||
| OffscreenCanvasRenderingContext2D
|
||||
| CanvasRenderingContext2D;
|
||||
const surface = createCanvasSurface({ width, height });
|
||||
this.canvas = surface.canvas;
|
||||
this.context = surface.context;
|
||||
}
|
||||
|
||||
getOutputCanvas(): HTMLCanvasElement {
|
||||
@@ -57,20 +45,9 @@ export class CanvasRenderer {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
if (this.canvas instanceof OffscreenCanvas) {
|
||||
this.canvas = new OffscreenCanvas(width, height);
|
||||
} else {
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = height;
|
||||
}
|
||||
|
||||
const context = this.canvas.getContext("2d");
|
||||
if (!context) {
|
||||
throw new Error("Failed to get canvas context");
|
||||
}
|
||||
this.context = context as
|
||||
| OffscreenCanvasRenderingContext2D
|
||||
| CanvasRenderingContext2D;
|
||||
const surface = createCanvasSurface({ width, height });
|
||||
this.canvas = surface.canvas;
|
||||
this.context = surface.context;
|
||||
}
|
||||
|
||||
async render({ node, time }: { node: AnyBaseNode; time: number }) {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
export function createOffscreenCanvas({
|
||||
export function createCanvasSurface({
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
}): OffscreenCanvas | HTMLCanvasElement {
|
||||
try {
|
||||
return new OffscreenCanvas(width, height);
|
||||
} catch {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
return canvas;
|
||||
}): {
|
||||
canvas: OffscreenCanvas;
|
||||
context: OffscreenCanvasRenderingContext2D;
|
||||
} {
|
||||
const canvas = new OffscreenCanvas(width, height);
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
throw new Error("Failed to create 2D rendering context");
|
||||
}
|
||||
return { canvas, context };
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { masksRegistry } from "@/masks";
|
||||
import { incrementCounter } from "@/diagnostics/render-perf";
|
||||
import type { AnyBaseNode } from "../nodes/base-node";
|
||||
import type { CanvasRenderer } from "../canvas-renderer";
|
||||
import { createOffscreenCanvas } from "../canvas-utils";
|
||||
import { createCanvasSurface } from "../canvas-utils";
|
||||
import { BlurBackgroundNode } from "../nodes/blur-background-node";
|
||||
import { ColorNode } from "../nodes/color-node";
|
||||
import { EffectLayerNode } from "../nodes/effect-layer-node";
|
||||
@@ -414,15 +414,11 @@ function buildMaskArtifacts({
|
||||
const { width: canvasWidth, height: canvasHeight } = renderer;
|
||||
const maskContentHash = `mask:${mask.type}:${JSON.stringify(mask.params)}:${transformHash(transform)}:${canvasWidth}x${canvasHeight}:direct=${shouldRenderMaskDirectly}`;
|
||||
const drawMask: TextureCanvasDrawFn = (ctx) => {
|
||||
const elementMaskCanvas = createOffscreenCanvas({
|
||||
width: Math.round(transform.width),
|
||||
height: Math.round(transform.height),
|
||||
});
|
||||
const elementMaskCtx = elementMaskCanvas.getContext("2d") as
|
||||
| CanvasRenderingContext2D
|
||||
| OffscreenCanvasRenderingContext2D
|
||||
| null;
|
||||
if (!elementMaskCtx) return;
|
||||
const { canvas: elementMaskCanvas, context: elementMaskCtx } =
|
||||
createCanvasSurface({
|
||||
width: Math.round(transform.width),
|
||||
height: Math.round(transform.height),
|
||||
});
|
||||
|
||||
if (shouldRenderMaskDirectly && definition.renderer.renderMask) {
|
||||
definition.renderer.renderMask({
|
||||
@@ -465,15 +461,10 @@ function buildMaskArtifacts({
|
||||
const strokeTextureId = `${path}:mask-stroke`;
|
||||
const strokeContentHash = `stroke:${mask.type}:${JSON.stringify(mask.params)}:${transformHash(transform)}:${canvasWidth}x${canvasHeight}`;
|
||||
const drawStroke: TextureCanvasDrawFn = (ctx) => {
|
||||
const strokeCanvas = createOffscreenCanvas({
|
||||
const { canvas: strokeCanvas, context: strokeCtx } = createCanvasSurface({
|
||||
width: Math.round(transform.width),
|
||||
height: Math.round(transform.height),
|
||||
});
|
||||
const strokeCtx = strokeCanvas.getContext("2d") as
|
||||
| CanvasRenderingContext2D
|
||||
| OffscreenCanvasRenderingContext2D
|
||||
| null;
|
||||
if (!strokeCtx) return;
|
||||
|
||||
if (definition.renderer.renderStroke) {
|
||||
definition.renderer.renderStroke({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createOffscreenCanvas } from "./canvas-utils";
|
||||
import { createCanvasSurface } from "./canvas-utils";
|
||||
import { effectsRegistry, resolveEffectPasses } from "@/effects";
|
||||
import { buildDefaultParamValues } from "@/params/registry";
|
||||
import type { ParamValues } from "@/params";
|
||||
@@ -8,7 +8,7 @@ const PREVIEW_SIZE = 160;
|
||||
const PREVIEW_IMAGE_PATH = "/effects/preview.jpg";
|
||||
|
||||
class EffectPreviewService {
|
||||
private testSourceCanvas: OffscreenCanvas | HTMLCanvasElement | null = null;
|
||||
private testSourceCanvas: OffscreenCanvas | null = null;
|
||||
private previewImageElement: HTMLImageElement | null = null;
|
||||
private onReadyCallbacks = new Set<() => void>();
|
||||
|
||||
@@ -98,7 +98,7 @@ class EffectPreviewService {
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
}): OffscreenCanvas | HTMLCanvasElement | null {
|
||||
}): OffscreenCanvas | null {
|
||||
const isImageReady =
|
||||
this.previewImageElement?.complete &&
|
||||
(this.previewImageElement.naturalWidth ?? 0) > 0;
|
||||
@@ -106,15 +106,8 @@ class EffectPreviewService {
|
||||
return null;
|
||||
}
|
||||
|
||||
const canvas = createOffscreenCanvas({ width, height });
|
||||
const ctx = canvas.getContext("2d") as
|
||||
| CanvasRenderingContext2D
|
||||
| OffscreenCanvasRenderingContext2D
|
||||
| null;
|
||||
if (!ctx) {
|
||||
throw new Error("failed to get 2d context for test source");
|
||||
}
|
||||
ctx.drawImage(this.previewImageElement, 0, 0, width, height);
|
||||
const { canvas, context } = createCanvasSurface({ width, height });
|
||||
context.drawImage(this.previewImageElement, 0, 0, width, height);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
@@ -124,7 +117,7 @@ class EffectPreviewService {
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
}): CanvasImageSource | null {
|
||||
}): OffscreenCanvas | null {
|
||||
if (
|
||||
!this.testSourceCanvas ||
|
||||
this.testSourceCanvas.width !== width ||
|
||||
@@ -141,17 +134,17 @@ class EffectPreviewService {
|
||||
height,
|
||||
passes,
|
||||
}: {
|
||||
source: CanvasImageSource;
|
||||
source: OffscreenCanvas;
|
||||
width: number;
|
||||
height: number;
|
||||
passes: ReturnType<typeof resolveEffectPasses>;
|
||||
}): OffscreenCanvas | HTMLCanvasElement {
|
||||
}): OffscreenCanvas {
|
||||
return gpuRenderer.applyEffect({
|
||||
source,
|
||||
width,
|
||||
height,
|
||||
passes,
|
||||
}) as OffscreenCanvas | HTMLCanvasElement;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,23 +34,17 @@ export const gpuRenderer = {
|
||||
height,
|
||||
passes,
|
||||
}: {
|
||||
source: CanvasImageSource;
|
||||
source: OffscreenCanvas;
|
||||
width: number;
|
||||
height: number;
|
||||
passes: EffectPass[];
|
||||
}): CanvasImageSource {
|
||||
}): OffscreenCanvas {
|
||||
if (passes.length === 0 || !gpuAvailable) {
|
||||
return source;
|
||||
}
|
||||
|
||||
const sourceCanvas = ensureOffscreenCanvas({
|
||||
source,
|
||||
width,
|
||||
height,
|
||||
label: "effect source",
|
||||
});
|
||||
return applyEffectPasses({
|
||||
source: sourceCanvas,
|
||||
source,
|
||||
width,
|
||||
height,
|
||||
passes: serializeEffectPasses(passes),
|
||||
@@ -63,23 +57,17 @@ export const gpuRenderer = {
|
||||
height,
|
||||
feather,
|
||||
}: {
|
||||
maskCanvas: CanvasImageSource;
|
||||
maskCanvas: OffscreenCanvas;
|
||||
width: number;
|
||||
height: number;
|
||||
feather: number;
|
||||
}): CanvasImageSource {
|
||||
}): OffscreenCanvas {
|
||||
if (!gpuAvailable) {
|
||||
return maskCanvas;
|
||||
}
|
||||
|
||||
const sourceCanvas = ensureOffscreenCanvas({
|
||||
source: maskCanvas,
|
||||
width,
|
||||
height,
|
||||
label: "mask source",
|
||||
});
|
||||
return applyMaskFeatherWasm({
|
||||
mask: sourceCanvas,
|
||||
mask: maskCanvas,
|
||||
width,
|
||||
height,
|
||||
feather,
|
||||
@@ -87,35 +75,6 @@ export const gpuRenderer = {
|
||||
},
|
||||
};
|
||||
|
||||
function ensureOffscreenCanvas({
|
||||
source,
|
||||
width,
|
||||
height,
|
||||
label,
|
||||
}: {
|
||||
source: CanvasImageSource;
|
||||
width: number;
|
||||
height: number;
|
||||
label: string;
|
||||
}): OffscreenCanvas {
|
||||
if (source instanceof OffscreenCanvas) {
|
||||
return source;
|
||||
}
|
||||
|
||||
if (typeof OffscreenCanvas === "undefined") {
|
||||
throw new Error(`OffscreenCanvas is required for the GPU ${label}`);
|
||||
}
|
||||
|
||||
const canvas = new OffscreenCanvas(width, height);
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
throw new Error(`Failed to get 2d context for the GPU ${label}`);
|
||||
}
|
||||
context.clearRect(0, 0, width, height);
|
||||
context.drawImage(source, 0, 0, width, height);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function serializeEffectPasses(passes: EffectPass[]) {
|
||||
return passes.map((pass) => ({
|
||||
shader: pass.shader,
|
||||
|
||||
@@ -6,15 +6,15 @@ export function applyMaskFeather({
|
||||
height,
|
||||
feather,
|
||||
}: {
|
||||
maskCanvas: CanvasImageSource;
|
||||
maskCanvas: OffscreenCanvas;
|
||||
width: number;
|
||||
height: number;
|
||||
feather: number;
|
||||
}): OffscreenCanvas | HTMLCanvasElement {
|
||||
}): OffscreenCanvas {
|
||||
return gpuRenderer.applyMaskFeather({
|
||||
maskCanvas,
|
||||
width,
|
||||
height,
|
||||
feather,
|
||||
}) as OffscreenCanvas | HTMLCanvasElement;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createOffscreenCanvas } from "../canvas-utils";
|
||||
import { createCanvasSurface } from "../canvas-utils";
|
||||
import {
|
||||
DEFAULT_GRAPHIC_SOURCE_SIZE,
|
||||
getGraphicDefinition,
|
||||
@@ -25,7 +25,7 @@ export class GraphicNode extends VisualNode<
|
||||
ResolvedGraphicNodeState
|
||||
> {
|
||||
private cachedKey: string | null = null;
|
||||
private cachedSource: OffscreenCanvas | HTMLCanvasElement | null = null;
|
||||
private cachedSource: OffscreenCanvas | null = null;
|
||||
|
||||
constructor(params: GraphicNodeParams) {
|
||||
super(params);
|
||||
@@ -36,7 +36,7 @@ export class GraphicNode extends VisualNode<
|
||||
resolvedParams,
|
||||
}: {
|
||||
resolvedParams: ParamValues;
|
||||
}): OffscreenCanvas | HTMLCanvasElement | null {
|
||||
}): OffscreenCanvas {
|
||||
const definition = getGraphicDefinition({
|
||||
definitionId: this.params.definitionId,
|
||||
});
|
||||
@@ -48,20 +48,13 @@ export class GraphicNode extends VisualNode<
|
||||
return this.cachedSource;
|
||||
}
|
||||
|
||||
const canvas = createOffscreenCanvas({
|
||||
const { canvas, context } = createCanvasSurface({
|
||||
width: DEFAULT_GRAPHIC_SOURCE_SIZE,
|
||||
height: DEFAULT_GRAPHIC_SOURCE_SIZE,
|
||||
});
|
||||
const ctx = canvas.getContext("2d") as
|
||||
| CanvasRenderingContext2D
|
||||
| OffscreenCanvasRenderingContext2D
|
||||
| null;
|
||||
if (!ctx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
definition.render({
|
||||
ctx,
|
||||
ctx: context,
|
||||
params: resolvedParams,
|
||||
width: DEFAULT_GRAPHIC_SOURCE_SIZE,
|
||||
height: DEFAULT_GRAPHIC_SOURCE_SIZE,
|
||||
|
||||
@@ -17,10 +17,13 @@ export interface CachedImageSource {
|
||||
|
||||
const imageSourceCache = new Map<string, Promise<CachedImageSource>>();
|
||||
|
||||
export function loadImageSource(
|
||||
url: string,
|
||||
maxSourceSize?: number,
|
||||
): Promise<CachedImageSource> {
|
||||
export function loadImageSource({
|
||||
url,
|
||||
maxSourceSize,
|
||||
}: {
|
||||
url: string;
|
||||
maxSourceSize?: number;
|
||||
}): Promise<CachedImageSource> {
|
||||
const cacheKey = `${url}::${maxSourceSize ?? "full"}`;
|
||||
|
||||
const cached = imageSourceCache.get(cacheKey);
|
||||
|
||||
@@ -235,10 +235,10 @@ async function resolveImageNode({
|
||||
node: ImageNode;
|
||||
context: ResolveContext;
|
||||
}): Promise<ResolvedVisualSourceNodeState | null> {
|
||||
const source = await loadImageSource(
|
||||
node.params.url,
|
||||
node.params.maxSourceSize,
|
||||
);
|
||||
const source = await loadImageSource({
|
||||
url: node.params.url,
|
||||
maxSourceSize: node.params.maxSourceSize,
|
||||
});
|
||||
const visualState = resolveVisualState({
|
||||
params: node.params,
|
||||
context,
|
||||
@@ -434,7 +434,7 @@ async function resolveBackdropSource({
|
||||
};
|
||||
}
|
||||
|
||||
const source = await loadImageSource(node.params.url);
|
||||
const source = await loadImageSource({ url: node.params.url });
|
||||
return {
|
||||
source: source.source,
|
||||
width: source.width,
|
||||
|
||||
@@ -5,7 +5,15 @@ export class IndexedDBAdapter<T> implements StorageAdapter<T> {
|
||||
private storeName: string;
|
||||
private version: number;
|
||||
|
||||
constructor(dbName: string, storeName: string, version = 1) {
|
||||
constructor({
|
||||
dbName,
|
||||
storeName,
|
||||
version = 1,
|
||||
}: {
|
||||
dbName: string;
|
||||
storeName: string;
|
||||
version?: number;
|
||||
}) {
|
||||
this.dbName = dbName;
|
||||
this.storeName = storeName;
|
||||
this.version = version;
|
||||
@@ -39,7 +47,13 @@ export class IndexedDBAdapter<T> implements StorageAdapter<T> {
|
||||
});
|
||||
}
|
||||
|
||||
async set(key: string, value: T): Promise<void> {
|
||||
async set({
|
||||
key,
|
||||
value,
|
||||
}: {
|
||||
key: string;
|
||||
value: T;
|
||||
}): Promise<void> {
|
||||
const db = await this.getDB();
|
||||
const transaction = db.transaction([this.storeName], "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Type-safe accessors for navigating migration output.
|
||||
*
|
||||
* Migrations input/output `Record<string, unknown>` because they handle
|
||||
* potentially malformed data from older versions. Tests need to inspect
|
||||
* specific properties of the migrated result without using type assertions.
|
||||
*
|
||||
* These helpers narrow through type guards (Array.isArray + a record
|
||||
* predicate), so the file contains no unsafe assertions despite turning
|
||||
* unknown into concrete shapes.
|
||||
*/
|
||||
|
||||
function describe(value: unknown): string {
|
||||
if (value === null) return "null";
|
||||
if (Array.isArray(value)) return "array";
|
||||
return typeof value;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function asRecord(value: unknown): Record<string, unknown> {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`Expected record, got ${describe(value)}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function asArray(value: unknown): unknown[] {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`Expected array, got ${describe(value)}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function asRecordArray(value: unknown): Record<string, unknown>[] {
|
||||
return asArray(value).map(asRecord);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
v0ProjectWithMetadata,
|
||||
v1Project,
|
||||
} from "./fixtures";
|
||||
import { asArray, asRecord, asRecordArray } from "./helpers";
|
||||
|
||||
describe("V0 to V1 Migration", () => {
|
||||
const fixedDate = new Date("2024-06-01T12:00:00.000Z");
|
||||
@@ -22,7 +23,7 @@ describe("V0 to V1 Migration", () => {
|
||||
expect(result.skipped).toBe(false);
|
||||
expect(result.project.version).toBe(1);
|
||||
expect(Array.isArray(result.project.scenes)).toBe(true);
|
||||
expect((result.project.scenes as unknown[]).length).toBe(1);
|
||||
expect(asArray(result.project.scenes).length).toBe(1);
|
||||
expect(result.project.currentSceneId).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -32,7 +33,7 @@ describe("V0 to V1 Migration", () => {
|
||||
options: { now: fixedDate },
|
||||
});
|
||||
|
||||
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||
const scenes = asRecordArray(result.project.scenes);
|
||||
const mainScene = scenes[0];
|
||||
|
||||
expect(mainScene.isMain).toBe(true);
|
||||
@@ -48,7 +49,7 @@ describe("V0 to V1 Migration", () => {
|
||||
options: { now: fixedDate },
|
||||
});
|
||||
|
||||
const metadata = result.project.metadata as Record<string, unknown>;
|
||||
const metadata = asRecord(result.project.metadata);
|
||||
expect(metadata.updatedAt).toBe(fixedDate.toISOString());
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
v1ProjectWithMultipleScenes,
|
||||
v2Project,
|
||||
} from "./fixtures";
|
||||
import { asRecord, asRecordArray } from "./helpers";
|
||||
|
||||
const DEFAULT_FPS = 30;
|
||||
const DEFAULT_BACKGROUND_BLUR_INTENSITY = 10;
|
||||
@@ -25,7 +26,7 @@ describe("V1 to V2 Migration", () => {
|
||||
expect(result.skipped).toBe(false);
|
||||
expect(result.project.version).toBe(2);
|
||||
|
||||
const metadata = result.project.metadata as Record<string, unknown>;
|
||||
const metadata = asRecord(result.project.metadata);
|
||||
expect(metadata.id).toBe(v1Project.id);
|
||||
expect(metadata.name).toBe(v1Project.name);
|
||||
expect(typeof metadata.createdAt).toBe("string");
|
||||
@@ -35,7 +36,7 @@ describe("V1 to V2 Migration", () => {
|
||||
test("creates settings object from flat properties", () => {
|
||||
const result = transformProjectV1ToV2({ project: v1Project });
|
||||
|
||||
const settings = result.project.settings as Record<string, unknown>;
|
||||
const settings = asRecord(result.project.settings);
|
||||
expect(settings.fps).toBe(v1Project.fps);
|
||||
expect(settings.canvasSize).toEqual(v1Project.canvasSize);
|
||||
expect(settings.originalCanvasSize).toBe(null);
|
||||
@@ -44,8 +45,8 @@ describe("V1 to V2 Migration", () => {
|
||||
test("converts color background correctly", () => {
|
||||
const result = transformProjectV1ToV2({ project: v1Project });
|
||||
|
||||
const settings = result.project.settings as Record<string, unknown>;
|
||||
const background = settings.background as Record<string, unknown>;
|
||||
const settings = asRecord(result.project.settings);
|
||||
const background = asRecord(settings.background);
|
||||
expect(background.type).toBe("color");
|
||||
expect(background.color).toBe(v1Project.backgroundColor);
|
||||
});
|
||||
@@ -58,8 +59,8 @@ describe("V1 to V2 Migration", () => {
|
||||
};
|
||||
const result = transformProjectV1ToV2({ project: projectWithBlur });
|
||||
|
||||
const settings = result.project.settings as Record<string, unknown>;
|
||||
const background = settings.background as Record<string, unknown>;
|
||||
const settings = asRecord(result.project.settings);
|
||||
const background = asRecord(settings.background);
|
||||
expect(background.type).toBe("blur");
|
||||
expect(background.blurIntensity).toBe(30);
|
||||
});
|
||||
@@ -67,7 +68,7 @@ describe("V1 to V2 Migration", () => {
|
||||
test("applies legacy bookmarks to main scene", () => {
|
||||
const result = transformProjectV1ToV2({ project: v1Project });
|
||||
|
||||
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||
const scenes = asRecordArray(result.project.scenes);
|
||||
const mainScene = scenes.find((s) => s.isMain === true);
|
||||
expect(mainScene?.bookmarks).toEqual(v1Project.bookmarks);
|
||||
});
|
||||
@@ -77,7 +78,7 @@ describe("V1 to V2 Migration", () => {
|
||||
project: v1ProjectWithMultipleScenes,
|
||||
});
|
||||
|
||||
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||
const scenes = asRecordArray(result.project.scenes);
|
||||
const introScene = scenes.find((s) => s.name === "Intro");
|
||||
expect(introScene?.bookmarks).toEqual([1.0]);
|
||||
});
|
||||
@@ -102,7 +103,7 @@ describe("V1 to V2 Migration", () => {
|
||||
});
|
||||
|
||||
expect(result.skipped).toBe(false);
|
||||
const settings = result.project.settings as Record<string, unknown>;
|
||||
const settings = asRecord(result.project.settings);
|
||||
expect(settings.fps).toBe(DEFAULT_FPS);
|
||||
expect(settings.canvasSize).toEqual(DEFAULT_CANVAS_SIZE);
|
||||
});
|
||||
@@ -115,11 +116,11 @@ describe("V1 to V2 Migration", () => {
|
||||
};
|
||||
const result = transformProjectV1ToV2({ project: minimalProject });
|
||||
|
||||
const settings = result.project.settings as Record<string, unknown>;
|
||||
const settings = asRecord(result.project.settings);
|
||||
expect(settings.fps).toBe(DEFAULT_FPS);
|
||||
expect(settings.canvasSize).toEqual(DEFAULT_CANVAS_SIZE);
|
||||
|
||||
const background = settings.background as Record<string, unknown>;
|
||||
const background = asRecord(settings.background);
|
||||
expect(background.type).toBe("color");
|
||||
expect(background.color).toBe(DEFAULT_BACKGROUND_COLOR);
|
||||
});
|
||||
@@ -135,8 +136,8 @@ describe("V1 to V2 Migration", () => {
|
||||
project: projectWithBlurNoIntensity,
|
||||
});
|
||||
|
||||
const settings = result.project.settings as Record<string, unknown>;
|
||||
const background = settings.background as Record<string, unknown>;
|
||||
const settings = asRecord(result.project.settings);
|
||||
const background = asRecord(settings.background);
|
||||
expect(background.blurIntensity).toBe(DEFAULT_BACKGROUND_BLUR_INTENSITY);
|
||||
});
|
||||
|
||||
@@ -183,9 +184,9 @@ describe("V1 to V2 Migration", () => {
|
||||
project: projectWithTracks,
|
||||
});
|
||||
|
||||
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||
const scenes = asRecordArray(result.project.scenes);
|
||||
const mainScene = scenes[0];
|
||||
const tracks = mainScene.tracks as Array<Record<string, unknown>>;
|
||||
const tracks = asRecordArray(mainScene.tracks);
|
||||
expect(tracks.length).toBe(1);
|
||||
expect(tracks[0].name).toBe("Existing Track");
|
||||
});
|
||||
@@ -240,9 +241,9 @@ describe("V1 to V2 Migration", () => {
|
||||
context,
|
||||
});
|
||||
|
||||
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||
const scenes = asRecordArray(result.project.scenes);
|
||||
const mainScene = scenes[0];
|
||||
const tracks = mainScene.tracks as Array<Record<string, unknown>>;
|
||||
const tracks = asRecordArray(mainScene.tracks);
|
||||
expect(Array.isArray(tracks)).toBe(true);
|
||||
expect(tracks).toHaveLength(1);
|
||||
expect(tracks[0].type).toBe("video");
|
||||
@@ -302,9 +303,9 @@ describe("V1 to V2 Migration", () => {
|
||||
});
|
||||
|
||||
expect(result.skipped).toBe(false);
|
||||
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||
const tracks = scenes[0].tracks as Array<Record<string, unknown>>;
|
||||
const elements = tracks[0].elements as Array<Record<string, unknown>>;
|
||||
const scenes = asRecordArray(result.project.scenes);
|
||||
const tracks = asRecordArray(scenes[0].tracks);
|
||||
const elements = asRecordArray(tracks[0].elements);
|
||||
const textElement = elements[0];
|
||||
expect(textElement.opacity).toBe(0.5);
|
||||
expect(textElement.transform).toEqual({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { transformProjectV15ToV16 } from "../transformers/v15-to-v16";
|
||||
import { asRecordArray } from "./helpers";
|
||||
|
||||
describe("V15 to V16 Migration", () => {
|
||||
test("renames sticker tracks to graphic tracks", () => {
|
||||
@@ -61,10 +62,10 @@ describe("V15 to V16 Migration", () => {
|
||||
|
||||
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");
|
||||
const firstTrack = asRecordArray(
|
||||
asRecordArray(result.project.scenes)[0].tracks,
|
||||
)[0];
|
||||
expect(firstTrack.type).toBe("graphic");
|
||||
});
|
||||
|
||||
test("skips projects already on v16", () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { transformProjectV16ToV17 } from "../transformers/v16-to-v17";
|
||||
import { asRecord, asRecordArray } from "./helpers";
|
||||
|
||||
describe("V16 to V17 Migration", () => {
|
||||
test("adds center stroke alignment to masks that do not have it", () => {
|
||||
@@ -97,13 +98,13 @@ describe("V16 to V17 Migration", () => {
|
||||
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)
|
||||
);
|
||||
const firstScene = asRecordArray(result.project.scenes)[0];
|
||||
const firstTrack = asRecordArray(firstScene.tracks)[0];
|
||||
const firstElement = asRecordArray(firstTrack.elements)[0];
|
||||
const migratedMasks = asRecordArray(firstElement.masks);
|
||||
|
||||
expect(migratedMasks[0].params.strokeAlign).toBe("center");
|
||||
expect(migratedMasks[1].params.strokeAlign).toBe("outside");
|
||||
expect(asRecord(migratedMasks[0].params).strokeAlign).toBe("center");
|
||||
expect(asRecord(migratedMasks[1].params).strokeAlign).toBe("outside");
|
||||
});
|
||||
|
||||
test("skips projects already on v17", () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { transformProjectV18ToV19 } from "../transformers/v18-to-v19";
|
||||
import { asRecord } from "./helpers";
|
||||
|
||||
describe("V18 to V19 Migration", () => {
|
||||
test("adds canvas size mode and empty remembered custom size defaults", () => {
|
||||
@@ -26,15 +27,13 @@ describe("V18 to V19 Migration", () => {
|
||||
|
||||
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 });
|
||||
const settings = asRecord(result.project.settings);
|
||||
expect(settings.canvasSizeMode).toBe("preset");
|
||||
expect(settings.lastCustomCanvasSize).toBeNull();
|
||||
expect(settings.originalCanvasSize).toEqual({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
});
|
||||
});
|
||||
|
||||
test("skips projects already on v19", () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { transformProjectV19ToV20 } from "../transformers/v19-to-v20";
|
||||
import { asRecordArray } from "./helpers";
|
||||
|
||||
describe("V19 to V20 Migration", () => {
|
||||
test("backfills source audio enabled state on video elements", () => {
|
||||
@@ -82,19 +83,16 @@ describe("V19 to V20 Migration", () => {
|
||||
|
||||
expect(result.skipped).toBe(false);
|
||||
expect(result.project.version).toBe(20);
|
||||
expect(
|
||||
((result.project.scenes as Array<Record<string, unknown>>)[0]
|
||||
.tracks as Array<Record<string, unknown>>)[0].elements,
|
||||
).toEqual([
|
||||
const scene = asRecordArray(result.project.scenes)[0];
|
||||
const tracks = asRecordArray(scene.tracks);
|
||||
expect(tracks[0].elements).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "video-1",
|
||||
isSourceAudioEnabled: true,
|
||||
}),
|
||||
]);
|
||||
expect(
|
||||
(((result.project.scenes as Array<Record<string, unknown>>)[0]
|
||||
.tracks as Array<Record<string, unknown>>)[1]
|
||||
.elements as Array<Record<string, unknown>>)[0].isSourceAudioEnabled,
|
||||
asRecordArray(tracks[1].elements)[0].isSourceAudioEnabled,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -120,11 +118,10 @@ describe("V19 to V20 Migration", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
(((result.project.scenes as Array<Record<string, unknown>>)[0]
|
||||
.tracks as Array<Record<string, unknown>>)[0]
|
||||
.elements as Array<Record<string, unknown>>)[0].isSourceAudioEnabled,
|
||||
).toBe(false);
|
||||
const scene = asRecordArray(result.project.scenes)[0];
|
||||
const track = asRecordArray(scene.tracks)[0];
|
||||
const element = asRecordArray(track.elements)[0];
|
||||
expect(element.isSourceAudioEnabled).toBe(false);
|
||||
});
|
||||
|
||||
test("skips projects already on v20", () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
v2ProjectWithBlurBackground,
|
||||
v3Project,
|
||||
} from "./fixtures";
|
||||
import { asRecord } from "./helpers";
|
||||
|
||||
describe("V2 to V3 Migration", () => {
|
||||
describe("transformProjectV2ToV3", () => {
|
||||
@@ -17,14 +18,14 @@ describe("V2 to V3 Migration", () => {
|
||||
expect(result.skipped).toBe(false);
|
||||
expect(result.project.version).toBe(3);
|
||||
|
||||
const metadata = result.project.metadata as Record<string, unknown>;
|
||||
const metadata = asRecord(result.project.metadata);
|
||||
expect(typeof metadata.duration).toBe("number");
|
||||
});
|
||||
|
||||
test("calculates duration from scene tracks", () => {
|
||||
const result = transformProjectV2ToV3({ project: v2Project });
|
||||
|
||||
const metadata = result.project.metadata as Record<string, unknown>;
|
||||
const metadata = asRecord(result.project.metadata);
|
||||
// v2Project has a video element with duration 15.5 and a text element at startTime 2 with duration 5
|
||||
// Total duration should be max(15.5, 2+5) = 15.5
|
||||
expect(metadata.duration).toBe(15.5);
|
||||
@@ -36,7 +37,7 @@ describe("V2 to V3 Migration", () => {
|
||||
});
|
||||
|
||||
expect(result.skipped).toBe(false);
|
||||
const metadata = result.project.metadata as Record<string, unknown>;
|
||||
const metadata = asRecord(result.project.metadata);
|
||||
// v2ProjectWithBlurBackground has a video with duration 30
|
||||
expect(metadata.duration).toBe(30);
|
||||
});
|
||||
@@ -45,7 +46,7 @@ describe("V2 to V3 Migration", () => {
|
||||
const result = transformProjectV2ToV3({ project: v2ProjectEmptyScenes });
|
||||
|
||||
expect(result.skipped).toBe(false);
|
||||
const metadata = result.project.metadata as Record<string, unknown>;
|
||||
const metadata = asRecord(result.project.metadata);
|
||||
expect(metadata.duration).toBe(0);
|
||||
});
|
||||
|
||||
@@ -55,7 +56,7 @@ describe("V2 to V3 Migration", () => {
|
||||
});
|
||||
|
||||
expect(result.skipped).toBe(false);
|
||||
const metadata = result.project.metadata as Record<string, unknown>;
|
||||
const metadata = asRecord(result.project.metadata);
|
||||
expect(metadata.duration).toBe(0);
|
||||
});
|
||||
|
||||
@@ -91,7 +92,7 @@ describe("V2 to V3 Migration", () => {
|
||||
test("preserves existing metadata fields", () => {
|
||||
const result = transformProjectV2ToV3({ project: v2Project });
|
||||
|
||||
const metadata = result.project.metadata as Record<string, unknown>;
|
||||
const metadata = asRecord(result.project.metadata);
|
||||
expect(metadata.id).toBe(v2Project.metadata.id);
|
||||
expect(metadata.name).toBe(v2Project.metadata.name);
|
||||
expect(metadata.thumbnail).toBe(v2Project.metadata.thumbnail);
|
||||
@@ -122,7 +123,7 @@ describe("V2 to V3 Migration", () => {
|
||||
});
|
||||
|
||||
expect(result.skipped).toBe(false);
|
||||
const metadata = result.project.metadata as Record<string, unknown>;
|
||||
const metadata = asRecord(result.project.metadata);
|
||||
expect(metadata.duration).toBe(0);
|
||||
});
|
||||
|
||||
@@ -156,7 +157,7 @@ describe("V2 to V3 Migration", () => {
|
||||
};
|
||||
const result = transformProjectV2ToV3({ project: multiSceneProject });
|
||||
|
||||
const metadata = result.project.metadata as Record<string, unknown>;
|
||||
const metadata = asRecord(result.project.metadata);
|
||||
// Duration is from main scene only, not sum of all scenes
|
||||
expect(metadata.duration).toBe(10);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { transformProjectV20ToV21 } from "../transformers/v20-to-v21";
|
||||
import { asRecord } from "./helpers";
|
||||
|
||||
const LEGACY_DEFAULT_BACKGROUND_BLUR_INTENSITY = 50;
|
||||
|
||||
@@ -27,8 +28,8 @@ describe("V20 to V21 Migration", () => {
|
||||
|
||||
expect(result.skipped).toBe(false);
|
||||
expect(result.project.version).toBe(21);
|
||||
const settings = result.project.settings as Record<string, unknown>;
|
||||
const background = settings.background as Record<string, unknown>;
|
||||
const settings = asRecord(result.project.settings);
|
||||
const background = asRecord(settings.background);
|
||||
expect(background.blurIntensity).toBe(500);
|
||||
});
|
||||
|
||||
@@ -45,8 +46,8 @@ describe("V20 to V21 Migration", () => {
|
||||
});
|
||||
|
||||
expect(result.skipped).toBe(false);
|
||||
const settings = result.project.settings as Record<string, unknown>;
|
||||
const background = settings.background as Record<string, unknown>;
|
||||
const settings = asRecord(result.project.settings);
|
||||
const background = asRecord(settings.background);
|
||||
expect(background.blurIntensity).toBe(
|
||||
LEGACY_DEFAULT_BACKGROUND_BLUR_INTENSITY,
|
||||
);
|
||||
@@ -65,7 +66,7 @@ describe("V20 to V21 Migration", () => {
|
||||
});
|
||||
|
||||
expect(result.skipped).toBe(false);
|
||||
const settings = result.project.settings as Record<string, unknown>;
|
||||
const settings = asRecord(result.project.settings);
|
||||
expect(settings.background).toEqual({ type: "color", color: "#000000" });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { transformProjectV21ToV22 } from "../transformers/v21-to-v22";
|
||||
import { asRecord, asRecordArray } from "./helpers";
|
||||
|
||||
describe("V21 to V22 Migration", () => {
|
||||
test("migrates legacy animation channels to bindings and component channels", () => {
|
||||
@@ -77,12 +78,12 @@ describe("V21 to V22 Migration", () => {
|
||||
expect(result.skipped).toBe(false);
|
||||
expect(result.project.version).toBe(22);
|
||||
|
||||
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||
const tracks = scenes[0].tracks as Array<Record<string, unknown>>;
|
||||
const elements = tracks[0].elements as Array<Record<string, unknown>>;
|
||||
const animations = elements[0].animations as Record<string, unknown>;
|
||||
const bindings = animations.bindings as Record<string, Record<string, unknown>>;
|
||||
const channels = animations.channels as Record<string, Record<string, unknown>>;
|
||||
const scenes = asRecordArray(result.project.scenes);
|
||||
const tracks = asRecordArray(scenes[0].tracks);
|
||||
const elements = asRecordArray(tracks[0].elements);
|
||||
const animations = asRecord(elements[0].animations);
|
||||
const bindings = asRecord(animations.bindings);
|
||||
const channels = asRecord(animations.channels);
|
||||
|
||||
expect(bindings.opacity).toEqual({
|
||||
path: "opacity",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { transformProjectV22ToV23 } from "../transformers/v22-to-v23";
|
||||
import { asRecord, asRecordArray } from "./helpers";
|
||||
|
||||
describe("V22 to V23 Migration", () => {
|
||||
test("converts project time values from seconds to ticks and fps to a frame-rate object", () => {
|
||||
@@ -101,20 +102,17 @@ describe("V22 to V23 Migration", () => {
|
||||
expect(result.skipped).toBe(false);
|
||||
expect(result.project.version).toBe(23);
|
||||
|
||||
const metadata = result.project.metadata as Record<string, unknown>;
|
||||
const metadata = asRecord(result.project.metadata);
|
||||
expect(metadata.duration).toBe(1_860_000);
|
||||
|
||||
const settings = result.project.settings as Record<string, unknown>;
|
||||
const settings = asRecord(result.project.settings);
|
||||
expect(settings.fps).toEqual({ numerator: 30_000, denominator: 1_001 });
|
||||
|
||||
const timelineViewState = result.project.timelineViewState as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const timelineViewState = asRecord(result.project.timelineViewState);
|
||||
expect(timelineViewState.playheadTime).toBe(150_000);
|
||||
expect(timelineViewState.scrollLeft).toBe(120);
|
||||
|
||||
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||
const scenes = asRecordArray(result.project.scenes);
|
||||
const scene = scenes[0];
|
||||
expect(scene.bookmarks).toEqual([
|
||||
{
|
||||
@@ -126,8 +124,8 @@ describe("V22 to V23 Migration", () => {
|
||||
{ time: 540_000 },
|
||||
]);
|
||||
|
||||
const tracks = scene.tracks as Array<Record<string, unknown>>;
|
||||
const elements = tracks[0].elements as Array<Record<string, unknown>>;
|
||||
const tracks = asRecordArray(scene.tracks);
|
||||
const elements = asRecordArray(tracks[0].elements);
|
||||
const element = elements[0];
|
||||
expect(element.startTime).toBe(150_000);
|
||||
expect(element.duration).toBe(660_000);
|
||||
@@ -135,8 +133,8 @@ describe("V22 to V23 Migration", () => {
|
||||
expect(element.trimEnd).toBe(60_000);
|
||||
expect(element.sourceDuration).toBe(750_000);
|
||||
|
||||
const animations = element.animations as Record<string, unknown>;
|
||||
const channels = animations.channels as Record<string, Record<string, unknown>>;
|
||||
const animations = asRecord(element.animations);
|
||||
const channels = asRecord(animations.channels);
|
||||
expect(channels["opacity:value"]).toEqual({
|
||||
kind: "scalar",
|
||||
keys: [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { transformProjectV26ToV27 } from "../transformers/v26-to-v27";
|
||||
import { asRecord, asRecordArray } from "./helpers";
|
||||
|
||||
describe("V26 to V27 Migration", () => {
|
||||
test("converts custom mask paths from JSON strings to typed point arrays", () => {
|
||||
@@ -59,13 +60,13 @@ describe("V26 to V27 Migration", () => {
|
||||
expect(result.skipped).toBe(false);
|
||||
expect(result.project.version).toBe(27);
|
||||
|
||||
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||
const tracks = scenes[0].tracks as Record<string, unknown>;
|
||||
const mainTrack = tracks.main as Record<string, unknown>;
|
||||
const elements = mainTrack.elements as Array<Record<string, unknown>>;
|
||||
const masks = elements[0].masks as Array<Record<string, unknown>>;
|
||||
const customParams = masks[0].params as Record<string, unknown>;
|
||||
const rectangleParams = masks[1].params as Record<string, unknown>;
|
||||
const scenes = asRecordArray(result.project.scenes);
|
||||
const tracks = asRecord(scenes[0].tracks);
|
||||
const mainTrack = asRecord(tracks.main);
|
||||
const elements = asRecordArray(mainTrack.elements);
|
||||
const masks = asRecordArray(elements[0].masks);
|
||||
const customParams = asRecord(masks[0].params);
|
||||
const rectangleParams = asRecord(masks[1].params);
|
||||
|
||||
expect(customParams.path).toEqual([
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { transformProjectV27ToV28 } from "../transformers/v27-to-v28";
|
||||
import { asRecord, asRecordArray } from "./helpers";
|
||||
|
||||
describe("V27 to V28 Migration", () => {
|
||||
test("rounds persisted media-time floats back to integer ticks", () => {
|
||||
@@ -96,18 +97,15 @@ describe("V27 to V28 Migration", () => {
|
||||
expect(result.skipped).toBe(false);
|
||||
expect(result.project.version).toBe(28);
|
||||
|
||||
const metadata = result.project.metadata as Record<string, unknown>;
|
||||
const metadata = asRecord(result.project.metadata);
|
||||
expect(metadata.duration).toBe(2_152_466);
|
||||
|
||||
const timelineViewState = result.project.timelineViewState as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const timelineViewState = asRecord(result.project.timelineViewState);
|
||||
expect(timelineViewState.playheadTime).toBe(301_235);
|
||||
expect(timelineViewState.zoomLevel).toBe(1.25);
|
||||
expect(timelineViewState.scrollLeft).toBe(120);
|
||||
|
||||
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||
const scenes = asRecordArray(result.project.scenes);
|
||||
const scene = scenes[0];
|
||||
expect(scene.bookmarks).toEqual([
|
||||
{
|
||||
@@ -118,9 +116,9 @@ describe("V27 to V28 Migration", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const tracks = scene.tracks as Record<string, unknown>;
|
||||
const mainTrack = tracks.main as Record<string, unknown>;
|
||||
const elements = mainTrack.elements as Array<Record<string, unknown>>;
|
||||
const tracks = asRecord(scene.tracks);
|
||||
const mainTrack = asRecord(tracks.main);
|
||||
const elements = asRecordArray(mainTrack.elements);
|
||||
const element = elements[0];
|
||||
expect(element.startTime).toBe(300_000);
|
||||
expect(element.duration).toBe(2_152_466);
|
||||
@@ -128,9 +126,9 @@ describe("V27 to V28 Migration", () => {
|
||||
expect(element.trimEnd).toBe(15_001);
|
||||
expect(element.sourceDuration).toBe(2_197_468);
|
||||
|
||||
const animations = element.animations as Record<string, unknown>;
|
||||
const channels = animations.channels as Record<string, Record<string, unknown>>;
|
||||
const opacityChannel = channels.opacity;
|
||||
const animations = asRecord(element.animations);
|
||||
const channels = asRecord(animations.channels);
|
||||
const opacityChannel = asRecord(channels.opacity);
|
||||
expect(opacityChannel.keys).toEqual([
|
||||
{
|
||||
id: "key-1",
|
||||
@@ -204,26 +202,23 @@ describe("V27 to V28 Migration", () => {
|
||||
expect(result.skipped).toBe(false);
|
||||
expect(result.project.version).toBe(28);
|
||||
|
||||
const metadata = result.project.metadata as Record<string, unknown>;
|
||||
const metadata = asRecord(result.project.metadata);
|
||||
expect(metadata.duration).toBe(120_000);
|
||||
|
||||
const timelineViewState = result.project.timelineViewState as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const timelineViewState = asRecord(result.project.timelineViewState);
|
||||
expect(timelineViewState).toEqual({
|
||||
zoomLevel: 2,
|
||||
scrollLeft: 300,
|
||||
playheadTime: 30_000,
|
||||
});
|
||||
|
||||
const scenes = result.project.scenes as Array<Record<string, unknown>>;
|
||||
const scenes = asRecordArray(result.project.scenes);
|
||||
const scene = scenes[0];
|
||||
expect(scene.bookmarks).toEqual([{ time: 60_000, duration: 15_000 }]);
|
||||
|
||||
const tracks = scene.tracks as Record<string, unknown>;
|
||||
const mainTrack = tracks.main as Record<string, unknown>;
|
||||
const element = (mainTrack.elements as Array<Record<string, unknown>>)[0];
|
||||
const tracks = asRecord(scene.tracks);
|
||||
const mainTrack = asRecord(tracks.main);
|
||||
const element = asRecordArray(mainTrack.elements)[0];
|
||||
expect(element.startTime).toBe(10_000);
|
||||
expect(element.duration).toBe(20_000);
|
||||
expect(element.trimStart).toBe(1_000);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { transformProjectV3ToV4 } from "../transformers/v3-to-v4";
|
||||
import { v3Project } from "./fixtures";
|
||||
import { asRecordArray } from "./helpers";
|
||||
|
||||
describe("V3 to V4 Migration", () => {
|
||||
test("normalizes legacy text fontWeight values", () => {
|
||||
@@ -54,15 +55,9 @@ describe("V3 to V4 Migration", () => {
|
||||
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];
|
||||
const migratedScene = asRecordArray(result.project.scenes)[0];
|
||||
const migratedTrack = asRecordArray(migratedScene.tracks)[0];
|
||||
const migratedElement = asRecordArray(migratedTrack.elements)[0];
|
||||
|
||||
expect(migratedElement.fontWeight).toBe("700");
|
||||
});
|
||||
@@ -104,15 +99,9 @@ describe("V3 to V4 Migration", () => {
|
||||
};
|
||||
|
||||
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];
|
||||
const migratedScene = asRecordArray(result.project.scenes)[0];
|
||||
const migratedTrack = asRecordArray(migratedScene.tracks)[0];
|
||||
const migratedElement = asRecordArray(migratedTrack.elements)[0];
|
||||
|
||||
expect(migratedElement.iconName).toBe("mdi:home");
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { transformProjectV4ToV5 } from "../transformers/v4-to-v5";
|
||||
import { v3Project } from "./fixtures";
|
||||
import { asRecordArray } from "./helpers";
|
||||
|
||||
describe("V4 to V5 Migration", () => {
|
||||
test("migrates sticker iconName to stickerId and removes legacy color", () => {
|
||||
@@ -48,15 +49,9 @@ describe("V4 to V5 Migration", () => {
|
||||
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];
|
||||
const migratedScene = asRecordArray(result.project.scenes)[0];
|
||||
const migratedTrack = asRecordArray(migratedScene.tracks)[0];
|
||||
const migratedElement = asRecordArray(migratedTrack.elements)[0];
|
||||
|
||||
expect(migratedElement.stickerId).toBe("icons:mdi:home");
|
||||
expect("iconName" in migratedElement).toBe(false);
|
||||
@@ -101,15 +96,9 @@ describe("V4 to V5 Migration", () => {
|
||||
};
|
||||
|
||||
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];
|
||||
const migratedScene = asRecordArray(result.project.scenes)[0];
|
||||
const migratedTrack = asRecordArray(migratedScene.tracks)[0];
|
||||
const migratedElement = asRecordArray(migratedTrack.elements)[0];
|
||||
|
||||
expect(migratedElement.stickerId).toBe("flags:AD");
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { transformProjectV5ToV6 } from "../transformers/v5-to-v6";
|
||||
import { v5Project } from "./fixtures";
|
||||
import { asRecordArray } from "./helpers";
|
||||
|
||||
describe("V5 to V6 Migration", () => {
|
||||
test("converts number bookmarks to Bookmark objects", async () => {
|
||||
@@ -13,19 +14,13 @@ describe("V5 to V6 Migration", () => {
|
||||
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([
|
||||
const scenes = asRecordArray(result.project.scenes);
|
||||
expect(scenes[0].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([]);
|
||||
expect(scenes[1].bookmarks).toEqual([]);
|
||||
});
|
||||
|
||||
test("skips projects that are already v6", () => {
|
||||
@@ -83,9 +78,7 @@ describe("V5 to V6 Migration", () => {
|
||||
});
|
||||
|
||||
expect(result.skipped).toBe(false);
|
||||
const mainScene = (
|
||||
result.project.scenes as Array<{ bookmarks: unknown[] }>
|
||||
)[0];
|
||||
const mainScene = asRecordArray(result.project.scenes)[0];
|
||||
expect(mainScene.bookmarks).toEqual([
|
||||
{ time: 1, note: "Intro", color: "#ef4444" },
|
||||
{ time: 5.5, duration: 2 },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { transformProjectV8ToV9 } from "../transformers/v8-to-v9";
|
||||
import { asRecord, asRecordArray } from "./helpers";
|
||||
|
||||
const v8ProjectWithText = {
|
||||
id: "project-v8-text",
|
||||
@@ -70,16 +71,16 @@ describe("V8 to V9 Migration", () => {
|
||||
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 } }>;
|
||||
const track = asRecordArray(asRecordArray(result.project.scenes)[0].tracks)[0];
|
||||
const elements = asRecordArray(track.elements);
|
||||
const background0 = asRecord(elements[0].background);
|
||||
const background1 = asRecord(elements[1].background);
|
||||
|
||||
expect(elements[0].background.enabled).toBe(true);
|
||||
expect(elements[0].background.color).toBe("#ff0000");
|
||||
expect(background0.enabled).toBe(true);
|
||||
expect(background0.color).toBe("#ff0000");
|
||||
|
||||
expect(elements[1].background.enabled).toBe(false);
|
||||
expect(elements[1].background.color).toBe("transparent");
|
||||
expect(background1.enabled).toBe(false);
|
||||
expect(background1.color).toBe("transparent");
|
||||
});
|
||||
|
||||
test("preserves existing background.enabled if already present", () => {
|
||||
@@ -87,7 +88,7 @@ describe("V8 to V9 Migration", () => {
|
||||
...v8ProjectWithText,
|
||||
scenes: [
|
||||
{
|
||||
...(v8ProjectWithText.scenes as Record<string, unknown>[])[0],
|
||||
...asRecordArray(v8ProjectWithText.scenes)[0],
|
||||
tracks: [
|
||||
{
|
||||
id: "track-text",
|
||||
@@ -116,10 +117,9 @@ describe("V8 to V9 Migration", () => {
|
||||
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);
|
||||
const track = asRecordArray(asRecordArray(result.project.scenes)[0].tracks)[0];
|
||||
const elements = asRecordArray(track.elements);
|
||||
expect(asRecord(elements[0].background).enabled).toBe(false);
|
||||
});
|
||||
|
||||
test("skips non-text elements and tracks", () => {
|
||||
|
||||
@@ -19,34 +19,6 @@ const EMPTY_V1_TO_V2_CONTEXT: V1ToV2Context = {
|
||||
mediaTypesById: {},
|
||||
};
|
||||
|
||||
interface LegacyMediaElement {
|
||||
type: "media";
|
||||
mediaId: string;
|
||||
muted?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface LegacyTextElement {
|
||||
type: "text";
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
opacity: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface LegacyAudioElement {
|
||||
type: "audio";
|
||||
mediaId: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface LegacyMediaTrack {
|
||||
type: "media";
|
||||
elements: unknown[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface V2Transform {
|
||||
scale: number;
|
||||
position: { x: number; y: number };
|
||||
@@ -315,7 +287,7 @@ function transformTracks({
|
||||
const isMain = !isFirstVideoTrackFound;
|
||||
isFirstVideoTrackFound = true;
|
||||
const videoTrack = transformMediaTrack({
|
||||
track: track as LegacyMediaTrack,
|
||||
track,
|
||||
context,
|
||||
isMain,
|
||||
});
|
||||
@@ -343,7 +315,7 @@ function transformMediaTrack({
|
||||
context,
|
||||
isMain,
|
||||
}: {
|
||||
track: LegacyMediaTrack;
|
||||
track: Record<string, unknown>;
|
||||
context: V1ToV2Context;
|
||||
isMain: boolean;
|
||||
}): V2VideoTrack {
|
||||
@@ -354,8 +326,7 @@ function transformMediaTrack({
|
||||
return null;
|
||||
}
|
||||
|
||||
const mediaElement = element as LegacyMediaElement;
|
||||
const mediaId = getStringValue({ value: mediaElement.mediaId });
|
||||
const mediaId = getStringValue({ value: element.mediaId });
|
||||
if (!mediaId) {
|
||||
return null;
|
||||
}
|
||||
@@ -372,7 +343,7 @@ function transformMediaTrack({
|
||||
rotate: 0,
|
||||
};
|
||||
|
||||
const muted = mediaElement.muted === true;
|
||||
const muted = element.muted === true;
|
||||
|
||||
if (mediaType === "image") {
|
||||
const imageElement: V2ImageElement = {
|
||||
@@ -442,15 +413,14 @@ function transformTextTrack({
|
||||
return null;
|
||||
}
|
||||
|
||||
const textElement = element as LegacyTextElement;
|
||||
const x = getNumberValue({ value: textElement.x, fallback: 0 });
|
||||
const y = getNumberValue({ value: textElement.y, fallback: 0 });
|
||||
const x = getNumberValue({ value: element.x, fallback: 0 });
|
||||
const y = getNumberValue({ value: element.y, fallback: 0 });
|
||||
const rotation = getNumberValue({
|
||||
value: textElement.rotation,
|
||||
value: element.rotation,
|
||||
fallback: 0,
|
||||
});
|
||||
const opacity = getNumberValue({
|
||||
value: textElement.opacity,
|
||||
value: element.opacity,
|
||||
fallback: 1,
|
||||
});
|
||||
|
||||
@@ -464,23 +434,23 @@ function transformTextTrack({
|
||||
id: getStringValue({ value: element.id, fallback: "" }),
|
||||
name: getStringValue({ value: element.name, fallback: "" }),
|
||||
type: "text",
|
||||
content: getStringValue({ value: textElement.content, fallback: "" }),
|
||||
content: getStringValue({ value: element.content, fallback: "" }),
|
||||
fontSize: getNumberValue({
|
||||
value: textElement.fontSize,
|
||||
value: element.fontSize,
|
||||
fallback: 16,
|
||||
}),
|
||||
fontFamily: getStringValue({
|
||||
value: textElement.fontFamily,
|
||||
value: element.fontFamily,
|
||||
fallback: "Arial",
|
||||
}),
|
||||
color: getStringValue({
|
||||
value: textElement.color,
|
||||
value: element.color,
|
||||
fallback: "#000000",
|
||||
}),
|
||||
background: {
|
||||
enabled: false,
|
||||
color: getStringValue({
|
||||
value: textElement.backgroundColor,
|
||||
value: element.backgroundColor,
|
||||
fallback: "transparent",
|
||||
}),
|
||||
cornerRadius: 0,
|
||||
@@ -489,22 +459,26 @@ function transformTextTrack({
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
},
|
||||
textAlign: (getStringValue({
|
||||
value: textElement.textAlign,
|
||||
textAlign: parseEnum({
|
||||
value: element.textAlign,
|
||||
allowed: ["left", "center", "right"] as const,
|
||||
fallback: "left",
|
||||
}) || "left") as "left" | "center" | "right",
|
||||
fontWeight: (getStringValue({
|
||||
value: textElement.fontWeight,
|
||||
}),
|
||||
fontWeight: parseEnum({
|
||||
value: element.fontWeight,
|
||||
allowed: ["normal", "bold"] as const,
|
||||
fallback: "normal",
|
||||
}) || "normal") as "normal" | "bold",
|
||||
fontStyle: (getStringValue({
|
||||
value: textElement.fontStyle,
|
||||
}),
|
||||
fontStyle: parseEnum({
|
||||
value: element.fontStyle,
|
||||
allowed: ["normal", "italic"] as const,
|
||||
fallback: "normal",
|
||||
}) || "normal") as "normal" | "italic",
|
||||
textDecoration: (getStringValue({
|
||||
value: textElement.textDecoration,
|
||||
}),
|
||||
textDecoration: parseEnum({
|
||||
value: element.textDecoration,
|
||||
allowed: ["none", "underline", "line-through"] as const,
|
||||
fallback: "none",
|
||||
}) || "none") as "none" | "underline" | "line-through",
|
||||
}),
|
||||
hidden: false,
|
||||
transform,
|
||||
opacity,
|
||||
@@ -538,8 +512,7 @@ function transformAudioTrack({
|
||||
return null;
|
||||
}
|
||||
|
||||
const audioElement = element as LegacyAudioElement;
|
||||
const mediaId = getStringValue({ value: audioElement.mediaId });
|
||||
const mediaId = getStringValue({ value: element.mediaId });
|
||||
if (!mediaId) {
|
||||
return null;
|
||||
}
|
||||
@@ -762,6 +735,23 @@ function getStringValue({
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function parseEnum<T extends string>({
|
||||
value,
|
||||
allowed,
|
||||
fallback,
|
||||
}: {
|
||||
value: unknown;
|
||||
allowed: readonly T[];
|
||||
fallback: T;
|
||||
}): T {
|
||||
for (const candidate of allowed) {
|
||||
if (value === candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizeDateString({ value }: { value: unknown }): string {
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
|
||||
@@ -94,10 +94,7 @@ function migrateSplitMask({ mask }: { mask: unknown }): unknown {
|
||||
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 };
|
||||
const { position: _removed, ...restParams } = params;
|
||||
|
||||
return {
|
||||
...mask,
|
||||
|
||||
@@ -12,19 +12,18 @@ interface CustomMaskPathPoint {
|
||||
}
|
||||
|
||||
function isCustomMaskPathPoint(value: unknown): value is CustomMaskPathPoint {
|
||||
if (!value || typeof value !== "object") {
|
||||
if (!isRecord(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidate = value as Record<string, unknown>;
|
||||
return (
|
||||
typeof candidate.id === "string" &&
|
||||
typeof candidate.x === "number" &&
|
||||
typeof candidate.y === "number" &&
|
||||
typeof candidate.inX === "number" &&
|
||||
typeof candidate.inY === "number" &&
|
||||
typeof candidate.outX === "number" &&
|
||||
typeof candidate.outY === "number"
|
||||
typeof value.id === "string" &&
|
||||
typeof value.x === "number" &&
|
||||
typeof value.y === "number" &&
|
||||
typeof value.inX === "number" &&
|
||||
typeof value.inY === "number" &&
|
||||
typeof value.outX === "number" &&
|
||||
typeof value.outY === "number"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { MigrationResult, ProjectRecord } from "./types";
|
||||
import { getProjectId, isRecord } from "./utils";
|
||||
|
||||
const LEGACY_FONT_WEIGHT_MAP = {
|
||||
normal: "400",
|
||||
bold: "700",
|
||||
} as const;
|
||||
const LEGACY_FONT_WEIGHT_MAP = new Map<string, string>([
|
||||
["normal", "400"],
|
||||
["bold", "700"],
|
||||
]);
|
||||
|
||||
const VALID_NUMERIC_FONT_WEIGHTS = new Set([
|
||||
"100",
|
||||
@@ -168,10 +168,9 @@ function normalizeFontWeight({ value }: { value: unknown }): unknown {
|
||||
}
|
||||
|
||||
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
|
||||
];
|
||||
const mapped = LEGACY_FONT_WEIGHT_MAP.get(normalized);
|
||||
if (mapped !== undefined) {
|
||||
return mapped;
|
||||
}
|
||||
|
||||
if (VALID_NUMERIC_FONT_WEIGHTS.has(normalized)) {
|
||||
|
||||
@@ -27,7 +27,13 @@ export class OPFSAdapter implements StorageAdapter<File> {
|
||||
}
|
||||
}
|
||||
|
||||
async set(key: string, file: File): Promise<void> {
|
||||
async set({
|
||||
key,
|
||||
value: file,
|
||||
}: {
|
||||
key: string;
|
||||
value: File;
|
||||
}): Promise<void> {
|
||||
const directory = await this.getDirectory();
|
||||
const fileHandle = await directory.getFileHandle(key, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
|
||||
@@ -65,17 +65,17 @@ class StorageService {
|
||||
version: 1,
|
||||
};
|
||||
|
||||
this.projectsAdapter = new IndexedDBAdapter<SerializedProject>(
|
||||
this.config.projectsDb,
|
||||
"projects",
|
||||
this.config.version,
|
||||
);
|
||||
this.projectsAdapter = new IndexedDBAdapter<SerializedProject>({
|
||||
dbName: this.config.projectsDb,
|
||||
storeName: "projects",
|
||||
version: this.config.version,
|
||||
});
|
||||
|
||||
this.savedSoundsAdapter = new IndexedDBAdapter<SavedSoundsData>(
|
||||
this.config.savedSoundsDb,
|
||||
"saved-sounds",
|
||||
this.config.version,
|
||||
);
|
||||
this.savedSoundsAdapter = new IndexedDBAdapter<SavedSoundsData>({
|
||||
dbName: this.config.savedSoundsDb,
|
||||
storeName: "saved-sounds",
|
||||
version: this.config.version,
|
||||
});
|
||||
}
|
||||
|
||||
private async ensureMigrations(): Promise<void> {
|
||||
@@ -91,11 +91,11 @@ class StorageService {
|
||||
}
|
||||
|
||||
private getProjectMediaAdapters({ projectId }: { projectId: string }) {
|
||||
const mediaMetadataAdapter = new IndexedDBAdapter<MediaAssetData>(
|
||||
`${this.config.mediaDb}-${projectId}`,
|
||||
"media-metadata",
|
||||
this.config.version,
|
||||
);
|
||||
const mediaMetadataAdapter = new IndexedDBAdapter<MediaAssetData>({
|
||||
dbName: `${this.config.mediaDb}-${projectId}`,
|
||||
storeName: "media-metadata",
|
||||
version: this.config.version,
|
||||
});
|
||||
|
||||
const mediaAssetsAdapter = new OPFSAdapter(`media-files-${projectId}`);
|
||||
|
||||
@@ -161,7 +161,10 @@ class StorageService {
|
||||
timelineViewState: project.timelineViewState,
|
||||
};
|
||||
|
||||
await this.projectsAdapter.set(project.metadata.id, serializedProject);
|
||||
await this.projectsAdapter.set({
|
||||
key: project.metadata.id,
|
||||
value: serializedProject,
|
||||
});
|
||||
}
|
||||
|
||||
async loadProject({
|
||||
@@ -305,8 +308,14 @@ class StorageService {
|
||||
};
|
||||
|
||||
try {
|
||||
await mediaAssetsAdapter.set(mediaAsset.id, mediaAsset.file);
|
||||
await mediaMetadataAdapter.set(mediaAsset.id, metadata);
|
||||
await mediaAssetsAdapter.set({
|
||||
key: mediaAsset.id,
|
||||
value: mediaAsset.file,
|
||||
});
|
||||
await mediaMetadataAdapter.set({
|
||||
key: mediaAsset.id,
|
||||
value: metadata,
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
await mediaAssetsAdapter.remove(mediaAsset.id);
|
||||
@@ -501,7 +510,10 @@ class StorageService {
|
||||
lastModified: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await this.savedSoundsAdapter.set("user-sounds", updatedData);
|
||||
await this.savedSoundsAdapter.set({
|
||||
key: "user-sounds",
|
||||
value: updatedData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to save sound effect:", error);
|
||||
throw error;
|
||||
@@ -517,7 +529,10 @@ class StorageService {
|
||||
lastModified: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await this.savedSoundsAdapter.set("user-sounds", updatedData);
|
||||
await this.savedSoundsAdapter.set({
|
||||
key: "user-sounds",
|
||||
value: updatedData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to remove saved sound:", error);
|
||||
throw error;
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { TScene } from "@/timeline";
|
||||
|
||||
export interface StorageAdapter<T> {
|
||||
get(key: string): Promise<T | null>;
|
||||
set(key: string, value: T): Promise<void>;
|
||||
set(args: { key: string; value: T }): Promise<void>;
|
||||
remove(key: string): Promise<void>;
|
||||
list(): Promise<string[]>;
|
||||
clear(): Promise<void>;
|
||||
|
||||
Reference in New Issue
Block a user