feat: replace native OS close dialog with custom in-app dialog
This commit is contained in:
Vendored
+2
@@ -143,6 +143,8 @@ interface Window {
|
||||
setMicrophoneExpanded: (expanded: boolean) => void;
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||
onRequestSaveBeforeClose: (callback: () => Promise<boolean> | boolean) => () => void;
|
||||
onRequestCloseConfirm: (callback: () => void) => () => void;
|
||||
sendCloseConfirmResponse: (choice: "save" | "discard" | "cancel") => void;
|
||||
setLocale: (locale: string) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
+17
-26
@@ -4,7 +4,6 @@ import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
dialog,
|
||||
ipcMain,
|
||||
Menu,
|
||||
nativeImage,
|
||||
@@ -288,35 +287,27 @@ function createEditorWindowWrapper() {
|
||||
|
||||
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"),
|
||||
});
|
||||
|
||||
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", (_, choice: "save" | "discard" | "cancel") => {
|
||||
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", (_, shouldClose: boolean) => {
|
||||
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": do nothing, window stays open
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Save, Trash2, X } from "lucide-react";
|
||||
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");
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 bg-black/80 backdrop-blur-md z-50 animate-in fade-in duration-200"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
<div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[60] bg-[#09090b] rounded-2xl shadow-2xl border border-white/10 p-6 w-[90vw] max-w-sm animate-in zoom-in-95 duration-200">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<img
|
||||
src="/openscreen.png"
|
||||
alt="OpenScreen"
|
||||
className="w-9 h-9 rounded-xl flex-shrink-0"
|
||||
/>
|
||||
<h2 className="text-base font-semibold text-slate-200 leading-tight">
|
||||
{td("unsavedChanges.title")}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="ml-auto rounded-full p-1 hover:bg-white/10 text-slate-500 hover:text-slate-300 transition-colors flex-shrink-0"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-slate-300 mb-1">{td("unsavedChanges.message")}</p>
|
||||
<p className="text-sm text-slate-500 mb-6">{td("unsavedChanges.detail")}</p>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSaveAndClose}
|
||||
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 rounded-lg bg-[#34B27B] hover:bg-[#2d9e6c] active:bg-[#27885c] text-white font-medium text-sm transition-colors"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{td("unsavedChanges.saveAndClose")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDiscardAndClose}
|
||||
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 rounded-lg bg-white/5 hover:bg-red-500/15 border border-white/10 hover:border-red-500/30 text-slate-300 hover:text-red-400 font-medium text-sm transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
{td("unsavedChanges.discardAndClose")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 rounded-lg hover:bg-white/5 text-slate-500 hover:text-slate-300 font-medium text-sm transition-colors"
|
||||
>
|
||||
{tc("actions.cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const videoPlaybackRef = useRef<VideoPlaybackRef>(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
|
||||
}
|
||||
/>
|
||||
|
||||
<UnsavedChangesDialog
|
||||
isOpen={showCloseConfirmDialog}
|
||||
onSaveAndClose={handleCloseConfirmSave}
|
||||
onDiscardAndClose={handleCloseConfirmDiscard}
|
||||
onCancel={handleCloseConfirmCancel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user