Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
};
}
Expand Down
50 changes: 23 additions & 27 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { fileURLToPath } from "node:url";
import {
app,
BrowserWindow,
dialog,
ipcMain,
Menu,
nativeImage,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
}

Expand Down
8 changes: 8 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
});
77 changes: 77 additions & 0 deletions src/components/video-editor/UnsavedChangesDialog.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>
<DialogContent className="bg-[#09090b] border-white/10 rounded-2xl max-w-sm p-6 gap-0">
<DialogHeader className="mb-5">
<div className="flex items-center gap-3">
<img
src="./openscreen.png"
alt=""
aria-hidden="true"
className="w-9 h-9 rounded-xl flex-shrink-0"
/>
<DialogTitle className="text-base font-semibold text-slate-200 leading-tight">
{td("unsavedChanges.title")}
</DialogTitle>
</div>
</DialogHeader>

<p className="text-sm text-slate-300 mb-1">{td("unsavedChanges.message")}</p>
<DialogDescription className="text-sm text-slate-500 mb-6">
{td("unsavedChanges.detail")}
</DialogDescription>

<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 outline-none focus-visible:ring-2 focus-visible:ring-[#34B27B] focus-visible:ring-offset-2 focus-visible:ring-offset-[#09090b]"
>
<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 outline-none focus-visible:ring-2 focus-visible:ring-white/30 focus-visible:ring-offset-2 focus-visible:ring-offset-[#09090b]"
>
<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 outline-none focus-visible:ring-2 focus-visible:ring-white/20 focus-visible:ring-offset-2 focus-visible:ring-offset-[#09090b]"
>
{tc("actions.cancel")}
</button>
</div>
</DialogContent>
</Dialog>
);
}
31 changes: 31 additions & 0 deletions src/components/video-editor/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import {
type ZoomFocusMode,
type ZoomRegion,
} from "./types";
import { UnsavedChangesDialog } from "./UnsavedChangesDialog";
import VideoPlayback, { VideoPlaybackRef } from "./VideoPlayback";

export default function VideoEditor() {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -2066,6 +2090,13 @@ export default function VideoEditor() {
exportedFilePath ? () => void handleShowExportedFile(exportedFilePath) : undefined
}
/>

<UnsavedChangesDialog
isOpen={showCloseConfirmDialog}
onSaveAndClose={handleCloseConfirmSave}
onDiscardAndClose={handleCloseConfirmDiscard}
onCancel={handleCloseConfirmCancel}
/>
</div>
);
}
Loading