fix(settings): protect unsaved edits on window close, quit, and reload

The settingsUnsavedChangesGuard only covers router navigation; closing
the Electron window, quitting the app, or reloading the page silently
dropped staged settings edits.

Renderer: SettingsUnloadGuardService (provided by SettingsComponent)
arms a beforeunload handler while the form is dirty — native leave
prompt in the PWA, cancelled-and-redialogued reload in Electron — and
mirrors the dirty state to the main process.

Main process: WindowCloseGuard intercepts BrowserWindow close before
beforeunload fires, pushes WINDOW:CLOSE_REQUESTED to the renderer for
the same save/discard/stay dialog, and replays the original intent
(close vs. quit, tracked via before-quit) only after the renderer
confirms. A failed save never confirms, keeping the guard's semantics.
The guard disarms on renderer navigation, crash, or destroyed
webContents so a window can never become unclosable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-09 10:51:44 +02:00
1 parent 1a6af75761
commit 3f9348584a
13 files changed
+980 -1

No files matched your search

@@ -618,6 +618,18 @@ export interface ElectronBridgeApi {
onWindowStateChange: (
callback: (state: ElectronBridgeWindowState) => void
) => () => void;
/**
* While active, the main process intercepts window close / app quit and
* pushes a close request to the renderer instead of closing. Used by the
* settings page while its form holds unsaved edits.
*/
setWindowCloseGuard: (active: boolean) => Promise<void>;
/**
* Completes a close the guard intercepted: the main process re-runs the
* original intent (window close or app quit) with the guard bypassed.
*/
confirmWindowClose: () => Promise<void>;
onWindowCloseRequested: (callback: () => void) => () => void;
fetchPlaylistByUrl: (
url: string,
title?: string,
@@ -113,3 +113,9 @@ export const WINDOW_TOGGLE_MAXIMIZE = 'WINDOW:TOGGLE_MAXIMIZE';
export const WINDOW_CLOSE = 'WINDOW:CLOSE';
export const WINDOW_GET_STATE = 'WINDOW:GET_STATE';
export const WINDOW_STATE_CHANGED = 'WINDOW:STATE_CHANGED';
// Close guard: while active, closing/quitting the app is intercepted in the
// main process and handed to the renderer for a save/discard/stay decision
export const WINDOW_SET_CLOSE_GUARD = 'WINDOW:SET_CLOSE_GUARD';
export const WINDOW_CONFIRM_CLOSE = 'WINDOW:CONFIRM_CLOSE';
export const WINDOW_CLOSE_REQUESTED = 'WINDOW:CLOSE_REQUESTED';