From e02ef0d2c00f478973598f0185b158435fd61d65 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Sat, 7 Mar 2026 19:44:00 -0800 Subject: [PATCH] unsaved changes warning and loading project in hud --- electron/electron-env.d.ts | 2 + electron/main.ts | 45 ++++++++++++++++++++- electron/preload.ts | 11 +++++ src/components/launch/LaunchWindow.tsx | 24 +++++++++-- src/components/video-editor/VideoEditor.tsx | 21 +++++----- 5 files changed, 86 insertions(+), 17 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 938cca0..8818fc2 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -90,6 +90,8 @@ interface Window { hudOverlayHide: () => void; hudOverlayClose: () => void; setMicrophoneExpanded: (expanded: boolean) => void; + setHasUnsavedChanges: (hasChanges: boolean) => void; + onRequestSaveBeforeClose: (callback: () => Promise) => () => void; }; } diff --git a/electron/main.ts b/electron/main.ts index cacdf6c..93c7590 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -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(); }); diff --git a/electron/preload.ts b/electron/preload.ts index 4ccc07a..f74b63a 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -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) => { + 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); + }, }); diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 1ad80ba..f565d1e 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -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() { )} - {/* Open file */} + {/* Open video file */} + + {/* Open project */} + {/* Window controls */} diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 7ce2553..2702100 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -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]);