mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
make storage versioning per-project + few other refactors
This commit is contained in:
@@ -691,8 +691,7 @@ parser.ts
|
|||||||
audio.ts
|
audio.ts
|
||||||
export type CollectedAudioElement = Omit<␍
|
export type CollectedAudioElement = Omit<␍
|
||||||
AudioElement,␍
|
AudioElement,␍
|
||||||
"type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl"
|
"type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl...
|
||||||
...
|
|
||||||
export function createAudioContext(): AudioContext
|
export function createAudioContext(): AudioContext
|
||||||
export interface DecodedAudio {
|
export interface DecodedAudio {
|
||||||
samples: Float32Array
|
samples: Float32Array
|
||||||
@@ -1220,6 +1219,11 @@ indexeddb-adapter.ts
|
|||||||
async getAll(): Promise<T[]>
|
async getAll(): Promise<T[]>
|
||||||
async clear(): Promise<void>
|
async clear(): Promise<void>
|
||||||
}
|
}
|
||||||
|
export function deleteDatabase({␍
|
||||||
|
dbName,␍
|
||||||
|
}: {␍
|
||||||
|
dbName: string;␍
|
||||||
|
}): Promise<void>
|
||||||
|
|
||||||
opfs-adapter.ts
|
opfs-adapter.ts
|
||||||
export class OPFSAdapter implements StorageAdapter<File> {
|
export class OPFSAdapter implements StorageAdapter<File> {
|
||||||
@@ -1256,7 +1260,6 @@ types.ts
|
|||||||
fps?: number
|
fps?: number
|
||||||
ephemeral?: boolean
|
ephemeral?: boolean
|
||||||
thumbnailUrl?: string
|
thumbnailUrl?: string
|
||||||
sourceStickerIconName?: string
|
|
||||||
}
|
}
|
||||||
export type SerializedScene = Omit<TScene, "createdAt" | "updatedAt"> & {␍
|
export type SerializedScene = Omit<TScene, "createdAt" | "updatedAt"> & {␍
|
||||||
createdAt: string;␍
|
createdAt: string;␍
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { buildDefaultScene, getProjectDurationFromScenes } from "@/lib/scenes";
|
|||||||
import { buildScene } from "@/services/renderer/scene-builder";
|
import { buildScene } from "@/services/renderer/scene-builder";
|
||||||
import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
|
import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
|
||||||
import {
|
import {
|
||||||
CURRENT_STORAGE_VERSION,
|
CURRENT_PROJECT_VERSION,
|
||||||
migrations,
|
migrations,
|
||||||
runStorageMigrations,
|
runStorageMigrations,
|
||||||
} from "@/services/storage/migrations";
|
} from "@/services/storage/migrations";
|
||||||
@@ -58,31 +58,7 @@ export class ProjectManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.storageMigrationPromise = (async () => {
|
this.storageMigrationPromise = (async () => {
|
||||||
let hasShownState = false;
|
await runStorageMigrations({ migrations });
|
||||||
|
|
||||||
await runStorageMigrations({
|
|
||||||
migrations,
|
|
||||||
callbacks: {
|
|
||||||
onMigrationStart: ({ fromVersion, toVersion }) => {
|
|
||||||
hasShownState = true;
|
|
||||||
this.setMigrationState({
|
|
||||||
isMigrating: true,
|
|
||||||
fromVersion,
|
|
||||||
toVersion,
|
|
||||||
projectName: null,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasShownState) {
|
|
||||||
this.setMigrationState({
|
|
||||||
isMigrating: false,
|
|
||||||
fromVersion: null,
|
|
||||||
toVersion: null,
|
|
||||||
projectName: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
await this.storageMigrationPromise;
|
await this.storageMigrationPromise;
|
||||||
@@ -109,7 +85,7 @@ export class ProjectManager {
|
|||||||
color: DEFAULT_COLOR,
|
color: DEFAULT_COLOR,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
version: CURRENT_STORAGE_VERSION,
|
version: CURRENT_PROJECT_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.active = newProject;
|
this.active = newProject;
|
||||||
@@ -572,11 +548,6 @@ export class ProjectManager {
|
|||||||
return this.migrationState;
|
return this.migrationState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setMigrationState(state: Partial<MigrationState>): void {
|
|
||||||
this.migrationState = { ...this.migrationState, ...state };
|
|
||||||
this.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
setActiveProject({ project }: { project: TProject }): void {
|
setActiveProject({ project }: { project: TProject }): void {
|
||||||
this.active = project;
|
this.active = project;
|
||||||
this.notify();
|
this.notify();
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import type { CanvasRenderer } from "../canvas-renderer";
|
import type { CanvasRenderer } from "../canvas-renderer";
|
||||||
import { BaseNode } from "./base-node";
|
import { BaseNode } from "./base-node";
|
||||||
import type { BaseMediaNodeParams } from "./video-node";
|
|
||||||
|
|
||||||
const IMAGE_EPSILON = 1 / 1000;
|
const IMAGE_EPSILON = 1 / 1000;
|
||||||
|
|
||||||
export type ImageNodeParams = BaseMediaNodeParams;
|
export interface ImageNodeParams {
|
||||||
|
url: string;
|
||||||
|
duration: number;
|
||||||
|
timeOffset: number;
|
||||||
|
trimStart: number;
|
||||||
|
trimEnd: number;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
opacity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class ImageNode extends BaseNode<ImageNodeParams> {
|
export class ImageNode extends BaseNode<ImageNodeParams> {
|
||||||
private image?: HTMLImageElement;
|
private image?: HTMLImageElement;
|
||||||
@@ -18,15 +28,12 @@ export class ImageNode extends BaseNode<ImageNodeParams> {
|
|||||||
private async load() {
|
private async load() {
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
this.image = image;
|
this.image = image;
|
||||||
const url = URL.createObjectURL(this.params.file);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
image.onload = () => resolve();
|
image.onload = () => resolve();
|
||||||
image.onerror = () => reject(new Error("Image load failed"));
|
image.onerror = () => reject(new Error("Image load failed"));
|
||||||
image.src = url;
|
image.src = this.params.url;
|
||||||
});
|
});
|
||||||
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getImageTime(time: number) {
|
private getImageTime(time: number) {
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { videoCache } from "@/services/video-cache/service";
|
|||||||
|
|
||||||
const VIDEO_EPSILON = 1 / 1000;
|
const VIDEO_EPSILON = 1 / 1000;
|
||||||
|
|
||||||
export interface BaseMediaNodeParams {
|
export interface VideoNodeParams {
|
||||||
|
url: string;
|
||||||
file: File;
|
file: File;
|
||||||
|
mediaId: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
timeOffset: number;
|
timeOffset: number;
|
||||||
trimStart: number;
|
trimStart: number;
|
||||||
@@ -17,10 +19,6 @@ export interface BaseMediaNodeParams {
|
|||||||
opacity?: number;
|
opacity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoNodeParams extends BaseMediaNodeParams {
|
|
||||||
mediaId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class VideoNode extends BaseNode<VideoNodeParams> {
|
export class VideoNode extends BaseNode<VideoNodeParams> {
|
||||||
private getVideoTime(time: number) {
|
private getVideoTime(time: number) {
|
||||||
return time - this.params.timeOffset + this.params.trimStart;
|
return time - this.params.timeOffset + this.params.trimStart;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function buildScene(params: BuildSceneParams) {
|
|||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
if (element.type === "video" || element.type === "image") {
|
if (element.type === "video" || element.type === "image") {
|
||||||
const mediaAsset = mediaMap.get(element.mediaId);
|
const mediaAsset = mediaMap.get(element.mediaId);
|
||||||
if (!mediaAsset?.file) {
|
if (!mediaAsset?.file || !mediaAsset?.url) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ export function buildScene(params: BuildSceneParams) {
|
|||||||
contentNodes.push(
|
contentNodes.push(
|
||||||
new VideoNode({
|
new VideoNode({
|
||||||
mediaId: mediaAsset.id,
|
mediaId: mediaAsset.id,
|
||||||
|
url: mediaAsset.url,
|
||||||
file: mediaAsset.file,
|
file: mediaAsset.file,
|
||||||
duration: element.duration,
|
duration: element.duration,
|
||||||
timeOffset: element.startTime,
|
timeOffset: element.startTime,
|
||||||
@@ -70,7 +71,7 @@ export function buildScene(params: BuildSceneParams) {
|
|||||||
if (mediaAsset.type === "image") {
|
if (mediaAsset.type === "image") {
|
||||||
contentNodes.push(
|
contentNodes.push(
|
||||||
new ImageNode({
|
new ImageNode({
|
||||||
file: mediaAsset.file,
|
url: mediaAsset.url,
|
||||||
duration: element.duration,
|
duration: element.duration,
|
||||||
timeOffset: element.startTime,
|
timeOffset: element.startTime,
|
||||||
trimStart: element.trimStart,
|
trimStart: element.trimStart,
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import type { MigrationResult, ProjectRecord } from "./transformers/types";
|
||||||
|
|
||||||
export abstract class StorageMigration {
|
export abstract class StorageMigration {
|
||||||
abstract from: number;
|
abstract from: number;
|
||||||
abstract to: number;
|
abstract to: number;
|
||||||
abstract run(): Promise<void>;
|
abstract transform(
|
||||||
|
project: ProjectRecord,
|
||||||
|
): Promise<MigrationResult<ProjectRecord>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
export { StorageMigration } from "./base";
|
export { StorageMigration } from "./base";
|
||||||
export { StorageVersionManager } from "./version-manager";
|
|
||||||
import { V0toV1Migration } from "./v0-to-v1";
|
import { V0toV1Migration } from "./v0-to-v1";
|
||||||
import { V1toV2Migration } from "./v1-to-v2";
|
import { V1toV2Migration } from "./v1-to-v2";
|
||||||
import { V2toV3Migration } from "./v2-to-v3";
|
import { V2toV3Migration } from "./v2-to-v3";
|
||||||
export { runStorageMigrations } from "./runner";
|
export { runStorageMigrations } from "./runner";
|
||||||
|
|
||||||
export const CURRENT_STORAGE_VERSION = 3;
|
export const CURRENT_PROJECT_VERSION = 3;
|
||||||
|
|
||||||
export const migrations = [
|
export const migrations = [
|
||||||
new V0toV1Migration(),
|
new V0toV1Migration(),
|
||||||
|
|||||||
@@ -1,138 +1,94 @@
|
|||||||
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
|
import {
|
||||||
|
IndexedDBAdapter,
|
||||||
|
deleteDatabase,
|
||||||
|
} from "@/services/storage/indexeddb-adapter";
|
||||||
import type { StorageMigration } from "./base";
|
import type { StorageMigration } from "./base";
|
||||||
import { StorageVersionManager } from "./version-manager";
|
import type { ProjectRecord } from "./transformers/types";
|
||||||
|
import { getProjectId } from "./transformers/utils";
|
||||||
|
|
||||||
export interface StorageMigrationResult {
|
export interface StorageMigrationResult {
|
||||||
fromVersion: number;
|
migratedCount: number;
|
||||||
toVersion: number;
|
|
||||||
migrated: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StorageMigrationCallbacks = {
|
let hasCleanedUpMetaDb = false;
|
||||||
onMigrationStart?: ({
|
|
||||||
fromVersion,
|
|
||||||
toVersion,
|
|
||||||
}: {
|
|
||||||
fromVersion: number;
|
|
||||||
toVersion: number;
|
|
||||||
}) => void;
|
|
||||||
onMigrationComplete?: ({
|
|
||||||
fromVersion,
|
|
||||||
toVersion,
|
|
||||||
}: {
|
|
||||||
fromVersion: number;
|
|
||||||
toVersion: number;
|
|
||||||
}) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProjectRecord = Record<string, unknown>;
|
|
||||||
|
|
||||||
export async function runStorageMigrations({
|
export async function runStorageMigrations({
|
||||||
migrations,
|
migrations,
|
||||||
versionManager = new StorageVersionManager(),
|
|
||||||
callbacks,
|
|
||||||
}: {
|
}: {
|
||||||
migrations: StorageMigration[];
|
migrations: StorageMigration[];
|
||||||
versionManager?: StorageVersionManager;
|
|
||||||
callbacks?: StorageMigrationCallbacks;
|
|
||||||
}): Promise<StorageMigrationResult> {
|
}): Promise<StorageMigrationResult> {
|
||||||
const versionRecord = await versionManager.getVersionRecord();
|
// One-time cleanup: delete the old global version database
|
||||||
const inferredVersion = versionRecord
|
if (!hasCleanedUpMetaDb) {
|
||||||
? null
|
try {
|
||||||
: await inferStorageVersionFromProjects();
|
await deleteDatabase({ dbName: "video-editor-meta" });
|
||||||
const fromVersion =
|
} catch {
|
||||||
versionRecord?.inProgress?.from ??
|
// Ignore errors - DB might not exist
|
||||||
versionRecord?.version ??
|
}
|
||||||
inferredVersion ??
|
hasCleanedUpMetaDb = true;
|
||||||
0;
|
|
||||||
|
|
||||||
if (!versionRecord) {
|
|
||||||
await versionManager.setVersion({ version: fromVersion });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderedMigrations = [...migrations].sort((a, b) => a.from - b.from);
|
const projectsAdapter = new IndexedDBAdapter<ProjectRecord>(
|
||||||
let currentVersion = fromVersion;
|
|
||||||
|
|
||||||
for (const migration of orderedMigrations) {
|
|
||||||
if (migration.from !== currentVersion) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await versionManager.setInProgress({
|
|
||||||
from: migration.from,
|
|
||||||
to: migration.to,
|
|
||||||
});
|
|
||||||
callbacks?.onMigrationStart?.({
|
|
||||||
fromVersion: migration.from,
|
|
||||||
toVersion: migration.to,
|
|
||||||
});
|
|
||||||
await migration.run();
|
|
||||||
currentVersion = migration.to;
|
|
||||||
await versionManager.setVersion({ version: currentVersion });
|
|
||||||
await versionManager.clearInProgress();
|
|
||||||
callbacks?.onMigrationComplete?.({
|
|
||||||
fromVersion: migration.from,
|
|
||||||
toVersion: migration.to,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
fromVersion,
|
|
||||||
toVersion: currentVersion,
|
|
||||||
migrated: currentVersion !== fromVersion,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function inferStorageVersionFromProjects(): Promise<number> {
|
|
||||||
const projectsAdapter = new IndexedDBAdapter<unknown>(
|
|
||||||
"video-editor-projects",
|
"video-editor-projects",
|
||||||
"projects",
|
"projects",
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
const projects = await projectsAdapter.getAll();
|
const projects = await projectsAdapter.getAll();
|
||||||
|
|
||||||
if (projects.length === 0) {
|
const orderedMigrations = [...migrations].sort((a, b) => a.from - b.from);
|
||||||
return 0;
|
let migratedCount = 0;
|
||||||
}
|
|
||||||
|
|
||||||
let lowestVersion = Number.POSITIVE_INFINITY;
|
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
const projectVersion = checkProjectVersion({ project });
|
if (typeof project !== "object" || project === null) {
|
||||||
if (projectVersion < lowestVersion) {
|
continue;
|
||||||
lowestVersion = projectVersion;
|
}
|
||||||
|
|
||||||
|
let projectRecord = project as ProjectRecord;
|
||||||
|
let currentVersion = getProjectVersion({ project: projectRecord });
|
||||||
|
|
||||||
|
// Apply migrations sequentially until project is up to date
|
||||||
|
for (const migration of orderedMigrations) {
|
||||||
|
if (migration.from !== currentVersion) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await migration.transform(projectRecord);
|
||||||
|
|
||||||
|
if (result.skipped) {
|
||||||
|
break; // Project is already at this version or higher
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update project with migrated version
|
||||||
|
const projectId = getProjectId({ project: result.project });
|
||||||
|
if (!projectId) {
|
||||||
|
break; // Can't save without ID
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectsAdapter.set(projectId, result.project);
|
||||||
|
migratedCount++;
|
||||||
|
currentVersion = migration.to;
|
||||||
|
|
||||||
|
// Use migrated project for next iteration
|
||||||
|
projectRecord = result.project;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lowestVersion === Number.POSITIVE_INFINITY) {
|
return { migratedCount };
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lowestVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkProjectVersion({ project }: { project: unknown }): number {
|
|
||||||
if (!isRecord(project)) {
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProjectVersion({ project }: { project: ProjectRecord }): number {
|
||||||
const versionValue = project.version;
|
const versionValue = project.version;
|
||||||
|
|
||||||
// v2 and up
|
// v2 and up - has explicit version field
|
||||||
if (typeof versionValue === "number") {
|
if (typeof versionValue === "number") {
|
||||||
return versionValue;
|
return versionValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// v1 (got scenes)
|
// v1 - has scenes array
|
||||||
const scenesValue = project.scenes;
|
const scenesValue = project.scenes;
|
||||||
if (Array.isArray(scenesValue) && scenesValue.length > 0) {
|
if (Array.isArray(scenesValue) && scenesValue.length > 0) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0 (didn't have scenes)
|
// v0 - no scenes
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is ProjectRecord {
|
|
||||||
return typeof value === "object" && value !== null;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { ProjectRecord } from "./types";
|
||||||
|
|
||||||
|
export function isRecord(value: unknown): value is ProjectRecord {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectId({
|
||||||
|
project,
|
||||||
|
}: {
|
||||||
|
project: ProjectRecord;
|
||||||
|
}): string | null {
|
||||||
|
const idValue = project.id;
|
||||||
|
if (typeof idValue === "string" && idValue.length > 0) {
|
||||||
|
return idValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataValue = project.metadata;
|
||||||
|
if (!isRecord(metadataValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataId = metadataValue.id;
|
||||||
|
if (typeof metadataId === "string" && metadataId.length > 0) {
|
||||||
|
return metadataId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { generateUUID } from "@/utils/id";
|
import { generateUUID } from "@/utils/id";
|
||||||
import type { SerializedScene } from "@/services/storage/types";
|
import type { SerializedScene } from "@/services/storage/types";
|
||||||
import type { MigrationResult, ProjectRecord } from "./types";
|
import type { MigrationResult, ProjectRecord } from "./types";
|
||||||
|
import { getProjectId, isRecord } from "./utils";
|
||||||
|
|
||||||
export interface TransformV0ToV1Options {
|
export interface TransformV0ToV1Options {
|
||||||
now?: Date;
|
now?: Date;
|
||||||
@@ -54,29 +55,4 @@ export function transformProjectV0ToV1({
|
|||||||
return { project: updatedProject, skipped: false };
|
return { project: updatedProject, skipped: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProjectId({
|
export { getProjectId } from "./utils";
|
||||||
project,
|
|
||||||
}: {
|
|
||||||
project: ProjectRecord;
|
|
||||||
}): string | null {
|
|
||||||
const idValue = project.id;
|
|
||||||
if (typeof idValue === "string" && idValue.length > 0) {
|
|
||||||
return idValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataValue = project.metadata;
|
|
||||||
if (!isRecord(metadataValue)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataId = metadataValue.id;
|
|
||||||
if (typeof metadataId === "string" && metadataId.length > 0) {
|
|
||||||
return metadataId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRecord(value: unknown): value is ProjectRecord {
|
|
||||||
return typeof value === "object" && value !== null;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type {
|
|||||||
VideoElement,
|
VideoElement,
|
||||||
} from "@/types/timeline";
|
} from "@/types/timeline";
|
||||||
import type { MigrationResult, ProjectRecord } from "./types";
|
import type { MigrationResult, ProjectRecord } from "./types";
|
||||||
|
import { getProjectId, isRecord } from "./utils";
|
||||||
|
|
||||||
interface LegacyTimelineData {
|
interface LegacyTimelineData {
|
||||||
tracks: unknown[];
|
tracks: unknown[];
|
||||||
@@ -51,10 +52,8 @@ interface LegacyMediaTrack {
|
|||||||
|
|
||||||
export interface TransformV1ToV2Options {
|
export interface TransformV1ToV2Options {
|
||||||
loadMediaAsset?: ({
|
loadMediaAsset?: ({
|
||||||
projectId,
|
|
||||||
mediaId,
|
mediaId,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
|
||||||
mediaId: string;
|
mediaId: string;
|
||||||
}) => Promise<MediaAssetData | null>;
|
}) => Promise<MediaAssetData | null>;
|
||||||
}
|
}
|
||||||
@@ -91,10 +90,8 @@ async function migrateProject({
|
|||||||
project: ProjectRecord;
|
project: ProjectRecord;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
loadMediaAsset?: ({
|
loadMediaAsset?: ({
|
||||||
projectId,
|
|
||||||
mediaId,
|
mediaId,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
|
||||||
mediaId: string;
|
mediaId: string;
|
||||||
}) => Promise<MediaAssetData | null>;
|
}) => Promise<MediaAssetData | null>;
|
||||||
}): Promise<ProjectRecord> {
|
}): Promise<ProjectRecord> {
|
||||||
@@ -261,10 +258,8 @@ async function transformTracks({
|
|||||||
tracks: unknown[];
|
tracks: unknown[];
|
||||||
projectId: string;
|
projectId: string;
|
||||||
loadMediaAsset?: ({
|
loadMediaAsset?: ({
|
||||||
projectId,
|
|
||||||
mediaId,
|
mediaId,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
|
||||||
mediaId: string;
|
mediaId: string;
|
||||||
}) => Promise<MediaAssetData | null>;
|
}) => Promise<MediaAssetData | null>;
|
||||||
}): Promise<TimelineTrack[]> {
|
}): Promise<TimelineTrack[]> {
|
||||||
@@ -318,10 +313,8 @@ async function transformMediaTrack({
|
|||||||
track: LegacyMediaTrack;
|
track: LegacyMediaTrack;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
loadMediaAsset?: ({
|
loadMediaAsset?: ({
|
||||||
projectId,
|
|
||||||
mediaId,
|
mediaId,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
|
||||||
mediaId: string;
|
mediaId: string;
|
||||||
}) => Promise<MediaAssetData | null>;
|
}) => Promise<MediaAssetData | null>;
|
||||||
isMain: boolean;
|
isMain: boolean;
|
||||||
@@ -342,7 +335,7 @@ async function transformMediaTrack({
|
|||||||
|
|
||||||
let mediaType: "video" | "image" = "video";
|
let mediaType: "video" | "image" = "video";
|
||||||
if (loadMediaAsset) {
|
if (loadMediaAsset) {
|
||||||
const mediaAsset = await loadMediaAsset({ projectId, mediaId });
|
const mediaAsset = await loadMediaAsset({ mediaId });
|
||||||
if (mediaAsset) {
|
if (mediaAsset) {
|
||||||
mediaType = mediaAsset.type === "image" ? "image" : "video";
|
mediaType = mediaAsset.type === "image" ? "image" : "video";
|
||||||
}
|
}
|
||||||
@@ -543,28 +536,7 @@ function transformAudioTrack({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProjectId({
|
export { getProjectId } from "./utils";
|
||||||
project,
|
|
||||||
}: {
|
|
||||||
project: ProjectRecord;
|
|
||||||
}): string | null {
|
|
||||||
const idValue = project.id;
|
|
||||||
if (typeof idValue === "string" && idValue.length > 0) {
|
|
||||||
return idValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataValue = project.metadata;
|
|
||||||
if (!isRecord(metadataValue)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataId = metadataValue.id;
|
|
||||||
if (typeof metadataId === "string" && metadataId.length > 0) {
|
|
||||||
return metadataId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentSceneId({
|
function getCurrentSceneId({
|
||||||
value,
|
value,
|
||||||
@@ -772,7 +744,3 @@ function isV2Project({ project }: { project: ProjectRecord }): boolean {
|
|||||||
|
|
||||||
return isRecord(project.metadata) && isRecord(project.settings);
|
return isRecord(project.metadata) && isRecord(project.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is ProjectRecord {
|
|
||||||
return typeof value === "object" && value !== null;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getProjectDurationFromScenes } from "@/lib/scenes";
|
import { getProjectDurationFromScenes } from "@/lib/scenes";
|
||||||
import type { TScene } from "@/types/timeline";
|
import type { TScene } from "@/types/timeline";
|
||||||
import type { MigrationResult, ProjectRecord } from "./types";
|
import type { MigrationResult, ProjectRecord } from "./types";
|
||||||
|
import { getProjectId, isRecord } from "./utils";
|
||||||
|
|
||||||
export function transformProjectV2ToV3({
|
export function transformProjectV2ToV3({
|
||||||
project,
|
project,
|
||||||
@@ -33,28 +34,7 @@ export function transformProjectV2ToV3({
|
|||||||
return { project: migratedProject, skipped: false };
|
return { project: migratedProject, skipped: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProjectId({
|
export { getProjectId } from "./utils";
|
||||||
project,
|
|
||||||
}: {
|
|
||||||
project: ProjectRecord;
|
|
||||||
}): string | null {
|
|
||||||
const idValue = project.id;
|
|
||||||
if (typeof idValue === "string" && idValue.length > 0) {
|
|
||||||
return idValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataValue = project.metadata;
|
|
||||||
if (!isRecord(metadataValue)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataId = metadataValue.id;
|
|
||||||
if (typeof metadataId === "string" && metadataId.length > 0) {
|
|
||||||
return metadataId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getScenes({ project }: { project: ProjectRecord }): TScene[] {
|
function getScenes({ project }: { project: ProjectRecord }): TScene[] {
|
||||||
const scenesValue = project.scenes;
|
const scenesValue = project.scenes;
|
||||||
@@ -75,7 +55,3 @@ function isV3Project({ project }: { project: ProjectRecord }): boolean {
|
|||||||
isRecord(project.metadata) && typeof project.metadata.duration === "number"
|
isRecord(project.metadata) && typeof project.metadata.duration === "number"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is ProjectRecord {
|
|
||||||
return typeof value === "object" && value !== null;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,41 +4,19 @@
|
|||||||
* Sets currentSceneId to the new scene's id
|
* Sets currentSceneId to the new scene's id
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
|
|
||||||
import { StorageMigration } from "./base";
|
import { StorageMigration } from "./base";
|
||||||
import { getProjectId, transformProjectV0ToV1 } from "./transformers/v0-to-v1";
|
import type { ProjectRecord } from "./transformers/types";
|
||||||
|
import { transformProjectV0ToV1 } from "./transformers/v0-to-v1";
|
||||||
|
|
||||||
export class V0toV1Migration extends StorageMigration {
|
export class V0toV1Migration extends StorageMigration {
|
||||||
from = 0;
|
from = 0;
|
||||||
to = 1;
|
to = 1;
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async transform(project: ProjectRecord): Promise<{
|
||||||
const projectsAdapter = new IndexedDBAdapter<unknown>(
|
project: ProjectRecord;
|
||||||
"video-editor-projects",
|
skipped: boolean;
|
||||||
"projects",
|
reason?: string;
|
||||||
1,
|
}> {
|
||||||
);
|
return transformProjectV0ToV1({ project });
|
||||||
const projects = await projectsAdapter.getAll();
|
|
||||||
|
|
||||||
for (const project of projects) {
|
|
||||||
if (typeof project !== "object" || project === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = transformProjectV0ToV1({
|
|
||||||
project: project as Record<string, unknown>,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.skipped) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectId = getProjectId({ project: result.project });
|
|
||||||
if (!projectId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await projectsAdapter.set(projectId, result.project);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
} from "@/services/storage/indexeddb-adapter";
|
} from "@/services/storage/indexeddb-adapter";
|
||||||
import type { MediaAssetData } from "@/services/storage/types";
|
import type { MediaAssetData } from "@/services/storage/types";
|
||||||
import { StorageMigration } from "./base";
|
import { StorageMigration } from "./base";
|
||||||
|
import type { ProjectRecord } from "./transformers/types";
|
||||||
import {
|
import {
|
||||||
getProjectId,
|
getProjectId,
|
||||||
transformProjectV1ToV2,
|
transformProjectV1ToV2,
|
||||||
@@ -14,41 +15,32 @@ export class V1toV2Migration extends StorageMigration {
|
|||||||
from = 1;
|
from = 1;
|
||||||
to = 2;
|
to = 2;
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async transform(project: ProjectRecord): Promise<{
|
||||||
const projectsAdapter = new IndexedDBAdapter<unknown>(
|
project: ProjectRecord;
|
||||||
"video-editor-projects",
|
skipped: boolean;
|
||||||
"projects",
|
reason?: string;
|
||||||
1,
|
}> {
|
||||||
);
|
const projectId = getProjectId({ project });
|
||||||
const projects = await projectsAdapter.getAll();
|
|
||||||
|
|
||||||
for (const project of projects) {
|
|
||||||
if (typeof project !== "object" || project === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectId = getProjectId({
|
|
||||||
project: project as Record<string, unknown>,
|
|
||||||
});
|
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
continue;
|
return { project, skipped: true, reason: "no project id" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadMediaAsset = createMediaAssetLoader({ projectId });
|
const loadMediaAsset = createMediaAssetLoader({ projectId });
|
||||||
|
|
||||||
const result = await transformProjectV1ToV2({
|
const result = await transformProjectV1ToV2({
|
||||||
project: project as Record<string, unknown>,
|
project,
|
||||||
options: { loadMediaAsset },
|
options: { loadMediaAsset },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.skipped) {
|
if (!result.skipped) {
|
||||||
continue;
|
// cleanup legacy timeline DBs after successful transformation
|
||||||
|
await cleanupLegacyTimelineDBs({
|
||||||
|
projectId,
|
||||||
|
project: result.project,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await projectsAdapter.set(projectId, result.project);
|
return result;
|
||||||
|
|
||||||
await cleanupLegacyTimelineDBs({ projectId, project: result.project });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +65,7 @@ async function cleanupLegacyTimelineDBs({
|
|||||||
project,
|
project,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
project: Record<string, unknown>;
|
project: ProjectRecord;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const scenes = project.scenes;
|
const scenes = project.scenes;
|
||||||
if (!Array.isArray(scenes)) {
|
if (!Array.isArray(scenes)) {
|
||||||
|
|||||||
@@ -2,41 +2,19 @@
|
|||||||
* Adds a "duration" field to each project's metadata
|
* Adds a "duration" field to each project's metadata
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
|
|
||||||
import { StorageMigration } from "./base";
|
import { StorageMigration } from "./base";
|
||||||
import { getProjectId, transformProjectV2ToV3 } from "./transformers/v2-to-v3";
|
import type { ProjectRecord } from "./transformers/types";
|
||||||
|
import { transformProjectV2ToV3 } from "./transformers/v2-to-v3";
|
||||||
|
|
||||||
export class V2toV3Migration extends StorageMigration {
|
export class V2toV3Migration extends StorageMigration {
|
||||||
from = 2;
|
from = 2;
|
||||||
to = 3;
|
to = 3;
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async transform(project: ProjectRecord): Promise<{
|
||||||
const projectsAdapter = new IndexedDBAdapter<unknown>(
|
project: ProjectRecord;
|
||||||
"video-editor-projects",
|
skipped: boolean;
|
||||||
"projects",
|
reason?: string;
|
||||||
1,
|
}> {
|
||||||
);
|
return transformProjectV2ToV3({ project });
|
||||||
const projects = await projectsAdapter.getAll();
|
|
||||||
|
|
||||||
for (const project of projects) {
|
|
||||||
if (typeof project !== "object" || project === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = transformProjectV2ToV3({
|
|
||||||
project: project as Record<string, unknown>,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.skipped) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectId = getProjectId({ project: result.project });
|
|
||||||
if (!projectId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await projectsAdapter.set(projectId, result.project);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
|
|
||||||
|
|
||||||
type StorageVersionRecord = {
|
|
||||||
version: number;
|
|
||||||
inProgress?: {
|
|
||||||
from: number;
|
|
||||||
to: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_DB_NAME = "video-editor-meta";
|
|
||||||
const DEFAULT_STORE_NAME = "storage-version";
|
|
||||||
const DEFAULT_DB_VERSION = 1;
|
|
||||||
const STORAGE_VERSION_KEY = "storage-version";
|
|
||||||
|
|
||||||
export class StorageVersionManager {
|
|
||||||
private adapter: IndexedDBAdapter<StorageVersionRecord>;
|
|
||||||
|
|
||||||
constructor({
|
|
||||||
dbName = DEFAULT_DB_NAME,
|
|
||||||
storeName = DEFAULT_STORE_NAME,
|
|
||||||
version = DEFAULT_DB_VERSION,
|
|
||||||
}: {
|
|
||||||
dbName?: string;
|
|
||||||
storeName?: string;
|
|
||||||
version?: number;
|
|
||||||
} = {}) {
|
|
||||||
this.adapter = new IndexedDBAdapter<StorageVersionRecord>(
|
|
||||||
dbName,
|
|
||||||
storeName,
|
|
||||||
version,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getVersion(): Promise<number> {
|
|
||||||
const record = await this.adapter.get(STORAGE_VERSION_KEY);
|
|
||||||
return record?.version ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getVersionRecord(): Promise<StorageVersionRecord | null> {
|
|
||||||
return this.adapter.get(STORAGE_VERSION_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setVersion({ version }: { version: number }): Promise<void> {
|
|
||||||
const record = await this.getVersionRecord();
|
|
||||||
const inProgress = record?.inProgress;
|
|
||||||
await this.adapter.set(STORAGE_VERSION_KEY, {
|
|
||||||
version,
|
|
||||||
...(inProgress ? { inProgress } : {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async setInProgress({
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
}: {
|
|
||||||
from: number;
|
|
||||||
to: number;
|
|
||||||
}): Promise<void> {
|
|
||||||
const record = await this.getVersionRecord();
|
|
||||||
const version = record?.version ?? 0;
|
|
||||||
await this.adapter.set(STORAGE_VERSION_KEY, {
|
|
||||||
version,
|
|
||||||
inProgress: { from, to },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearInProgress(): Promise<void> {
|
|
||||||
const record = await this.getVersionRecord();
|
|
||||||
const version = record?.version ?? 0;
|
|
||||||
await this.adapter.set(STORAGE_VERSION_KEY, { version });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,7 +26,6 @@ export interface MediaAssetData {
|
|||||||
fps?: number;
|
fps?: number;
|
||||||
ephemeral?: boolean;
|
ephemeral?: boolean;
|
||||||
thumbnailUrl?: string;
|
thumbnailUrl?: string;
|
||||||
sourceStickerIconName?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedScene = Omit<TScene, "createdAt" | "updatedAt"> & {
|
export type SerializedScene = Omit<TScene, "createdAt" | "updatedAt"> & {
|
||||||
|
|||||||
Reference in New Issue
Block a user