diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx
index 793b5309..5d17a46c 100644
--- a/apps/web/src/app/projects/page.tsx
+++ b/apps/web/src/app/projects/page.tsx
@@ -862,7 +862,7 @@ function ProjectsSkeleton() {
);
return (
-
+
{skeletonIds.map((skeletonId) => (
!("hidden" in track && track.hidden),
);
- const mainTrack = visibleTracks.find((track) => isMainTrack(track)) ?? null;
const orderedTracksTopToBottom = [
...visibleTracks.filter((track) => !isMainTrack(track)),
- ...(mainTrack ? [mainTrack] : []),
+ ...visibleTracks.filter((track) => isMainTrack(track)),
];
const orderedTracksBottomToTop = orderedTracksTopToBottom.slice().reverse();
diff --git a/apps/web/src/services/storage/migrations/runner.ts b/apps/web/src/services/storage/migrations/runner.ts
index bd34c661..b261252b 100644
--- a/apps/web/src/services/storage/migrations/runner.ts
+++ b/apps/web/src/services/storage/migrations/runner.ts
@@ -19,6 +19,8 @@ export interface MigrationProgress {
let hasCleanedUpMetaDb = false;
+const MIN_MIGRATION_DISPLAY_MS = 1000;
+
export async function runStorageMigrations({
migrations,
onProgress,
@@ -45,6 +47,7 @@ export async function runStorageMigrations({
const orderedMigrations = [...migrations].sort((a, b) => a.from - b.from);
let migratedCount = 0;
+ let migrationStartTime: number | null = null;
for (const project of projects) {
if (typeof project !== "object" || project === null) {
@@ -59,6 +62,11 @@ export async function runStorageMigrations({
continue;
}
+ // Track when we first showed the migration dialog
+ if (migrationStartTime === null) {
+ migrationStartTime = Date.now();
+ }
+
const projectName = getProjectName({ project: projectRecord });
onProgress?.({
isMigrating: true,
@@ -90,6 +98,16 @@ export async function runStorageMigrations({
}
}
+ // Ensure dialog is visible for minimum time so users can see it
+ if (migrationStartTime !== null) {
+ const elapsed = Date.now() - migrationStartTime;
+ if (elapsed < MIN_MIGRATION_DISPLAY_MS) {
+ await new Promise((resolve) =>
+ setTimeout(resolve, MIN_MIGRATION_DISPLAY_MS - elapsed),
+ );
+ }
+ }
+
onProgress?.({
isMigrating: false,
fromVersion: null,
diff --git a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
index 6242f8c3..098101d6 100644
--- a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
@@ -26,7 +26,7 @@ interface LegacyMediaElement {
type: "media";
mediaId: string;
muted?: boolean;
- [id: string]: unknown;
+ [key: string]: unknown;
}
interface LegacyTextElement {
@@ -35,19 +35,19 @@ interface LegacyTextElement {
y: number;
rotation: number;
opacity: number;
- [id: string]: unknown;
+ [key: string]: unknown;
}
interface LegacyAudioElement {
type: "audio";
mediaId: string;
- [id: string]: unknown;
+ [key: string]: unknown;
}
interface LegacyMediaTrack {
type: "media";
elements: unknown[];
- [id: string]: unknown;
+ [key: string]: unknown;
}
export interface TransformV1ToV2Options {
@@ -265,35 +265,38 @@ async function transformTracks({
}
let isFirstVideoTrackFound = false;
+ const transformedTracks: (TimelineTrack | null)[] = [];
- const transformedTracks = await Promise.all(
- tracks.map(async (track) => {
- if (!isRecord(track)) {
- return null;
- }
+ for (const track of tracks) {
+ if (!isRecord(track)) {
+ transformedTracks.push(null);
+ continue;
+ }
- const trackType = track.type;
- if (trackType === "media") {
- const videoTrack = await transformMediaTrack({
- track: track as LegacyMediaTrack,
- loadMediaAsset,
- isMain: !isFirstVideoTrackFound,
- });
- isFirstVideoTrackFound = true;
- return videoTrack;
- }
+ const trackType = track.type;
+ if (trackType === "media") {
+ const videoTrack = await transformMediaTrack({
+ track: track as LegacyMediaTrack,
+ loadMediaAsset,
+ isMain: !isFirstVideoTrackFound,
+ });
+ isFirstVideoTrackFound = true;
+ transformedTracks.push(videoTrack);
+ continue;
+ }
- if (trackType === "text") {
- return transformTextTrack({ track });
- }
+ if (trackType === "text") {
+ transformedTracks.push(transformTextTrack({ track }));
+ continue;
+ }
- if (trackType === "audio") {
- return transformAudioTrack({ track });
- }
+ if (trackType === "audio") {
+ transformedTracks.push(transformAudioTrack({ track }));
+ continue;
+ }
- return null;
- }),
- );
+ transformedTracks.push(null);
+ }
return transformedTracks.filter(
(track): track is TimelineTrack => track !== null,
diff --git a/apps/web/src/services/storage/migrations/v1-to-v2.ts b/apps/web/src/services/storage/migrations/v1-to-v2.ts
index eff4397e..ef381541 100644
--- a/apps/web/src/services/storage/migrations/v1-to-v2.ts
+++ b/apps/web/src/services/storage/migrations/v1-to-v2.ts
@@ -33,8 +33,7 @@ export class V1toV2Migration extends StorageMigration {
});
if (!result.skipped) {
- // cleanup legacy timeline DBs after successful transformation
- await cleanupLegacyTimelineDBs({
+ void cleanupLegacyTimelineDBs({
projectId,
project: result.project,
});
@@ -60,13 +59,13 @@ function createMediaAssetLoader({
};
}
-async function cleanupLegacyTimelineDBs({
+function cleanupLegacyTimelineDBs({
projectId,
project,
}: {
projectId: string;
project: ProjectRecord;
-}): Promise {
+}): void {
const scenes = project.scenes;
if (!Array.isArray(scenes)) {
return;
@@ -89,11 +88,12 @@ async function cleanupLegacyTimelineDBs({
const projectDbName = `video-editor-timelines-${projectId}`;
dbNamesToDelete.push(projectDbName);
- for (const dbName of dbNamesToDelete) {
- try {
- await deleteDatabase({ dbName });
- } catch {
- // ignore errors, DB might not exist or already deleted
- }
- }
+ // Fire-and-forget: delete in parallel, don't block migration
+ void Promise.all(
+ dbNamesToDelete.map((dbName) =>
+ deleteDatabase({ dbName }).catch(() => {
+ // ignore errors, DB might not exist or already deleted
+ }),
+ ),
+ );
}