fix(editor): track unsaved changes for new projects (#321)

fix(editor): track unsaved changes for new projects
This commit is contained in:
Sid
2026-04-05 11:02:42 -07:00
committed by GitHub
3 changed files with 80 additions and 18 deletions
+23 -18
View File
@@ -32,6 +32,8 @@ import { ExportDialog } from "./ExportDialog";
import PlaybackControls from "./PlaybackControls";
import {
createProjectData,
createProjectSnapshot,
hasProjectUnsavedChanges,
deriveNextId,
fromFileUrl,
normalizeProjectEditor,
@@ -239,13 +241,11 @@ export default function VideoEditor() {
) + 1;
setLastSavedSnapshot(
JSON.stringify(
createProjectData(
webcamSourcePath
? { screenVideoPath: sourcePath, webcamVideoPath: webcamSourcePath }
: { screenVideoPath: sourcePath },
normalizedEditor,
),
createProjectSnapshot(
webcamSourcePath
? { screenVideoPath: sourcePath, webcamVideoPath: webcamSourcePath }
: { screenVideoPath: sourcePath },
normalizedEditor,
),
);
return true;
@@ -257,8 +257,7 @@ export default function VideoEditor() {
if (!currentProjectMedia) {
return null;
}
return JSON.stringify(
createProjectData(currentProjectMedia, {
return createProjectSnapshot(currentProjectMedia, {
wallpaper,
shadowIntensity,
showBlur,
@@ -279,8 +278,7 @@ export default function VideoEditor() {
gifFrameRate,
gifLoop,
gifSizePreset,
}),
);
});
}, [
currentProjectMedia,
wallpaper,
@@ -305,11 +303,9 @@ export default function VideoEditor() {
gifSizePreset,
]);
const hasUnsavedChanges = Boolean(
currentProjectPath &&
currentProjectSnapshot &&
lastSavedSnapshot &&
currentProjectSnapshot !== lastSavedSnapshot,
const hasUnsavedChanges = hasProjectUnsavedChanges(
currentProjectSnapshot,
lastSavedSnapshot,
);
useEffect(() => {
@@ -338,7 +334,14 @@ export default function VideoEditor() {
setWebcamVideoSourcePath(webcamSourcePath);
setWebcamVideoPath(webcamSourcePath ? toFileUrl(webcamSourcePath) : null);
setCurrentProjectPath(null);
setLastSavedSnapshot(null);
setLastSavedSnapshot(
createProjectSnapshot(
webcamSourcePath
? { screenVideoPath: sourcePath, webcamVideoPath: webcamSourcePath }
: { screenVideoPath: sourcePath },
INITIAL_EDITOR_STATE,
),
);
return;
}
@@ -350,7 +353,9 @@ export default function VideoEditor() {
setWebcamVideoSourcePath(null);
setWebcamVideoPath(null);
setCurrentProjectPath(null);
setLastSavedSnapshot(null);
setLastSavedSnapshot(
createProjectSnapshot({ screenVideoPath: sourcePath }, INITIAL_EDITOR_STATE),
);
} else {
setError("No video to load. Please record or select a video.");
}
@@ -1,6 +1,8 @@
import { describe, expect, it } from "vitest";
import {
createProjectData,
createProjectSnapshot,
hasProjectUnsavedChanges,
normalizeProjectEditor,
PROJECT_VERSION,
resolveProjectMedia,
@@ -65,3 +67,40 @@ describe("projectPersistence media compatibility", () => {
).toBe("rectangle");
});
});
it("creates stable snapshots for identical project state", () => {
const media = {
screenVideoPath: "/tmp/screen.webm",
webcamVideoPath: "/tmp/webcam.webm",
};
const editor = normalizeProjectEditor({
wallpaper: "/wallpapers/wallpaper1.jpg",
shadowIntensity: 0,
showBlur: false,
motionBlurAmount: 0,
borderRadius: 0,
padding: 50,
cropRegion: { x: 0, y: 0, width: 1, height: 1 },
zoomRegions: [],
trimRegions: [],
speedRegions: [],
annotationRegions: [],
aspectRatio: "16:9",
webcamLayoutPreset: "picture-in-picture",
webcamMaskShape: "circle",
exportQuality: "good",
exportFormat: "mp4",
gifFrameRate: 15,
gifLoop: true,
gifSizePreset: "medium",
});
expect(createProjectSnapshot(media, editor)).toBe(createProjectSnapshot(media, editor));
});
it("detects unsaved changes from differing snapshots", () => {
expect(hasProjectUnsavedChanges(null, null)).toBe(false);
expect(hasProjectUnsavedChanges("same", "same")).toBe(false);
expect(hasProjectUnsavedChanges("current", "baseline")).toBe(true);
});
@@ -405,3 +405,21 @@ export function createProjectData(
editor,
};
}
export function createProjectSnapshot(
media: ProjectMedia,
editor: ProjectEditorState,
): string {
return JSON.stringify(createProjectData(media, editor));
}
export function hasProjectUnsavedChanges(
currentSnapshot: string | null,
baselineSnapshot: string | null,
): boolean {
return Boolean(
currentSnapshot !== null &&
baselineSnapshot !== null &&
currentSnapshot !== baselineSnapshot,
);
}