Quit guard during transfers + background completion notifications
- before-quit is blocked while jobs are queued/scanning/running; a native message box asks 'N transfers are still running - quit anyway?' (Cancel default) and only an explicit Quit Anyway proceeds via Utils.quit() - when the window is unfocused, finished transfers raise a native notification (silent for completions, sound + error message for failures) - engine.hasActiveJobs()/activeJobCount() (truth table verified headless) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+52
-2
@@ -1,4 +1,4 @@
|
||||
import { ApplicationMenu, BrowserWindow, Updater } from "electrobun/bun";
|
||||
import Electrobun, { ApplicationMenu, BrowserWindow, Updater, Utils } from "electrobun/bun";
|
||||
import { BRANDING } from "../shared/branding";
|
||||
import { effectiveConcurrency } from "../shared/types";
|
||||
import { PanelContextMenu } from "./context-menu";
|
||||
@@ -69,7 +69,57 @@ const win = new BrowserWindow({
|
||||
new AppTray(win);
|
||||
contextMenu.onAction = (action) => rpc.send.menuAction({ action });
|
||||
|
||||
engine.onUpdate = (snapshot) => rpc.send.transferUpdate(snapshot);
|
||||
// notify on finished transfers while the window is in the background
|
||||
let windowFocused = true;
|
||||
win.on("focus", () => (windowFocused = true));
|
||||
win.on("blur", () => (windowFocused = false));
|
||||
const notifiedStates = new Map<string, string>();
|
||||
|
||||
engine.onUpdate = (snapshot) => {
|
||||
rpc.send.transferUpdate(snapshot);
|
||||
|
||||
const previous = notifiedStates.get(snapshot.id);
|
||||
if (previous !== snapshot.state) {
|
||||
notifiedStates.set(snapshot.id, snapshot.state);
|
||||
if (!windowFocused && (snapshot.state === "completed" || snapshot.state === "failed")) {
|
||||
Utils.showNotification({
|
||||
title: snapshot.state === "completed" ? "Transfer complete" : "Transfer failed",
|
||||
body:
|
||||
snapshot.state === "completed"
|
||||
? snapshot.name
|
||||
: `${snapshot.name}: ${snapshot.error ?? "unknown error"}`,
|
||||
silent: snapshot.state === "completed",
|
||||
});
|
||||
}
|
||||
if (snapshot.state === "completed" || snapshot.state === "failed" || snapshot.state === "cancelled") {
|
||||
// keep the map from growing over long sessions
|
||||
setTimeout(() => notifiedStates.delete(snapshot.id), 60_000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// don't quit out from under running transfers
|
||||
let quitConfirmed = false;
|
||||
Electrobun.events.on("before-quit", (event) => {
|
||||
if (quitConfirmed || !engine.hasActiveJobs()) return;
|
||||
(event as { response: { allow: boolean } }).response = { allow: false };
|
||||
void (async () => {
|
||||
const count = engine.activeJobCount();
|
||||
const { response } = await Utils.showMessageBox({
|
||||
type: "warning",
|
||||
title: BRANDING.appName,
|
||||
message: `${count} transfer${count === 1 ? " is" : "s are"} still running.`,
|
||||
detail: "Quitting now will abort them.",
|
||||
buttons: ["Quit Anyway", "Cancel"],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
});
|
||||
if (response === 0) {
|
||||
quitConfirmed = true;
|
||||
Utils.quit();
|
||||
}
|
||||
})();
|
||||
});
|
||||
events.sessionClosed = (info) => rpc.send.sessionClosed(info);
|
||||
events.connectionsChanged = () => void applyApplicationMenu(store);
|
||||
void settings.load().then((s) => engine.setConcurrency(effectiveConcurrency(s)));
|
||||
|
||||
@@ -146,6 +146,22 @@ export class TransferEngine {
|
||||
if (job && TERMINAL.includes(job.snapshot.state)) this.jobs.delete(jobId);
|
||||
}
|
||||
|
||||
/** True while any job is queued, scanning or running (quit guard). */
|
||||
hasActiveJobs(): boolean {
|
||||
for (const job of this.jobs.values()) {
|
||||
if (!TERMINAL.includes(job.snapshot.state)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
activeJobCount(): number {
|
||||
let count = 0;
|
||||
for (const job of this.jobs.values()) {
|
||||
if (!TERMINAL.includes(job.snapshot.state)) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Fail every non-terminal job that touches a dead session. */
|
||||
failSession(sessionId: string, reason: string): void {
|
||||
for (const job of this.jobs.values()) {
|
||||
|
||||
Reference in New Issue
Block a user