unsaved changes warning and loading project in hud

This commit is contained in:
Siddharth
2026-03-07 19:44:00 -08:00
parent fc7c1d28e5
commit e02ef0d2c0
5 changed files with 86 additions and 17 deletions
+2
View File
@@ -90,6 +90,8 @@ interface Window {
hudOverlayHide: () => void;
hudOverlayClose: () => void;
setMicrophoneExpanded: (expanded: boolean) => void;
setHasUnsavedChanges: (hasChanges: boolean) => void;
onRequestSaveBeforeClose: (callback: () => Promise<void>) => () => void;
};
}
+43 -2
View File
@@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { app, BrowserWindow, Menu, nativeImage, Tray } from "electron";
import { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage, Tray } from "electron";
import { registerIpcHandlers } from "./ipc/handlers";
import { createEditorWindow, createHudOverlayWindow, createSourceSelectorWindow } from "./windows";
@@ -217,12 +217,54 @@ function updateTrayMenu(recording: boolean = false) {
tray.setContextMenu(Menu.buildFromTemplate(menuTemplate));
}
let editorHasUnsavedChanges = false;
let isForceClosing = false;
ipcMain.on("set-has-unsaved-changes", (_, hasChanges: boolean) => {
editorHasUnsavedChanges = hasChanges;
});
function createEditorWindowWrapper() {
if (mainWindow) {
isForceClosing = true;
mainWindow.close();
isForceClosing = false;
mainWindow = null;
}
mainWindow = createEditorWindow();
editorHasUnsavedChanges = false;
mainWindow.on("close", (event) => {
if (isForceClosing || !editorHasUnsavedChanges) return;
event.preventDefault();
const choice = dialog.showMessageBoxSync(mainWindow!, {
type: "warning",
buttons: ["Save & Close", "Discard & Close", "Cancel"],
defaultId: 0,
cancelId: 2,
title: "Unsaved Changes",
message: "You have unsaved changes.",
detail: "Do you want to save your project before closing?",
});
if (choice === 0) {
// Save & Close — tell renderer to save, then close
mainWindow!.webContents.send("request-save-before-close");
ipcMain.once("save-before-close-done", () => {
isForceClosing = true;
mainWindow?.close();
isForceClosing = false;
});
} else if (choice === 1) {
// Discard & Close
isForceClosing = true;
mainWindow?.close();
isForceClosing = false;
}
// choice === 2: Cancel — do nothing, window stays open
});
}
function createSourceSelectorWindowWrapper() {
@@ -250,7 +292,6 @@ app.on("activate", () => {
// Register all IPC handlers when app is ready
app.whenReady().then(async () => {
// Listen for HUD overlay quit event (macOS only)
const { ipcMain } = await import("electron");
ipcMain.on("hud-overlay-close", () => {
app.quit();
});
+11
View File
@@ -102,4 +102,15 @@ contextBridge.exposeInMainWorld("electronAPI", {
setMicrophoneExpanded: (expanded: boolean) => {
ipcRenderer.send("hud:setMicrophoneExpanded", expanded);
},
setHasUnsavedChanges: (hasChanges: boolean) => {
ipcRenderer.send("set-has-unsaved-changes", hasChanges);
},
onRequestSaveBeforeClose: (callback: () => Promise<void>) => {
const listener = async () => {
await callback();
ipcRenderer.send("save-before-close-done");
};
ipcRenderer.on("request-save-before-close", listener);
return () => ipcRenderer.removeListener("request-save-before-close", listener);
},
});
+20 -4
View File
@@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { BsRecordCircle } from "react-icons/bs";
import { FaRegStopCircle } from "react-icons/fa";
import { FaFolderMinus } from "react-icons/fa6";
import { FaFolderOpen } from "react-icons/fa6";
import { FiMinus, FiX } from "react-icons/fi";
import { MdMic, MdMicOff, MdMonitor, MdVolumeOff, MdVolumeUp } from "react-icons/md";
import { MdMic, MdMicOff, MdMonitor, MdVideoFile, MdVolumeOff, MdVolumeUp } from "react-icons/md";
import { RxDragHandleDots2 } from "react-icons/rx";
import { useAudioLevelMeter } from "../../hooks/useAudioLevelMeter";
import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices";
@@ -107,6 +107,12 @@ export function LaunchWindow() {
}
};
const openProjectFile = async () => {
const result = await window.electronAPI.loadProjectFile();
if (result.canceled || !result.success) return;
await window.electronAPI.switchToEditor();
};
const sendHudOverlayHide = () => {
if (window.electronAPI && window.electronAPI.hudOverlayHide) {
window.electronAPI.hudOverlayHide();
@@ -230,14 +236,24 @@ export function LaunchWindow() {
)}
</button>
{/* Open file */}
{/* Open video file */}
<button
className={`${styles.hudIconBtn} ${styles.electronNoDrag}`}
onClick={openVideoFile}
disabled={recording}
title="Open video file"
>
<FaFolderMinus size={14} className="text-white/60" />
<MdVideoFile size={14} className="text-white/60" />
</button>
{/* Open project */}
<button
className={`${styles.hudIconBtn} ${styles.electronNoDrag}`}
onClick={openProjectFile}
disabled={recording}
title="Open project"
>
<FaFolderOpen size={14} className="text-white/60" />
</button>
{/* Window controls */}
+10 -11
View File
@@ -346,20 +346,19 @@ export default function VideoEditor() {
],
);
// Sync unsaved changes state to main process for close dialog
useEffect(() => {
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
if (!hasUnsavedChanges) {
return;
}
event.preventDefault();
event.returnValue = "";
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
window.electronAPI.setHasUnsavedChanges(hasUnsavedChanges);
}, [hasUnsavedChanges]);
// Handle save request from main process before close
useEffect(() => {
const cleanup = window.electronAPI.onRequestSaveBeforeClose(async () => {
await saveProject(false);
});
return () => cleanup();
}, [saveProject]);
const handleSaveProject = useCallback(async () => {
await saveProject(false);
}, [saveProject]);