diff --git a/index.html b/index.html index 5919d4c..47f9c94 100644 --- a/index.html +++ b/index.html @@ -410,6 +410,8 @@ solution +
@@ -1006,6 +1008,34 @@ // queued -> running -> done (rendered) / error / cancelled. const pending = new Map(); + // --- "notify when queue empty" checkbox --- + // The checkbox only appears while solves are queued/running. Permission is + // requested the first time it's checked (not on page load). When the queue + // drains to empty it fires a notification (if still checked), then unchecks + // itself and hides until there's work again. + const notifyWrap = document.getElementById("notifyWrap"); + const notifyDone = document.getElementById("notifyDone"); + notifyDone.onchange = () => { + if (notifyDone.checked && "Notification" in window + && Notification.permission === "default") { + Notification.requestPermission(); + } + }; + // Show/hide the checkbox to match the current queue state, and uncheck it + // once the queue is empty. Call after any change to `pending`. + function updateNotifyUi() { + notifyWrap.style.display = pending.size ? "" : "none"; + if (!pending.size) notifyDone.checked = false; + } + // Fire the "queue empty" notification if the user opted in and granted it. + function notifyQueueEmpty() { + if (notifyDone.checked && "Notification" in window + && Notification.permission === "granted") { + new Notification("DWS Solver", + {body: "All queued solves have finished."}); + } + } + function uuid() { return crypto.randomUUID ? crypto.randomUUID() : String(Date.now()) + Math.random(); @@ -1168,7 +1198,7 @@ card.append( el("div", {class: "pending-head"}, [el("span", {class: "spinner"}), nameSpan, - document.createTextNode(" — "), status]), + document.createTextNode(" — "), status]), actions); return card; } @@ -1190,7 +1220,11 @@ if (!entry) return; pending.delete(token); action(entry); - if (pending.size === 0 && stream) {stream.close(); stream = null;} + if (pending.size === 0) { + if (stream) {stream.close(); stream = null;} + notifyQueueEmpty(); + } + updateNotifyUi(); } // Put a pending card into a terminal state: show the message, drop the @@ -1283,6 +1317,7 @@ // Stash the inputs alongside the pending solve so, once it finishes, // its card can export a self-contained {problem, solution} bundle. pending.set(token, {token, card, n, confirmed: false, problem, maxTime: time}); + updateNotifyUi(); fetch("/solve", { method: "POST", headers: {"Content-Type": "application/json"}, @@ -1330,8 +1365,10 @@ } actions.append(el("button", { class: "mini", type: "button", - onclick: () => exportSolve({token, n, solution: s, problem, maxTime, - name: solveLabel(token, n)}), + onclick: () => exportSolve({ + token, n, solution: s, problem, maxTime, + name: solveLabel(token, n) + }), }, "Export")); // The problem may be on hand (a solve queued from this tab) or, for a // solve opened via a share link, fetched from the server by token on @@ -1742,6 +1779,7 @@ // Load in reverse so the first id ends up on top (each load prepends). for (const token of sharedTokens.slice().reverse()) loadSharedSolve(token); if (sharedTokens.length) syncStream(); + updateNotifyUi();