diff --git a/js-dialog-preload.js b/js-dialog-preload.js index f69a44a1..7d37b5a2 100644 --- a/js-dialog-preload.js +++ b/js-dialog-preload.js @@ -4,5 +4,6 @@ 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)), + // value: a string for prompt(); { response, checkboxChecked } for one of Theseus's own prompts. + answer: (reqId, ok, value) => ipcRenderer.invoke("jsdialog-answer", reqId, !!ok, value == null ? null : (typeof value === "object" ? value : String(value))), }); diff --git a/js-dialog.html b/js-dialog.html index beb6d700..f8cea12e 100644 --- a/js-dialog.html +++ b/js-dialog.html @@ -1,121 +1,170 @@ - - - - -Dialog - - - - - - + + + + +Dialog + + + + + + diff --git a/main.js b/main.js index 8372063a..371c39d5 100644 --- a/main.js +++ b/main.js @@ -2163,8 +2163,7 @@ function initAddons() { restartApp: async (addonName) => { const who = String(addonName || "An add-on").slice(0, 60); console.log(`[restart] ${who} asked to relaunch Theseus`); - const parent = win && !win.isDestroyed() ? win : undefined; - const { response } = await dialog.showMessageBox(parent, { + const { response } = await askSheet({ type: "question", title: "Restart Theseus?", message: `${who} wants to restart Theseus.`, detail: "Usually to finish installing its own update. Open tabs are restored after the restart. If you choose Later, the update still applies the next time Theseus starts.", @@ -4191,12 +4190,12 @@ async function installExtensionWithConsent(id, requester) { try { card = (await fetchCommunityCatalog()).find((e) => e.id === id) || null; } catch {} const parent = win && !win.isDestroyed() ? win : undefined; if (!card) { - await dialog.showMessageBox(parent, { type: "warning", title: "Extension not found", message: `"${id}" is not in the community catalog.`, buttons: ["OK"] }); + await askSheet({ type: "warning", title: "Extension not found", message: `"${id}" is not in the community catalog.`, buttons: ["OK"] }); return { ok: false, error: "not in the catalog" }; } const installed = addonHost ? addonHost.snapshot().installed.find((a) => a.id === id) : null; if (installed && addonUpdater.cmpVer(card.latest, installed.version) <= 0) { - const { response: r0 } = await dialog.showMessageBox(parent, { + const { response: r0 } = await askSheet({ type: "info", title: "Already installed", message: `${card.name || id} ${installed.version} is already installed.`, detail: "Newer signed versions arrive through the regular update check. Manage it under Settings › Extensions.", @@ -4206,7 +4205,7 @@ async function installExtensionWithConsent(id, requester) { return { ok: true, version: installed.version, publisher: installed.publisher || card.publisher, alreadyInstalled: true }; } const from = requester ? `Requested by ${requester}.\n\n` : ""; - const { response } = await dialog.showMessageBox(parent, { + const { response } = await askSheet({ type: "question", title: "Install extension", message: installed ? `Update ${card.name || id} ${installed.version} → ${card.latest}?` @@ -4218,7 +4217,7 @@ async function installExtensionWithConsent(id, requester) { if (response !== 0) return { ok: false, error: "cancelled" }; const r = await installCommunityById(id); if (r.ok) { - const { response: after } = await dialog.showMessageBox(parent, { + const { response: after } = await askSheet({ type: "info", title: "Extension installed", message: `${card.name || id} ${r.version} is installed and active.`, detail: `Signed by ${r.publisher || card.publisher}. Manage it under Settings › Extensions.`, @@ -4226,7 +4225,7 @@ async function installExtensionWithConsent(id, requester) { }); if (after === 1) openSettingsTab("addons"); } else { - await dialog.showMessageBox(parent, { type: "error", title: "Install failed", message: `${card.name || id} was not installed.`, detail: r.error || "unknown error", buttons: ["OK"] }); + await askSheet({ type: "error", title: "Install failed", message: `${card.name || id} was not installed.`, detail: r.error || "unknown error", buttons: ["OK"] }); } return r; } finally { installPromptOpen = false; } @@ -4456,6 +4455,14 @@ function jsDialogWho(sender, tab) { } function jsDialogReply(item, ok, value) { const { kind } = item.req; + if (item.resolve) { // one of Theseus's own prompts (askSheet) + const v = value && typeof value === "object" ? value : {}; + const buttons = item.req.buttons || []; + let response = ok ? Number(v.response) : item.req.cancelId; + if (!Number.isInteger(response) || response < 0 || response >= buttons.length) response = item.req.cancelId; + item.resolve({ response, checkboxChecked: !!v.checkboxChecked }); + return; + } const out = kind === "confirm" ? (ok ? "1" : "0") : kind === "prompt" ? (ok ? String(value ?? "") : null) : ""; try { item.e.returnValue = { handled: true, value: out }; } catch {} } @@ -4504,6 +4511,33 @@ function finishJsDialog(ok, value) { try { const t = cur.tabId != null ? tabById(cur.tabId) : null; if (t && t.id === activeId) t.view.webContents.focus(); } catch {} pumpJsDialog(); } +// Theseus's own prompts — install this app / extension, remove, restart — +// in the same sheet as page dialogs. Same option shape as +// dialog.showMessageBox (title, message, detail, buttons, defaultId, +// cancelId, checkboxLabel, checkboxChecked, icon, type), same result; the +// native box remains the fallback when the browser window is not there. +function askSheet(opts) { + const o = opts || {}; + const buttons = Array.isArray(o.buttons) && o.buttons.length ? o.buttons.map(String) : ["OK"]; + const defaultId = Number.isInteger(o.defaultId) && o.defaultId >= 0 && o.defaultId < buttons.length ? o.defaultId : 0; + const cancelId = Number.isInteger(o.cancelId) && o.cancelId >= 0 && o.cancelId < buttons.length ? o.cancelId : (buttons.length > 1 ? buttons.length - 1 : 0); + if (!jsDialogPop || !winAlive()) { + const parent = win && !win.isDestroyed() ? win : undefined; + return dialog.showMessageBox(parent, { ...o, noLink: true }).catch(() => ({ response: cancelId, checkboxChecked: false })); + } + let iconUrl = null; + try { if (o.icon && typeof o.icon.toDataURL === "function" && !o.icon.isEmpty()) iconUrl = o.icon.toDataURL(); } catch {} + const req = { + reqId: ++jsDialogSeq, kind: "app", who: { label: "Theseus", kind: "app" }, + title: String(o.title || ""), message: String(o.message || ""), detail: String(o.detail || ""), + buttons, defaultId, cancelId, type: String(o.type || "none"), iconUrl, + checkbox: o.checkboxLabel ? { label: String(o.checkboxLabel), checked: !!o.checkboxChecked } : null, + }; + return new Promise((resolve) => { + jsDialogQueue.push({ resolve, tabId: null, req }); + 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) { @@ -6361,6 +6395,7 @@ if (!process.env.THESEUS_NO_AUTOSTART && !app.requestSingleInstanceLock()) { initAddons(); webapps.init({ parentWindow: () => (win && !win.isDestroyed() ? win : undefined), + ask: askSheet, openInTab: openInMainTab, targetUrlFor, isBnsHost, prepareContents: (wc) => { styleScrollbars(wc); try { wc.setWebRTCIPHandlingPolicy(webrtcPolicy()); } catch {} try { wc.setBackgroundThrottling(settings.backgroundThrottle); } catch {} applyFingerprint(wc); }, diff --git a/webapps.js b/webapps.js index a46cebd1..b51cbb61 100644 --- a/webapps.js +++ b/webapps.js @@ -269,7 +269,8 @@ async function install(desc, ctx = {}) { const parent = deps.parentWindow ? deps.parentWindow() : undefined; let icon; // best effort preview in the dialog try { const raw = await fetchBytes(desc.iconUrl || ""); const i = nativeImage.createFromBuffer(raw); if (!i.isEmpty()) icon = i.resize({ width: 64, height: 64 }); } catch {} - const { response, checkboxChecked } = await dialog.showMessageBox(parent, { + const ask = (o) => (deps.ask ? deps.ask(o) : dialog.showMessageBox(parent, o)); + const { response, checkboxChecked } = await ask({ type: "question", title: "Install app", icon, message: `Install ${desc.name}?`, detail: `${desc.host}\n\nIt opens in its own window and gets a Start Menu entry. It keeps running inside Theseus, with your names, add-ons and settings.`, @@ -302,7 +303,8 @@ async function uninstall(key, ask = true) { const entry = find(key); if (!entry) return false; if (ask) { const parent = deps.parentWindow ? deps.parentWindow() : undefined; - const { response } = await dialog.showMessageBox(parent, { + const ask = (o) => (deps.ask ? deps.ask(o) : dialog.showMessageBox(parent, o)); + const { response } = await ask({ type: "question", title: "Remove app", message: `Remove ${entry.name} from Theseus?`, detail: "Its window and shortcuts go away. The site itself and your data on it are untouched.", buttons: ["Remove", "Cancel"], defaultId: 0, cancelId: 1, noLink: true,