feat(theseus): page dialogs drawn by Theseus instead of Chromium's stock boxes
alert / confirm / prompt from a page came up as bare OS message boxes titled "theseus-navigator" (the package name), with no hint of who was asking and nothing of the browser's styling — the PDF Editor's "Delete signature?" was the reported case. The session preload replaces the page's three functions with wrappers that hand the call to the isolated world through a DOM event, which asks main synchronously and writes the answer back; pages see Chrome's return values (confirm → boolean, prompt → string or null) and no new global. Main answers from a sheet hanging under the toolbar, in the same surface as add-on approvals, that names who is asking: the site's host, the add-on's name for an add-on page or panel (identified by its path under the profile's extensions directory), or Theseus for its own pages. The sheet belongs to the tab that asked — hidden while another tab is in front, back when its tab returns — and a closing tab or window answers "cancel" so no renderer stays blocked. Windows without the chrome (installed apps, plain windows) get a native box with a proper title, and app.name now reads "Theseus Navigator" for whatever else still shows one.
This commit is contained in:
parent
ce066bb4ca
commit
a75707d0ed
5 changed files with 290 additions and 1 deletions
|
|
@ -52,3 +52,57 @@ try {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
// ---- page dialogs (alert / confirm / prompt) ----
|
||||||
|
// Chromium's stock boxes are bare OS message boxes titled with the package
|
||||||
|
// name. The main world's alert/confirm/prompt are replaced with wrappers that
|
||||||
|
// hand the call to this isolated world through a DOM event (synchronous, no
|
||||||
|
// global left on window for pages to fingerprint), which asks main over
|
||||||
|
// sendSync and writes the answer back on the element. If nothing answers,
|
||||||
|
// the wrapper falls through to the original function. Theseus's own views
|
||||||
|
// share the session and get the same wrappers; main decides by sender.
|
||||||
|
try {
|
||||||
|
const { webFrame } = require("electron");
|
||||||
|
document.addEventListener("theseus:jsdialog", (e) => {
|
||||||
|
const el = e.target;
|
||||||
|
if (!el || typeof el.getAttribute !== "function" || el.localName !== "theseus-dialog") return;
|
||||||
|
let r = null;
|
||||||
|
try {
|
||||||
|
r = ipcRenderer.sendSync("js-dialog", {
|
||||||
|
kind: el.getAttribute("data-kind"),
|
||||||
|
message: el.getAttribute("data-message") || "",
|
||||||
|
def: el.hasAttribute("data-default") ? el.getAttribute("data-default") : null,
|
||||||
|
});
|
||||||
|
} catch { r = null; }
|
||||||
|
if (r && typeof r === "object" && r.handled) {
|
||||||
|
el.setAttribute("data-handled", r.value == null ? "null" : "1");
|
||||||
|
el.setAttribute("data-result", r.value == null ? "" : String(r.value));
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
webFrame.executeJavaScript([
|
||||||
|
"(() => {",
|
||||||
|
" const natives = { alert: window.alert, confirm: window.confirm, prompt: window.prompt };",
|
||||||
|
" const ask = (kind, message, def) => {",
|
||||||
|
" const root = document.documentElement; if (!root) return null;",
|
||||||
|
" const el = document.createElement('theseus-dialog'); el.hidden = true;",
|
||||||
|
" el.setAttribute('data-kind', kind); el.setAttribute('data-message', message);",
|
||||||
|
" if (def != null) el.setAttribute('data-default', def);",
|
||||||
|
" root.appendChild(el);",
|
||||||
|
" try { el.dispatchEvent(new Event('theseus:jsdialog', { bubbles: true })); } finally { el.remove(); }",
|
||||||
|
" if (!el.hasAttribute('data-handled')) return null;",
|
||||||
|
" return { value: el.getAttribute('data-handled') === 'null' ? null : el.getAttribute('data-result') };",
|
||||||
|
" };",
|
||||||
|
" const wrap = (kind) => function (message, def) {",
|
||||||
|
" const msg = arguments.length ? String(message) : '';",
|
||||||
|
" const r = ask(kind, msg, kind === 'prompt' ? (def == null ? '' : String(def)) : null);",
|
||||||
|
" if (!r) return natives[kind].apply(window, arguments);",
|
||||||
|
" if (kind === 'alert') return undefined;",
|
||||||
|
" if (kind === 'confirm') return r.value === '1';",
|
||||||
|
" return r.value;",
|
||||||
|
" };",
|
||||||
|
" for (const k of ['alert', 'confirm', 'prompt']) {",
|
||||||
|
" try { const f = wrap(k); Object.defineProperty(f, 'name', { value: k }); f.toString = () => 'function ' + k + '() { [native code] }'; window[k] = f; } catch (e) {}",
|
||||||
|
" }",
|
||||||
|
"})();",
|
||||||
|
].join("\n")).catch(() => {});
|
||||||
|
} catch {}
|
||||||
|
|
|
||||||
8
js-dialog-preload.js
Normal file
8
js-dialog-preload.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// Preload for the page-dialog overlay (js-dialog.html): the Theseus-drawn
|
||||||
|
// replacement for a page's alert / confirm / prompt. Main pushes one request
|
||||||
|
// at a time via `jsdialog-show`; the page answers with `jsdialog-answer`.
|
||||||
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
contextBridge.exposeInMainWorld("jsdialog", {
|
||||||
|
onShow: (cb) => ipcRenderer.on("jsdialog-show", (_e, req) => cb(req)),
|
||||||
|
answer: (reqId, ok, value) => ipcRenderer.invoke("jsdialog-answer", reqId, !!ok, value == null ? null : String(value)),
|
||||||
|
});
|
||||||
86
js-dialog.html
Normal file
86
js-dialog.html
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Dialog</title>
|
||||||
|
<style>
|
||||||
|
/* Same surface tokens as approval.html so page dialogs and add-on approvals
|
||||||
|
read as one family. */
|
||||||
|
:root { color-scheme: light dark;
|
||||||
|
--surface:#1c222c; --surface2:#0f1621; --line:rgba(255,255,255,.12);
|
||||||
|
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d; }
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root { --surface:#ffffff; --surface2:#f1f4fa; --line:rgba(0,0,0,.12);
|
||||||
|
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --acid:#0AC18E; }
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; height: 100%; background: transparent; }
|
||||||
|
body { font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color: var(--ink); }
|
||||||
|
.promptmask { position: fixed; inset: 0; background: rgba(0,0,0,.35);
|
||||||
|
display: grid; place-items: start center; padding-top: 0; }
|
||||||
|
/* A sheet hanging from the toolbar, the way Chrome anchors page dialogs:
|
||||||
|
the page stays visible and dimmed behind it. */
|
||||||
|
.promptbox { background: var(--surface); border: 1px solid var(--line); border-top: none;
|
||||||
|
border-radius: 0 0 12px 12px; padding: 14px 18px 14px;
|
||||||
|
width: min(480px, calc(100vw - 32px)); max-height: calc(100vh - 24px); overflow: auto;
|
||||||
|
box-shadow: 0 20px 60px #000b; animation: drop .12s ease-out; }
|
||||||
|
@keyframes drop { from { transform: translateY(-8px); opacity: 0; } to { transform: none; opacity: 1; } }
|
||||||
|
.who { display: flex; align-items: center; gap: 7px; color: var(--dim); font-size: 11.5px; margin-bottom: 8px; min-width: 0; }
|
||||||
|
.who .src { color: var(--mut); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.who .src.site { font: 600 12px/1.3 ui-monospace, "Cascadia Code", Consolas, monospace; }
|
||||||
|
.body { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); font-size: 13.5px; margin: 0 0 12px; max-height: 40vh; overflow: auto; }
|
||||||
|
.body:empty { display: none; }
|
||||||
|
input.pin { display: block; width: 100%; margin: 0 0 12px; padding: 8px 10px; border-radius: 7px;
|
||||||
|
background: var(--surface2); color: var(--ink); border: 1px solid var(--line); font: inherit; font-size: 13px; }
|
||||||
|
input.pin:focus { outline: none; border-color: rgb(from var(--acid) r g b / .6); }
|
||||||
|
.pact { display: flex; gap: 6px; justify-content: flex-end; }
|
||||||
|
.pbtn { padding: 7px 16px; border-radius: 7px; border: 1px solid var(--line); background: var(--surface2);
|
||||||
|
color: var(--ink); cursor: pointer; font: inherit; font-size: 12.5px; min-width: 76px; }
|
||||||
|
.pbtn:hover { border-color: rgb(from var(--acid) r g b / .35); }
|
||||||
|
.pbtn.primary { background: var(--acid); color: #0b0e14; border-color: transparent; font-weight: 650; }
|
||||||
|
.pbtn:focus-visible { outline: 2px solid rgb(from var(--acid) r g b / .6); outline-offset: 1px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" })[c]);
|
||||||
|
let current = null;
|
||||||
|
function finish(ok) {
|
||||||
|
if (!current) return;
|
||||||
|
const { reqId, kind } = current;
|
||||||
|
const input = document.getElementById("pin");
|
||||||
|
const value = kind === "prompt" ? (ok ? (input ? input.value : "") : null) : null;
|
||||||
|
current = null;
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
window.jsdialog.answer(reqId, !!ok, value);
|
||||||
|
}
|
||||||
|
window.jsdialog.onShow((req) => {
|
||||||
|
current = req;
|
||||||
|
const who = req.who || { label: "This page", kind: "site" };
|
||||||
|
const icon = who.kind === "addon" ? "🧩" : who.kind === "app" ? "⛓" : "🌐";
|
||||||
|
const verb = req.kind === "prompt" ? "asks" : "says";
|
||||||
|
document.body.innerHTML =
|
||||||
|
`<div class="promptmask"><div class="promptbox" role="${req.kind === "alert" ? "alertdialog" : "dialog"}" aria-modal="true">
|
||||||
|
<div class="who"><span>${icon}</span><span class="src ${esc(who.kind)}" title="${esc(who.label)}">${esc(who.label)}</span><span>${verb}</span></div>
|
||||||
|
<div class="body">${esc(req.message)}</div>
|
||||||
|
${req.kind === "prompt" ? `<input class="pin" id="pin" type="text" value="${esc(req.def ?? "")}" spellcheck="false">` : ""}
|
||||||
|
<div class="pact">
|
||||||
|
${req.kind === "alert" ? "" : `<button class="pbtn" type="button" id="cancel">Cancel</button>`}
|
||||||
|
<button class="pbtn primary" type="button" id="ok">OK</button>
|
||||||
|
</div>
|
||||||
|
</div></div>`;
|
||||||
|
document.getElementById("ok").addEventListener("click", () => finish(true));
|
||||||
|
const c = document.getElementById("cancel"); if (c) c.addEventListener("click", () => finish(false));
|
||||||
|
const mask = document.querySelector(".promptmask");
|
||||||
|
mask.addEventListener("mousedown", (e) => { if (e.target === mask) finish(req.kind === "alert"); });
|
||||||
|
const input = document.getElementById("pin");
|
||||||
|
setTimeout(() => { if (input) { input.focus(); input.select(); } else document.getElementById("ok").focus(); }, 0);
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (!current) return;
|
||||||
|
if (e.key === "Escape") { e.preventDefault(); finish(current.kind === "alert"); }
|
||||||
|
else if (e.key === "Enter" && !(e.target && e.target.id === "cancel")) { e.preventDefault(); finish(true); }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
141
main.js
141
main.js
|
|
@ -69,6 +69,10 @@ if (process.env.THESEUS_USER_DATA) {
|
||||||
try { app.setPath("userData", relocateProfile(app.getPath("appData"))); }
|
try { app.setPath("userData", relocateProfile(app.getPath("appData"))); }
|
||||||
catch (e) { console.warn("profile relocation failed:", e?.message); }
|
catch (e) { console.warn("profile relocation failed:", e?.message); }
|
||||||
}
|
}
|
||||||
|
// Native boxes (and anything else that reads app.name) say "Theseus Navigator",
|
||||||
|
// not the package name. After the userData paths above: the default profile
|
||||||
|
// location derives from the name, and those are set explicitly already.
|
||||||
|
try { app.setName("Theseus Navigator"); } catch {}
|
||||||
function relocateProfile(appData) {
|
function relocateProfile(appData) {
|
||||||
const newDir = path.join(appData, "Theseus");
|
const newDir = path.join(appData, "Theseus");
|
||||||
// Two possible previous locations: "Theseus Navigator" (what the code
|
// Two possible previous locations: "Theseus Navigator" (what the code
|
||||||
|
|
@ -2468,6 +2472,7 @@ function layout() {
|
||||||
// Approval overlay sits exactly over the tab area — the page underneath
|
// Approval overlay sits exactly over the tab area — the page underneath
|
||||||
// keeps running; only pointer input is intercepted.
|
// keeps running; only pointer input is intercepted.
|
||||||
if (approvalPop) approvalPop.setBounds({ x: 0, y: CHROME_H, width: tabW, height: bodyH });
|
if (approvalPop) approvalPop.setBounds({ x: 0, y: CHROME_H, width: tabW, height: bodyH });
|
||||||
|
if (jsDialogPop) jsDialogPop.setBounds({ x: 0, y: CHROME_H, width: tabW, height: bodyH });
|
||||||
positionPopover();
|
positionPopover();
|
||||||
positionEnginePicker();
|
positionEnginePicker();
|
||||||
positionDownloads();
|
positionDownloads();
|
||||||
|
|
@ -2849,6 +2854,7 @@ function setActive(id) {
|
||||||
if (t && !t.addonId && !t.settings) lastCapturableTabId = t.id;
|
if (t && !t.addonId && !t.settings) lastCapturableTabId = t.id;
|
||||||
if (t?.prov) pushNav(t.prov);
|
if (t?.prov) pushNav(t.prov);
|
||||||
chrome.webContents.send("bcnr-offer", t?.bcnrOffer ? { host: t.bcnrOffer.host, tld: t.bcnrOffer.tld, registry: REGISTRY } : null);
|
chrome.webContents.send("bcnr-offer", t?.bcnrOffer ? { host: t.bcnrOffer.host, tld: t.bcnrOffer.tld, registry: REGISTRY } : null);
|
||||||
|
syncJsDialogVisibility();
|
||||||
emitTabs();
|
emitTabs();
|
||||||
}
|
}
|
||||||
function emitTabs() {
|
function emitTabs() {
|
||||||
|
|
@ -3339,6 +3345,7 @@ function closeTab(id) {
|
||||||
const i = tabs.findIndex((t) => t.id === id);
|
const i = tabs.findIndex((t) => t.id === id);
|
||||||
if (i < 0) return;
|
if (i < 0) return;
|
||||||
const [t] = tabs.splice(i, 1);
|
const [t] = tabs.splice(i, 1);
|
||||||
|
dismissJsDialogFor(id);
|
||||||
win.contentView.removeChildView(t.view);
|
win.contentView.removeChildView(t.view);
|
||||||
t.view.webContents.destroy?.();
|
t.view.webContents.destroy?.();
|
||||||
if (tabs.length === 0) { createTab(); return; }
|
if (tabs.length === 0) { createTab(); return; }
|
||||||
|
|
@ -3446,6 +3453,13 @@ function createWindow() {
|
||||||
styleScrollbars(approvalPop.webContents);
|
styleScrollbars(approvalPop.webContents);
|
||||||
deferOverlayLoad(approvalPop, "approval.html");
|
deferOverlayLoad(approvalPop, "approval.html");
|
||||||
approvalPop.setVisible(false);
|
approvalPop.setVisible(false);
|
||||||
|
// Page dialogs (alert / confirm / prompt) — see the js-dialog block.
|
||||||
|
jsDialogPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "js-dialog-preload.js") } });
|
||||||
|
try { jsDialogPop.setBackgroundColor("#00000000"); } catch {}
|
||||||
|
win.contentView.addChildView(jsDialogPop);
|
||||||
|
styleScrollbars(jsDialogPop.webContents);
|
||||||
|
deferOverlayLoad(jsDialogPop, "js-dialog.html");
|
||||||
|
jsDialogPop.setVisible(false);
|
||||||
// Everything that isn't needed to paint the toolbar waits for chrome.html.
|
// Everything that isn't needed to paint the toolbar waits for chrome.html.
|
||||||
// dom-ready, NOT did-finish-load: the load event also waits for every
|
// dom-ready, NOT did-finish-load: the load event also waits for every
|
||||||
// subresource, and the bookmarks bar pulls its favicons over bns:// —
|
// subresource, and the bookmarks bar pulls its favicons over bns:// —
|
||||||
|
|
@ -3465,7 +3479,8 @@ function createWindow() {
|
||||||
win.on("close", () => { saveSession(); sessionSavedAtClose = true; });
|
win.on("close", () => { saveSession(); sessionSavedAtClose = true; });
|
||||||
win.on("closed", () => {
|
win.on("closed", () => {
|
||||||
win = null; chrome = null; popover = null; enginePicker = null; downloadsPop = null;
|
win = null; chrome = null; popover = null; enginePicker = null; downloadsPop = null;
|
||||||
addressPicker = null; pwFillPop = null; linkStatus = null; sidebar = null; approvalPop = null;
|
dismissJsDialogFor(null);
|
||||||
|
addressPicker = null; pwFillPop = null; linkStatus = null; sidebar = null; approvalPop = null; jsDialogPop = null;
|
||||||
linkStatusVisible = false;
|
linkStatusVisible = false;
|
||||||
});
|
});
|
||||||
layout();
|
layout();
|
||||||
|
|
@ -4364,6 +4379,130 @@ function injectionsForSender(e, href) {
|
||||||
ipcMain.on("addon-inject-scripts", (e, href) => { e.returnValue = injectionsForSender(e, href); });
|
ipcMain.on("addon-inject-scripts", (e, href) => { e.returnValue = injectionsForSender(e, href); });
|
||||||
// Approval overlay. One request at a time; later callers queue behind the
|
// Approval overlay. One request at a time; later callers queue behind the
|
||||||
// visible one so two dapps can't race each other for the same click.
|
// visible one so two dapps can't race each other for the same click.
|
||||||
|
// ---- page dialogs: alert / confirm / prompt ----
|
||||||
|
// Chromium's stock JavaScript dialogs are bare OS message boxes titled with
|
||||||
|
// the package name ("theseus-navigator"). The session preload reroutes the
|
||||||
|
// page's calls here, synchronously (sendSync), and the answer comes from a
|
||||||
|
// Theseus-drawn sheet under the toolbar that names who is asking — the
|
||||||
|
// site's host, or the add-on's name for add-on pages — like Chrome's
|
||||||
|
// "example.com says". Windows without the chrome (app windows, plain
|
||||||
|
// windows) fall back to a native box with a proper title.
|
||||||
|
let jsDialogPop = null;
|
||||||
|
const jsDialogQueue = [];
|
||||||
|
let jsDialogCurrent = null; // { e, req, tabId }
|
||||||
|
let jsDialogSeq = 0;
|
||||||
|
function jsDialogWho(sender, tab) {
|
||||||
|
let u = ""; try { u = sender.getURL() || ""; } catch {}
|
||||||
|
// Add-on pages — a full-tab page, a sidebar panel, a background page — all
|
||||||
|
// load from <profile>/extensions/<id>/…; the id is the first segment after
|
||||||
|
// that directory. (A tab's addonId flag would also stay set after the tab
|
||||||
|
// navigated elsewhere, so the URL is the reliable source.)
|
||||||
|
if (/^file:/i.test(u)) {
|
||||||
|
try {
|
||||||
|
const base = url.pathToFileURL(addonsUserDir()).href.replace(/\/?$/, "/");
|
||||||
|
if (u.startsWith(base)) {
|
||||||
|
const id = decodeURIComponent(u.slice(base.length).split(/[/?#]/)[0]);
|
||||||
|
const a = addonHost && addonHost.getInstalled().find((x) => x.manifest && x.manifest.id === id);
|
||||||
|
return { label: a && a.manifest.name ? String(a.manifest.name) : id, kind: "addon" };
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
if (tab && (tab.settings || (tab.prov && tab.prov.kind === "home"))) return { label: "Theseus", kind: "app" };
|
||||||
|
// file: outside a tab is one of Theseus's own views; inside a tab it is a local file the user opened.
|
||||||
|
if (/^file:/i.test(u)) return tab ? { label: "This page", kind: "site" } : { label: "Theseus", kind: "app" };
|
||||||
|
try { const p = new URL(u.replace(/^bns:\/\//i, "https://")); if (p.host) return { label: p.host, kind: "site" }; } catch {}
|
||||||
|
return { label: "This page", kind: "site" };
|
||||||
|
}
|
||||||
|
function jsDialogReply(item, ok, value) {
|
||||||
|
const { kind } = item.req;
|
||||||
|
const out = kind === "confirm" ? (ok ? "1" : "0") : kind === "prompt" ? (ok ? String(value ?? "") : null) : "";
|
||||||
|
try { item.e.returnValue = { handled: true, value: out }; } catch {}
|
||||||
|
}
|
||||||
|
function pumpJsDialog() {
|
||||||
|
if (jsDialogCurrent || !jsDialogQueue.length) return;
|
||||||
|
if (!jsDialogPop || !winAlive()) { for (const it of jsDialogQueue.splice(0)) jsDialogReply(it, false, null); return; }
|
||||||
|
const next = jsDialogQueue.shift();
|
||||||
|
if (next.tabId != null && !tabById(next.tabId)) { jsDialogReply(next, false, null); return pumpJsDialog(); }
|
||||||
|
jsDialogCurrent = next;
|
||||||
|
// The dialog belongs to the tab that asked: bring that tab forward so
|
||||||
|
// the sheet never appears over an unrelated page. (A panel's dialog has
|
||||||
|
// no tab and shows over whatever is current.)
|
||||||
|
if (next.tabId != null && next.tabId !== activeId) { try { setActive(next.tabId); } catch {} }
|
||||||
|
overlayReady(jsDialogPop).then(() => {
|
||||||
|
if (jsDialogCurrent !== next) return;
|
||||||
|
try {
|
||||||
|
jsDialogPop.webContents.send("jsdialog-show", next.req);
|
||||||
|
jsDialogPop.setVisible(true);
|
||||||
|
try { win.contentView.removeChildView(jsDialogPop); win.contentView.addChildView(jsDialogPop); } catch {}
|
||||||
|
layout();
|
||||||
|
jsDialogPop.webContents.focus();
|
||||||
|
} catch (err) {
|
||||||
|
jsDialogCurrent = null;
|
||||||
|
jsDialogReply(next, false, null);
|
||||||
|
console.warn("page dialog show failed:", err?.message);
|
||||||
|
pumpJsDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// The sheet belongs to one tab: hidden while another tab is in front, back
|
||||||
|
// (and above any tab added since) when its tab returns. A panel's sheet has
|
||||||
|
// no tab and stays.
|
||||||
|
function syncJsDialogVisibility() {
|
||||||
|
if (!jsDialogPop || !jsDialogCurrent || !winAlive()) return;
|
||||||
|
const show = jsDialogCurrent.tabId == null || jsDialogCurrent.tabId === activeId;
|
||||||
|
try {
|
||||||
|
jsDialogPop.setVisible(show);
|
||||||
|
if (show) { win.contentView.removeChildView(jsDialogPop); win.contentView.addChildView(jsDialogPop); jsDialogPop.webContents.focus(); }
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
function finishJsDialog(ok, value) {
|
||||||
|
const cur = jsDialogCurrent; if (!cur) return;
|
||||||
|
jsDialogCurrent = null;
|
||||||
|
try { if (jsDialogPop) jsDialogPop.setVisible(false); } catch {}
|
||||||
|
jsDialogReply(cur, ok, value);
|
||||||
|
try { const t = cur.tabId != null ? tabById(cur.tabId) : null; if (t && t.id === activeId) t.view.webContents.focus(); } catch {}
|
||||||
|
pumpJsDialog();
|
||||||
|
}
|
||||||
|
// A tab closing (or the window going away) must not leave its renderer
|
||||||
|
// blocked inside a sendSync that nobody will answer.
|
||||||
|
function dismissJsDialogFor(tabId) {
|
||||||
|
for (let i = jsDialogQueue.length - 1; i >= 0; i--) if (tabId == null || jsDialogQueue[i].tabId === tabId) jsDialogReply(jsDialogQueue.splice(i, 1)[0], false, null);
|
||||||
|
if (jsDialogCurrent && (tabId == null || jsDialogCurrent.tabId === tabId)) finishJsDialog(false, null);
|
||||||
|
}
|
||||||
|
ipcMain.on("js-dialog", (e, raw) => {
|
||||||
|
const kind = ["alert", "confirm", "prompt"].includes(raw && raw.kind) ? raw.kind : "alert";
|
||||||
|
const message = String((raw && raw.message) ?? "").slice(0, 4000);
|
||||||
|
const def = raw && raw.def != null ? String(raw.def).slice(0, 2000) : null;
|
||||||
|
const tab = tabs.find((t) => t.view?.webContents === e.sender);
|
||||||
|
const who = jsDialogWho(e.sender, tab);
|
||||||
|
// Anything living in the browser window — a tab, a sidebar panel, an
|
||||||
|
// add-on's page — gets the sheet. Only another window (an installed app's
|
||||||
|
// window, a plain window) gets a native box: the sheet is drawn in win.
|
||||||
|
let owner = null; try { owner = BrowserWindow.fromWebContents(e.sender); } catch {}
|
||||||
|
const inMain = winAlive() && (!!tab || !owner || owner === win);
|
||||||
|
if (!inMain || !jsDialogPop) {
|
||||||
|
if (kind === "prompt") { e.returnValue = { handled: false }; return; }
|
||||||
|
let parent; try { parent = BrowserWindow.fromWebContents(e.sender) || undefined; } catch {}
|
||||||
|
let r = 0;
|
||||||
|
try {
|
||||||
|
r = dialog.showMessageBoxSync(parent, {
|
||||||
|
type: kind === "confirm" ? "question" : "info", title: "Theseus Navigator",
|
||||||
|
message: `${who.label} says`, detail: message,
|
||||||
|
buttons: kind === "confirm" ? ["OK", "Cancel"] : ["OK"], defaultId: 0, cancelId: 1, noLink: true,
|
||||||
|
});
|
||||||
|
} catch { r = 1; }
|
||||||
|
e.returnValue = { handled: true, value: kind === "confirm" ? (r === 0 ? "1" : "0") : "" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jsDialogQueue.push({ e, tabId: tab ? tab.id : null, req: { reqId: ++jsDialogSeq, kind, message, def, who } });
|
||||||
|
pumpJsDialog();
|
||||||
|
});
|
||||||
|
ipcMain.handle("jsdialog-answer", (e, reqId, ok, value) => {
|
||||||
|
if (!jsDialogPop || e.sender !== jsDialogPop.webContents) return false;
|
||||||
|
if (!jsDialogCurrent || jsDialogCurrent.req.reqId !== reqId) return false;
|
||||||
|
finishJsDialog(!!ok, value);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
let approvalPop = null;
|
let approvalPop = null;
|
||||||
const approvalQueue = [];
|
const approvalQueue = [];
|
||||||
let approvalCurrent = null; // { reqId, resolve }
|
let approvalCurrent = null; // { reqId, resolve }
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,8 @@
|
||||||
"bcnr-preload.js",
|
"bcnr-preload.js",
|
||||||
"bcnr-origin.js",
|
"bcnr-origin.js",
|
||||||
"webapps.js",
|
"webapps.js",
|
||||||
|
"js-dialog.html",
|
||||||
|
"js-dialog-preload.js",
|
||||||
"lib/**/*",
|
"lib/**/*",
|
||||||
"package.json",
|
"package.json",
|
||||||
"node_modules/**/*",
|
"node_modules/**/*",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue