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 { BRANDING } from "../shared/branding";
|
||||||
import { effectiveConcurrency } from "../shared/types";
|
import { effectiveConcurrency } from "../shared/types";
|
||||||
import { PanelContextMenu } from "./context-menu";
|
import { PanelContextMenu } from "./context-menu";
|
||||||
@@ -69,7 +69,57 @@ const win = new BrowserWindow({
|
|||||||
new AppTray(win);
|
new AppTray(win);
|
||||||
contextMenu.onAction = (action) => rpc.send.menuAction({ action });
|
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.sessionClosed = (info) => rpc.send.sessionClosed(info);
|
||||||
events.connectionsChanged = () => void applyApplicationMenu(store);
|
events.connectionsChanged = () => void applyApplicationMenu(store);
|
||||||
void settings.load().then((s) => engine.setConcurrency(effectiveConcurrency(s)));
|
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);
|
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. */
|
/** Fail every non-terminal job that touches a dead session. */
|
||||||
failSession(sessionId: string, reason: string): void {
|
failSession(sessionId: string, reason: string): void {
|
||||||
for (const job of this.jobs.values()) {
|
for (const job of this.jobs.values()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user