diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 85d82946..f4b379fb 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -143,6 +143,8 @@ interface Window { setMicrophoneExpanded: (expanded: boolean) => void; setHasUnsavedChanges: (hasChanges: boolean) => void; onRequestSaveBeforeClose: (callback: () => Promise | boolean) => () => void; + onRequestCloseConfirm: (callback: () => void) => () => void; + sendCloseConfirmResponse: (choice: "save" | "discard" | "cancel") => void; setLocale: (locale: string) => Promise; }; } diff --git a/electron/main.ts b/electron/main.ts index ad0a33fc..3e0b2321 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -4,7 +4,6 @@ import { fileURLToPath } from "node:url"; import { app, BrowserWindow, - dialog, ipcMain, Menu, nativeImage, @@ -253,6 +252,7 @@ function updateTrayMenu(recording: boolean = false) { let editorHasUnsavedChanges = false; let isForceClosing = false; +let isCloseConfirmInFlight = false; ipcMain.on("set-has-unsaved-changes", (_, hasChanges: boolean) => { editorHasUnsavedChanges = hasChanges; @@ -284,39 +284,35 @@ function createEditorWindowWrapper() { editorHasUnsavedChanges = false; mainWindow.on("close", (event) => { - if (isForceClosing || !editorHasUnsavedChanges) return; + if (isForceClosing || !editorHasUnsavedChanges || isCloseConfirmInFlight) return; event.preventDefault(); - - const choice = dialog.showMessageBoxSync(mainWindow!, { - type: "warning", - buttons: [ - mainT("dialogs", "unsavedChanges.saveAndClose"), - mainT("dialogs", "unsavedChanges.discardAndClose"), - mainT("common", "actions.cancel"), - ], - defaultId: 0, - cancelId: 2, - title: mainT("dialogs", "unsavedChanges.title"), - message: mainT("dialogs", "unsavedChanges.message"), - detail: mainT("dialogs", "unsavedChanges.detail"), - }); + isCloseConfirmInFlight = true; const windowToClose = mainWindow; if (!windowToClose || windowToClose.isDestroyed()) return; - if (choice === 0) { - // Save & Close — tell renderer to save, then close - windowToClose.webContents.send("request-save-before-close"); - ipcMain.once("save-before-close-done", (_, shouldClose: boolean) => { - if (!shouldClose) return; + // Ask renderer to show the custom in-app dialog + windowToClose.webContents.send("request-close-confirm"); + + ipcMain.once("close-confirm-response", (event, choice: "save" | "discard" | "cancel") => { + if (event.sender.id !== windowToClose?.webContents.id) return; + isCloseConfirmInFlight = false; + if (!windowToClose || windowToClose.isDestroyed()) return; + + if (choice === "save") { + // Tell renderer to save the project, then close when done + windowToClose.webContents.send("request-save-before-close"); + ipcMain.once("save-before-close-done", (event, shouldClose: boolean) => { + if (event.sender.id !== windowToClose?.webContents.id) return; + if (!shouldClose) return; + forceCloseEditorWindow(windowToClose); + }); + } else if (choice === "discard") { forceCloseEditorWindow(windowToClose); - }); - } else if (choice === 1) { - // Discard & Close - forceCloseEditorWindow(windowToClose); - } - // choice === 2: Cancel — do nothing, window stays open + } + // "cancel": flag reset, window stays open + }); }); } diff --git a/electron/preload.ts b/electron/preload.ts index 46e16f04..2e065bd5 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -163,4 +163,12 @@ contextBridge.exposeInMainWorld("electronAPI", { ipcRenderer.on("request-save-before-close", listener); return () => ipcRenderer.removeListener("request-save-before-close", listener); }, + onRequestCloseConfirm: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on("request-close-confirm", listener); + return () => ipcRenderer.removeListener("request-close-confirm", listener); + }, + sendCloseConfirmResponse: (choice: "save" | "discard" | "cancel") => { + ipcRenderer.send("close-confirm-response", choice); + }, }); diff --git a/src/components/video-editor/UnsavedChangesDialog.tsx b/src/components/video-editor/UnsavedChangesDialog.tsx new file mode 100644 index 00000000..902b1427 --- /dev/null +++ b/src/components/video-editor/UnsavedChangesDialog.tsx @@ -0,0 +1,77 @@ +import { Save, Trash2 } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useScopedT } from "@/contexts/I18nContext"; + +interface UnsavedChangesDialogProps { + isOpen: boolean; + onSaveAndClose: () => void; + onDiscardAndClose: () => void; + onCancel: () => void; +} + +export function UnsavedChangesDialog({ + isOpen, + onSaveAndClose, + onDiscardAndClose, + onCancel, +}: UnsavedChangesDialogProps) { + const td = useScopedT("dialogs"); + const tc = useScopedT("common"); + + return ( + !open && onCancel()}> + + +
+ + + {td("unsavedChanges.title")} + +
+
+ +

{td("unsavedChanges.message")}

+ + {td("unsavedChanges.detail")} + + +
+ + + +
+
+
+ ); +} diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 7adc558e..14c695ab 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -74,6 +74,7 @@ import { type ZoomFocusMode, type ZoomRegion, } from "./types"; +import { UnsavedChangesDialog } from "./UnsavedChangesDialog"; import VideoPlayback, { VideoPlaybackRef } from "./VideoPlayback"; export default function VideoEditor() { @@ -144,6 +145,7 @@ export default function VideoEditor() { format: string; } | null>(null); const [isFullscreen, setIsFullscreen] = useState(false); + const [showCloseConfirmDialog, setShowCloseConfirmDialog] = useState(false); const playerContainerRef = useRef(null); const videoPlaybackRef = useRef(null); @@ -524,6 +526,28 @@ export default function VideoEditor() { return () => cleanup(); }, [saveProject]); + useEffect(() => { + const cleanup = window.electronAPI.onRequestCloseConfirm(() => { + setShowCloseConfirmDialog(true); + }); + return () => cleanup(); + }, []); + + const handleCloseConfirmSave = useCallback(() => { + setShowCloseConfirmDialog(false); + window.electronAPI.sendCloseConfirmResponse("save"); + }, []); + + const handleCloseConfirmDiscard = useCallback(() => { + setShowCloseConfirmDialog(false); + window.electronAPI.sendCloseConfirmResponse("discard"); + }, []); + + const handleCloseConfirmCancel = useCallback(() => { + setShowCloseConfirmDialog(false); + window.electronAPI.sendCloseConfirmResponse("cancel"); + }, []); + const handleSaveProject = useCallback(async () => { await saveProject(false); }, [saveProject]); @@ -2066,6 +2090,13 @@ export default function VideoEditor() { exportedFilePath ? () => void handleShowExportedFile(exportedFilePath) : undefined } /> + + ); }