fix(theseus): extension updates re-check, announce and install in place
Updates published after launch never showed: the add-on channel was checked once, 30 s after boot, and staged copies only applied on the next launch with nothing telling the user. On 2026-09-22 Aegis 0.8.3 and VPN 0.1.3 landed minutes after the app's only check and stayed invisible through manual scans made earlier and a restart made before they were published. Now: the check repeats every 4 hours; every check (boot, timer, manual, add-on-driven) reports what is staged to the chrome, which shows a chip for staged extensions; clicking it, or the "Update to vX" button that appears on the extension's row and detail in Settings, promotes the staged folder over the installed one and rebuilds the add-on host, so the new version runs without a restart. Plug-ins (Aegis) are excluded from the chip and the hot swap — a wallet updates from its own panel and applies on the next launch. Also from the same review: the new-tab button follows the last tab and parks after the scroll arrow only when the strip overflows; Tor sits left of the Aegis chip; plug-ins no longer appear in Settings › Extensions (they have Plug-ins); the extension detail view has a labelled Back button, a close button and a Check now button; the promotion helper returns what it promoted and accepts a filter.
This commit is contained in:
parent
806a976647
commit
b2cfcd6dbf
6 changed files with 159 additions and 36 deletions
|
|
@ -53,12 +53,16 @@ function readAddonJson(dir) {
|
||||||
// Called BEFORE seedBundledAddons at boot. Any staged folder whose version
|
// Called BEFORE seedBundledAddons at boot. Any staged folder whose version
|
||||||
// beats the currently installed copy is promoted; older or matching stages
|
// beats the currently installed copy is promoted; older or matching stages
|
||||||
// are cleaned up so they don't loop on every boot.
|
// are cleaned up so they don't loop on every boot.
|
||||||
function promoteStagedUpdates({ addonsDir, backupsDir, stagedDir, logger }) {
|
// `only(manifest)` limits the promotion to some staged entries (the live
|
||||||
|
// "Install update" path applies extensions in place but leaves plug-ins for
|
||||||
|
// the next launch). Returns the list of { id, version } actually promoted.
|
||||||
|
function promoteStagedUpdates({ addonsDir, backupsDir, stagedDir, logger, only }) {
|
||||||
const log = logger || (() => {});
|
const log = logger || (() => {});
|
||||||
if (!fs.existsSync(stagedDir)) return;
|
const promoted = [];
|
||||||
|
if (!fs.existsSync(stagedDir)) return promoted;
|
||||||
let entries;
|
let entries;
|
||||||
try { entries = fs.readdirSync(stagedDir, { withFileTypes: true }); }
|
try { entries = fs.readdirSync(stagedDir, { withFileTypes: true }); }
|
||||||
catch (e) { log("promote: readdir failed:", e?.message); return; }
|
catch (e) { log("promote: readdir failed:", e?.message); return promoted; }
|
||||||
for (const de of entries) {
|
for (const de of entries) {
|
||||||
if (!de.isDirectory()) continue;
|
if (!de.isDirectory()) continue;
|
||||||
const from = path.join(stagedDir, de.name);
|
const from = path.join(stagedDir, de.name);
|
||||||
|
|
@ -68,6 +72,7 @@ function promoteStagedUpdates({ addonsDir, backupsDir, stagedDir, logger }) {
|
||||||
try { fs.rmSync(from, { recursive: true, force: true }); } catch {}
|
try { fs.rmSync(from, { recursive: true, force: true }); } catch {}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (typeof only === "function" && !only(manifest)) continue;
|
||||||
const target = path.join(addonsDir, manifest.id);
|
const target = path.join(addonsDir, manifest.id);
|
||||||
const currentVer = readAddonJson(target)?.version;
|
const currentVer = readAddonJson(target)?.version;
|
||||||
if (currentVer && cmpVer(currentVer, manifest.version) >= 0) {
|
if (currentVer && cmpVer(currentVer, manifest.version) >= 0) {
|
||||||
|
|
@ -82,13 +87,14 @@ function promoteStagedUpdates({ addonsDir, backupsDir, stagedDir, logger }) {
|
||||||
try { fs.renameSync(target, backup); }
|
try { fs.renameSync(target, backup); }
|
||||||
catch (e) { log(`promote: backup ${manifest.id} failed, keeping staged for next boot:`, e?.message); continue; }
|
catch (e) { log(`promote: backup ${manifest.id} failed, keeping staged for next boot:`, e?.message); continue; }
|
||||||
}
|
}
|
||||||
try { fs.renameSync(from, target); log(`promote: ${manifest.id} -> ${manifest.version}`); }
|
try { fs.renameSync(from, target); log(`promote: ${manifest.id} -> ${manifest.version}`); promoted.push({ id: manifest.id, version: manifest.version }); }
|
||||||
catch (e) {
|
catch (e) {
|
||||||
// Cross-drive rename can fail on Windows; copy then rm.
|
// Cross-drive rename can fail on Windows; copy then rm.
|
||||||
try { fs.cpSync(from, target, { recursive: true }); fs.rmSync(from, { recursive: true, force: true }); log(`promote: ${manifest.id} -> ${manifest.version} (via copy)`); }
|
try { fs.cpSync(from, target, { recursive: true }); fs.rmSync(from, { recursive: true, force: true }); promoted.push({ id: manifest.id, version: manifest.version }); log(`promote: ${manifest.id} -> ${manifest.version} (via copy)`); }
|
||||||
catch (ee) { log(`promote: move ${manifest.id} failed:`, ee.message); }
|
catch (ee) { log(`promote: move ${manifest.id} failed:`, ee.message); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return promoted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- HTTP helpers --------------------------------------------------
|
// ---------- HTTP helpers --------------------------------------------------
|
||||||
|
|
|
||||||
58
chrome.html
58
chrome.html
|
|
@ -55,6 +55,7 @@
|
||||||
matter how many tabs are open; the row scrolls (wheel) once tabs hit
|
matter how many tabs are open; the row scrolls (wheel) once tabs hit
|
||||||
their minimum width, like Chrome. */
|
their minimum width, like Chrome. */
|
||||||
.tabs > .newtab { flex: none; margin-left: 2px; }
|
.tabs > .newtab { flex: none; margin-left: 2px; }
|
||||||
|
.tabrow > .newtab { flex: none; align-self: center; margin-left: 2px; }
|
||||||
/* Firefox-style overflow: once tabs hit their minimum width the row
|
/* Firefox-style overflow: once tabs hit their minimum width the row
|
||||||
scrolls, the earliest tabs slide out on the left, and an arrow at each
|
scrolls, the earliest tabs slide out on the left, and an arrow at each
|
||||||
end moves the row (they only appear when there is something to scroll). */
|
end moves the row (they only appear when there is something to scroll). */
|
||||||
|
|
@ -492,10 +493,11 @@
|
||||||
<button class="extmore" id="extmore" title="Extensions" aria-label="Extensions"></button>
|
<button class="extmore" id="extmore" title="Extensions" aria-label="Extensions"></button>
|
||||||
</span>
|
</span>
|
||||||
<span class="upchip" id="upchip" hidden><button class="upcore" id="upDownload" title=""></button><button class="updismiss" id="upDismiss" title="Not now">✕</button></span>
|
<span class="upchip" id="upchip" hidden><button class="upcore" id="upDownload" title=""></button><button class="updismiss" id="upDismiss" title="Not now">✕</button></span>
|
||||||
|
<span class="upchip" id="xupchip" hidden><button class="upcore" id="xupRestart" title=""></button><button class="updismiss" id="xupDismiss" title="Not now">✕</button></span>
|
||||||
<button class="logo" id="logo" title="Settings"><span>⛓ Theseus</span><span class="gear">⚙</span></button>
|
<button class="logo" id="logo" title="Settings"><span>⛓ Theseus</span><span class="gear">⚙</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="loadbar" id="loadbar"></div>
|
<div class="loadbar" id="loadbar"></div>
|
||||||
<div class="bookmarks" id="bookmarks" hidden><div id="bmlist"></div><span class="bmright"><span class="plugdock" id="plugdock" hidden></span><button class="tor" id="tor" title="Route traffic through Tor">🧅 Tor: Off</button></span></div>
|
<div class="bookmarks" id="bookmarks" hidden><div id="bmlist"></div><span class="bmright"><button class="tor" id="tor" title="Route traffic through Tor">🧅 Tor: Off</button><span class="plugdock" id="plugdock" hidden></span></span></div>
|
||||||
<div class="findbar" id="findbar" hidden>
|
<div class="findbar" id="findbar" hidden>
|
||||||
<input id="findInput" type="text" placeholder="Find in page" spellcheck="false" autocomplete="off">
|
<input id="findInput" type="text" placeholder="Find in page" spellcheck="false" autocomplete="off">
|
||||||
<span class="findcount" id="findCount">–</span>
|
<span class="findcount" id="findCount">–</span>
|
||||||
|
|
@ -1237,6 +1239,33 @@
|
||||||
});
|
});
|
||||||
$("upDismiss") && ($("upDismiss").onclick = () => T.dismissUpdate());
|
$("upDismiss") && ($("upDismiss").onclick = () => T.dismissUpdate());
|
||||||
|
|
||||||
|
// ---- staged extension updates chip ----
|
||||||
|
// Main sends "addon-updates" { staged: [{id,name,version}] } after every
|
||||||
|
// update check (boot, 4-hourly, manual) — extensions only, plug-ins update
|
||||||
|
// from inside their own panel. Clicking installs the staged versions in
|
||||||
|
// place (no restart). Dismiss hides this exact set; a new version staged
|
||||||
|
// later brings the chip back.
|
||||||
|
let xupDismissedKey = "";
|
||||||
|
function renderAddonUpdates(d) {
|
||||||
|
const chip = $("xupchip"); if (!chip) return;
|
||||||
|
const staged = (d && Array.isArray(d.staged)) ? d.staged : [];
|
||||||
|
const key = staged.map((s) => s.id + "@" + s.version).sort().join(",");
|
||||||
|
if (!staged.length || key === xupDismissedKey) { chip.hidden = true; return; }
|
||||||
|
chip.hidden = false;
|
||||||
|
const core = $("xupRestart");
|
||||||
|
const names = staged.map((s) => (s.name || s.id) + " " + s.version).join(", ");
|
||||||
|
core.textContent = staged.length === 1 ? `↻ Update ${staged[0].name || staged[0].id} to ${staged[0].version}` : `↻ ${staged.length} extension updates — install`;
|
||||||
|
core.title = `Signed updates ready: ${names}. Installs them now, no restart needed.`;
|
||||||
|
core.onclick = async () => {
|
||||||
|
core.textContent = "↻ Installing…"; core.onclick = null;
|
||||||
|
try { await (T.applyStagedAddons ? T.applyStagedAddons() : Promise.resolve()); } catch {}
|
||||||
|
// main re-sends "addon-updates" with whatever is still pending.
|
||||||
|
};
|
||||||
|
$("xupDismiss").onclick = () => { xupDismissedKey = key; chip.hidden = true; };
|
||||||
|
}
|
||||||
|
T.onAddonUpdates && T.onAddonUpdates(renderAddonUpdates);
|
||||||
|
T.stagedAddons && T.stagedAddons().then((list) => renderAddonUpdates({ staged: list || [] })).catch(() => {});
|
||||||
|
|
||||||
// ---- security badge + minimal registry indicator ----
|
// ---- security badge + minimal registry indicator ----
|
||||||
// Shield colour communicates connection state at a glance:
|
// Shield colour communicates connection state at a glance:
|
||||||
// neutral — home / resolving (no site OR pending)
|
// neutral — home / resolving (no site OR pending)
|
||||||
|
|
@ -1445,15 +1474,20 @@
|
||||||
updateTab(el, t);
|
updateTab(el, t);
|
||||||
order.push(el);
|
order.push(el);
|
||||||
}
|
}
|
||||||
// "+" lives next to the row, not in it (see CSS) — created once.
|
// "+" follows the last tab (Firefox/Chrome style) while the row fits;
|
||||||
if (!document.getElementById("newtab")) {
|
// once tabs overflow it parks after the right scroll arrow so it stays
|
||||||
const plus = document.createElement("span"); plus.className = "newtab"; plus.id = "newtab"; plus.textContent = "+"; plus.title = "New tab (Ctrl+T)";
|
// reachable. placeNewTab() moves it between the two spots — created once.
|
||||||
|
let plus = document.getElementById("newtab");
|
||||||
|
if (!plus) {
|
||||||
|
plus = document.createElement("span"); plus.className = "newtab"; plus.id = "newtab"; plus.textContent = "+"; plus.title = "New tab (Ctrl+T)";
|
||||||
plus.onclick = () => T.newTab();
|
plus.onclick = () => T.newTab();
|
||||||
box.parentElement.appendChild(plus);
|
box.appendChild(plus);
|
||||||
}
|
}
|
||||||
const keep = new Set(order);
|
const keep = new Set(order); keep.add(plus);
|
||||||
for (const el of [...box.children]) if (!keep.has(el)) el.remove();
|
for (const el of [...box.children]) if (!keep.has(el)) el.remove();
|
||||||
order.forEach((el, i) => { if (box.children[i] !== el) box.insertBefore(el, box.children[i] || null); });
|
order.forEach((el, i) => { if (box.children[i] !== el) box.insertBefore(el, box.children[i] || null); });
|
||||||
|
if (plus.parentElement === box && box.lastElementChild !== plus) box.appendChild(plus);
|
||||||
|
placeNewTab();
|
||||||
// Bring the selected tab into view when the SELECTION changes (or a tab
|
// Bring the selected tab into view when the SELECTION changes (or a tab
|
||||||
// appears). Not on every tabs event — those fire on every title/favicon/
|
// appears). Not on every tabs event — those fire on every title/favicon/
|
||||||
// loading change, and re-snapping then would undo the user's own
|
// loading change, and re-snapping then would undo the user's own
|
||||||
|
|
@ -1477,8 +1511,20 @@
|
||||||
// Arrows at both ends of the row: shown only while the row overflows,
|
// Arrows at both ends of the row: shown only while the row overflows,
|
||||||
// each disabled at its end of travel. A click moves by ~60% of the
|
// each disabled at its end of travel. A click moves by ~60% of the
|
||||||
// visible width; the wheel scrolls too.
|
// visible width; the wheel scrolls too.
|
||||||
|
// The + button: inside the row after the last tab while everything fits,
|
||||||
|
// after the right arrow once the row overflows. Measured with the button
|
||||||
|
// out of the row so its own width can't flip the decision back and forth.
|
||||||
|
function placeNewTab() {
|
||||||
|
const box = $("tabs"), plus = $("newtab"); if (!plus) return;
|
||||||
|
const strip = box.parentElement;
|
||||||
|
const tabsWidth = [...box.children].filter((el) => el !== plus).reduce((w, el) => w + el.getBoundingClientRect().width + 4, 0);
|
||||||
|
const over = tabsWidth + (plus.getBoundingClientRect().width || 28) > box.clientWidth + 1;
|
||||||
|
if (over && plus.parentElement === box) strip.appendChild(plus);
|
||||||
|
else if (!over && plus.parentElement !== box) box.appendChild(plus);
|
||||||
|
}
|
||||||
function updateTabScroll() {
|
function updateTabScroll() {
|
||||||
const box = $("tabs"), left = $("tabsLeft"), right = $("tabsRight");
|
const box = $("tabs"), left = $("tabsLeft"), right = $("tabsRight");
|
||||||
|
placeNewTab();
|
||||||
const over = box.scrollWidth > box.clientWidth + 1;
|
const over = box.scrollWidth > box.clientWidth + 1;
|
||||||
left.hidden = right.hidden = !over;
|
left.hidden = right.hidden = !over;
|
||||||
if (!over) return;
|
if (!over) return;
|
||||||
|
|
|
||||||
67
main.js
67
main.js
|
|
@ -2055,13 +2055,15 @@ function initAddons() {
|
||||||
pubkeysHex: ADDON_UPDATE_PUBKEYS,
|
pubkeysHex: ADDON_UPDATE_PUBKEYS,
|
||||||
logger: (...a) => console.log("[addons]", ...a),
|
logger: (...a) => console.log("[addons]", ...a),
|
||||||
});
|
});
|
||||||
return { report: result?.report || [], skipped: result?.skipped || null, staged: listStagedAddons(stagedDir) };
|
const staged = listStagedAddons(stagedDir);
|
||||||
|
notifyStagedAddons(staged);
|
||||||
|
return { report: result?.report || [], skipped: result?.skipped || null, staged };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[addons] panel-driven check-updates failed:", e?.message || e);
|
console.warn("[addons] panel-driven check-updates failed:", e?.message || e);
|
||||||
return { report: [], skipped: "unexpected-error", staged: [] };
|
return { report: [], skipped: "unexpected-error", staged: [] };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
restartApp: () => { try { app.relaunch(); } catch {} app.quit(); },
|
restartApp: () => { console.log("[restart] requested by an add-on"); try { app.relaunch(); } catch {} app.quit(); },
|
||||||
// open-tab (addon-file variant): open one of the add-on's OWN files in a
|
// open-tab (addon-file variant): open one of the add-on's OWN files in a
|
||||||
// full tab. The path is joined against the resolved add-on folder and
|
// full tab. The path is joined against the resolved add-on folder and
|
||||||
// rejected if the result escapes it — belt-and-braces with the sanity
|
// rejected if the result escapes it — belt-and-braces with the sanity
|
||||||
|
|
@ -4015,10 +4017,11 @@ ipcMain.handle("bcnr:installExtension", (e, id) => {
|
||||||
try { requester = new URL(e.sender.getURL()).host || null; } catch {}
|
try { requester = new URL(e.sender.getURL()).host || null; } catch {}
|
||||||
return installExtensionWithConsent(id, requester);
|
return installExtensionWithConsent(id, requester);
|
||||||
});
|
});
|
||||||
ipcMain.handle("addons-check-updates", async () => {
|
// Background / manual add-on update check. Whatever gets staged is pushed to
|
||||||
|
// the chrome ("addon-updates") so the toolbar can offer "Restart to apply".
|
||||||
|
async function pollAddonUpdates(reason) {
|
||||||
const stagedDir = addonsStagedDir();
|
const stagedDir = addonsStagedDir();
|
||||||
let report = [];
|
let report = [], skipped = null;
|
||||||
let skipped = null;
|
|
||||||
try {
|
try {
|
||||||
const result = await addonUpdater.checkAndStageUpdates({
|
const result = await addonUpdater.checkAndStageUpdates({
|
||||||
addonsDir: addonsUserDir(),
|
addonsDir: addonsUserDir(),
|
||||||
|
|
@ -4029,8 +4032,40 @@ ipcMain.handle("addons-check-updates", async () => {
|
||||||
});
|
});
|
||||||
report = result?.report || [];
|
report = result?.report || [];
|
||||||
skipped = result?.skipped || null;
|
skipped = result?.skipped || null;
|
||||||
} catch (e) { console.warn("[addons] check-updates failed:", e?.message || e); }
|
} catch (e) { console.warn(`[addons] check-updates (${reason}) failed:`, e?.message || e); }
|
||||||
return { report, skipped, staged: listStagedAddons(stagedDir) };
|
const staged = listStagedAddons(stagedDir);
|
||||||
|
notifyStagedAddons(staged);
|
||||||
|
return { report, skipped, staged };
|
||||||
|
}
|
||||||
|
// Plug-ins (manifest category "plugin", e.g. Aegis) carry their own update
|
||||||
|
// UI inside their panel, so the toolbar chip only announces extensions.
|
||||||
|
function isPluginAddon(id) {
|
||||||
|
try { return (addonHost?.snapshot().installed || []).some((a) => a.id === id && a.category === "plugin"); } catch { return false; }
|
||||||
|
}
|
||||||
|
function notifyStagedAddons(staged) {
|
||||||
|
const list = (staged || listStagedAddons(addonsStagedDir())).filter((s) => !isPluginAddon(s.id));
|
||||||
|
try { chrome?.webContents.send("addon-updates", { staged: list.map((s) => ({ id: s.id, name: s.name, version: s.version })) }); } catch {}
|
||||||
|
}
|
||||||
|
ipcMain.handle("addons-check-updates", () => pollAddonUpdates("manual"));
|
||||||
|
// Apply staged extension updates now (no restart): promote the staged
|
||||||
|
// folder(s) over the installed copy and rebuild the add-on host, which
|
||||||
|
// re-requires each add-on's main from disk. Plug-ins are left for the next
|
||||||
|
// launch — a wallet mid-session is not something to hot-swap. `id` limits
|
||||||
|
// the apply to one extension (Settings row button); omitted = every
|
||||||
|
// staged extension (toolbar chip).
|
||||||
|
ipcMain.handle("addons-apply-staged", (_e, id) => {
|
||||||
|
if (!addonHost) return { ok: false, error: "add-ons not ready", applied: [] };
|
||||||
|
const want = typeof id === "string" && id ? id : null;
|
||||||
|
const applied = addonUpdater.promoteStagedUpdates({
|
||||||
|
addonsDir: addonsUserDir(),
|
||||||
|
backupsDir: addonsBackupDir(),
|
||||||
|
stagedDir: addonsStagedDir(),
|
||||||
|
logger: (...a) => console.log("[addons]", ...a),
|
||||||
|
only: (m) => (want ? m.id === want : true) && !isPluginAddon(m.id),
|
||||||
|
});
|
||||||
|
if (applied.length) { addonHost.discoverAndActivate(); emitSidebarState(); }
|
||||||
|
notifyStagedAddons();
|
||||||
|
return { ok: true, applied, pending: listStagedAddons(addonsStagedDir()).map((s) => ({ id: s.id, version: s.version })) };
|
||||||
});
|
});
|
||||||
ipcMain.handle("addons-list-staged", () => listStagedAddons(addonsStagedDir()));
|
ipcMain.handle("addons-list-staged", () => listStagedAddons(addonsStagedDir()));
|
||||||
function listStagedAddons(stagedDir) {
|
function listStagedAddons(stagedDir) {
|
||||||
|
|
@ -4316,7 +4351,7 @@ ipcMain.handle("find-stop", () => {
|
||||||
ipcMain.handle("app-version", () => app.getVersion());
|
ipcMain.handle("app-version", () => app.getVersion());
|
||||||
// Clean relaunch — used by the Aegis card to apply a staged add-on update
|
// Clean relaunch — used by the Aegis card to apply a staged add-on update
|
||||||
// (promotion happens on next boot; this is just how the user gets there).
|
// (promotion happens on next boot; this is just how the user gets there).
|
||||||
ipcMain.handle("app-restart", () => { try { app.relaunch(); } catch {} app.quit(); });
|
ipcMain.handle("app-restart", (e) => { let from = "?"; try { from = e.sender.getURL(); } catch {} console.log("[restart] requested via app-restart IPC from", from); try { app.relaunch(); } catch {} app.quit(); });
|
||||||
ipcMain.handle("recheck-update", async () => {
|
ipcMain.handle("recheck-update", async () => {
|
||||||
// Manual "Check for updates" also un-dismisses any chip the user closed
|
// Manual "Check for updates" also un-dismisses any chip the user closed
|
||||||
// in this session — they're actively asking to see the status, so honour
|
// in this session — they're actively asking to see the status, so honour
|
||||||
|
|
@ -5897,15 +5932,13 @@ if (!process.env.THESEUS_NO_AUTOSTART) {
|
||||||
// staged/, and promoteStagedUpdates() picks it up on the NEXT initAddons.
|
// staged/, and promoteStagedUpdates() picks it up on the NEXT initAddons.
|
||||||
// Empty PUBKEYS_HEX (the shipping default until an operator ceremonies a
|
// Empty PUBKEYS_HEX (the shipping default until an operator ceremonies a
|
||||||
// key in) short-circuits inside checkAndStageUpdates — no HTTP is made.
|
// key in) short-circuits inside checkAndStageUpdates — no HTTP is made.
|
||||||
setTimeout(() => {
|
// A single boot check missed everything published after launch (2026-09-22:
|
||||||
addonUpdater.checkAndStageUpdates({
|
// Aegis 0.8.3 and VPN 0.1.3 landed on the channel minutes after the app's
|
||||||
addonsDir: addonsUserDir(),
|
// check and never showed up), so re-check every 4 h while running, and
|
||||||
stagedDir: addonsStagedDir(),
|
// tell the chrome whenever something is staged so the user sees a
|
||||||
pubkeysHex: ADDON_UPDATE_PUBKEYS,
|
// "restart to apply" chip instead of finding out in Settings.
|
||||||
verifyPublisher: verifyPublisherEntry,
|
setTimeout(() => pollAddonUpdates("boot"), 30_000);
|
||||||
logger: (...a) => console.log("[addons]", ...a),
|
setInterval(() => pollAddonUpdates("interval"), 4 * 60 * 60 * 1000);
|
||||||
}).catch(() => {});
|
|
||||||
}, 30_000);
|
|
||||||
createWindow();
|
createWindow();
|
||||||
// Multi-source BNS warm-up so the first .bch page opens near-instantly and
|
// Multi-source BNS warm-up so the first .bch page opens near-instantly and
|
||||||
// stays fresh for as long as the browser is running. Every source runs in
|
// stays fresh for as long as the browser is running. Every source runs in
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,13 @@ contextBridge.exposeInMainWorld("theseus", {
|
||||||
// download button opens the URL in the system browser; ✕ dismisses for
|
// download button opens the URL in the system browser; ✕ dismisses for
|
||||||
// the current session.
|
// the current session.
|
||||||
onUpdateAvailable: (cb) => ipcRenderer.on("update-available", (_e, d) => cb(d)),
|
onUpdateAvailable: (cb) => ipcRenderer.on("update-available", (_e, d) => cb(d)),
|
||||||
|
// Signed extension updates staged by the background check: { staged:
|
||||||
|
// [{id,name,version}] }. They apply on the next launch, so the chrome
|
||||||
|
// offers a restart. stagedAddons() asks once at boot.
|
||||||
|
onAddonUpdates: (cb) => ipcRenderer.on("addon-updates", (_e, d) => cb(d)),
|
||||||
|
stagedAddons: () => ipcRenderer.invoke("addons-list-staged"),
|
||||||
|
applyStagedAddons: () => ipcRenderer.invoke("addons-apply-staged"),
|
||||||
|
restartApp: () => ipcRenderer.invoke("app-restart"),
|
||||||
openUpdateDownload: (url) => ipcRenderer.invoke("open-update-download", url),
|
openUpdateDownload: (url) => ipcRenderer.invoke("open-update-download", url),
|
||||||
installUpdateNow: () => ipcRenderer.invoke("install-update-now"),
|
installUpdateNow: () => ipcRenderer.invoke("install-update-now"),
|
||||||
dismissUpdate: () => ipcRenderer.invoke("dismiss-update"),
|
dismissUpdate: () => ipcRenderer.invoke("dismiss-update"),
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ contextBridge.exposeInMainWorld("cfg", {
|
||||||
revealAddon: (folder) => ipcRenderer.invoke("addons-reveal", folder),
|
revealAddon: (folder) => ipcRenderer.invoke("addons-reveal", folder),
|
||||||
// Delete a non-bundled extension's folder (Settings › Extensions › ⋯ › Remove).
|
// Delete a non-bundled extension's folder (Settings › Extensions › ⋯ › Remove).
|
||||||
removeAddon: (id) => ipcRenderer.invoke("addons-remove", id),
|
removeAddon: (id) => ipcRenderer.invoke("addons-remove", id),
|
||||||
|
// Install a staged signed update now (Settings › Extensions › "Update to vX"); no id = every staged extension.
|
||||||
|
applyStagedAddons: (id) => ipcRenderer.invoke("addons-apply-staged", typeof id === "string" ? id : undefined),
|
||||||
openAddonsDir: () => ipcRenderer.invoke("addons-open-dir"),
|
openAddonsDir: () => ipcRenderer.invoke("addons-open-dir"),
|
||||||
reloadAddons: () => ipcRenderer.invoke("addons-reload"),
|
reloadAddons: () => ipcRenderer.invoke("addons-reload"),
|
||||||
// Community extensions from theseus.x/extensions: the catalog (with what
|
// Community extensions from theseus.x/extensions: the catalog (with what
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,8 @@
|
||||||
.xhead{display:flex;align-items:flex-start;gap:12px;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 14px 14px 10px}
|
.xhead{display:flex;align-items:flex-start;gap:12px;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 14px 14px 10px}
|
||||||
.xhead .xi{width:36px;height:36px;flex:none;display:inline-grid;place-items:center;font-size:26px;line-height:1}
|
.xhead .xi{width:36px;height:36px;flex:none;display:inline-grid;place-items:center;font-size:26px;line-height:1}
|
||||||
.xhead .xtitle{flex:1;min-width:0} .xhead .xtitle .t{font-weight:700;font-size:15px} .xhead .xtitle .d{color:var(--mut);font-size:13px;margin-top:3px}
|
.xhead .xtitle{flex:1;min-width:0} .xhead .xtitle .t{font-weight:700;font-size:15px} .xhead .xtitle .d{color:var(--mut);font-size:13px;margin-top:3px}
|
||||||
.xback{width:32px;height:32px;border-radius:8px;border:1px solid var(--line);background:transparent;color:var(--ink);font-size:18px;cursor:pointer;flex:none;margin-top:2px}
|
.xback{height:32px;padding:0 12px 0 8px;border-radius:8px;border:1px solid var(--line);background:transparent;color:var(--ink);font:inherit;font-size:13px;font-weight:600;cursor:pointer;flex:none;margin-top:2px;white-space:nowrap}
|
||||||
|
.xupd{padding:5px 12px;font-size:12.5px;font-weight:600;background:var(--acid);color:#0b0e14;border-color:transparent} .xupd:hover{filter:brightness(1.08);background:var(--acid)}
|
||||||
.xback:hover{background:#ffffff14}
|
.xback:hover{background:#ffffff14}
|
||||||
.xdt{margin-top:10px;background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden}
|
.xdt{margin-top:10px;background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden}
|
||||||
.xdt .xr{display:flex;align-items:center;gap:16px;padding:11px 14px;border-top:1px solid var(--line);font-size:13.5px}
|
.xdt .xr{display:flex;align-items:center;gap:16px;padding:11px 14px;border-top:1px solid var(--line);font-size:13.5px}
|
||||||
|
|
@ -1363,7 +1364,8 @@
|
||||||
};
|
};
|
||||||
function renderAddons(snap) {
|
function renderAddons(snap) {
|
||||||
lastAddonSnap = snap;
|
lastAddonSnap = snap;
|
||||||
const items = (snap && snap.installed) || [];
|
// Plug-ins (Aegis, category "plugin") have their own cards under Plug-ins.
|
||||||
|
const items = ((snap && snap.installed) || []).filter((a) => a.category !== "plugin");
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
addonsList.innerHTML = '<div class="d" style="color:var(--dim)">No extensions installed. Install one from theseus.x/extensions, or drop a folder into the extensions directory.</div>';
|
addonsList.innerHTML = '<div class="d" style="color:var(--dim)">No extensions installed. Install one from theseus.x/extensions, or drop a folder into the extensions directory.</div>';
|
||||||
return;
|
return;
|
||||||
|
|
@ -1373,11 +1375,13 @@
|
||||||
return '<div class="xrow" data-id="" data-folder="' + escapeAttr(a.folder) + '" title="' + escapeAttr(a.error) + '"><span class="xi">⚠</span><span class="xn">Load failed <span class="xv">' + escapeHtml(a.folder) + '</span></span><span class="xs warn">' + escapeHtml(a.error) + '</span><span class="xctl"><button class="btn" data-reveal="' + escapeAttr(a.folder) + '" style="padding:5px 10px;font-size:12px">Show folder</button></span></div>';
|
return '<div class="xrow" data-id="" data-folder="' + escapeAttr(a.folder) + '" title="' + escapeAttr(a.error) + '"><span class="xi">⚠</span><span class="xn">Load failed <span class="xv">' + escapeHtml(a.folder) + '</span></span><span class="xs warn">' + escapeHtml(a.error) + '</span><span class="xctl"><button class="btn" data-reveal="' + escapeAttr(a.folder) + '" style="padding:5px 10px;font-size:12px">Show folder</button></span></div>';
|
||||||
}
|
}
|
||||||
const s = addonShortStatus(a);
|
const s = addonShortStatus(a);
|
||||||
|
const st = addonUpdates.staged[a.id];
|
||||||
return '<div class="xrow" data-id="' + escapeAttr(a.id) + '" tabindex="0" title="' + escapeAttr(a.description || "") + '">'
|
return '<div class="xrow" data-id="' + escapeAttr(a.id) + '" tabindex="0" title="' + escapeAttr(a.description || "") + '">'
|
||||||
+ '<span class="xi">' + addonIconHtml(a.icon) + '</span>'
|
+ '<span class="xi">' + addonIconHtml(a.icon) + '</span>'
|
||||||
+ '<span class="xn">' + escapeHtml(a.name) + (a.bundled ? '<span class="xbadge">BUILT-IN</span>' : '') + '<span class="xv">' + escapeHtml(a.version) + '</span></span>'
|
+ '<span class="xn">' + escapeHtml(a.name) + (a.bundled ? '<span class="xbadge">BUILT-IN</span>' : '') + '<span class="xv">' + escapeHtml(a.version) + '</span></span>'
|
||||||
+ (s ? '<span class="xs ' + s.cls + '">' + escapeHtml(s.text) + '</span>' : '')
|
+ (st ? '' : (s ? '<span class="xs ' + s.cls + '">' + escapeHtml(s.text) + '</span>' : ''))
|
||||||
+ '<span class="xctl"><label class="sw sm" title="' + (a.enabled ? "On" : "Off") + '"><input type="checkbox" data-toggle="' + escapeAttr(a.id) + '" ' + (a.enabled ? "checked" : "") + '><span class="track"><span class="knob"></span></span></label>'
|
+ '<span class="xctl">' + (st ? '<button class="btn xupd" data-apply="' + escapeAttr(a.id) + '" title="Signed update staged — installs now, no restart">Update to ' + escapeHtml(st.version) + '</button>' : '')
|
||||||
|
+ '<label class="sw sm" title="' + (a.enabled ? "On" : "Off") + '"><input type="checkbox" data-toggle="' + escapeAttr(a.id) + '" ' + (a.enabled ? "checked" : "") + '><span class="track"><span class="knob"></span></span></label>'
|
||||||
+ '<button class="xmore" data-more="' + escapeAttr(a.id) + '" title="More options" aria-label="More options">⋯</button></span>'
|
+ '<button class="xmore" data-more="' + escapeAttr(a.id) + '" title="More options" aria-label="More options">⋯</button></span>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
};
|
};
|
||||||
|
|
@ -1404,6 +1408,25 @@
|
||||||
cb.addEventListener("change", async () => { await C.setAddonEnabled(cb.dataset.toggle, cb.checked); loadAddons(); });
|
cb.addEventListener("change", async () => { await C.setAddonEnabled(cb.dataset.toggle, cb.checked); loadAddons(); });
|
||||||
});
|
});
|
||||||
root.querySelectorAll('button[data-reveal]').forEach((btn) => btn.addEventListener("click", (e) => { e.stopPropagation(); C.revealAddon(btn.dataset.reveal); }));
|
root.querySelectorAll('button[data-reveal]').forEach((btn) => btn.addEventListener("click", (e) => { e.stopPropagation(); C.revealAddon(btn.dataset.reveal); }));
|
||||||
|
root.querySelectorAll('button[data-apply]').forEach((btn) => btn.addEventListener("click", async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
btn.disabled = true; btn.textContent = "Installing…";
|
||||||
|
try { await C.applyStagedAddons(btn.dataset.apply); } catch {}
|
||||||
|
await loadAddonUpdates(); loadAddons();
|
||||||
|
}));
|
||||||
|
root.querySelectorAll('button[data-check]').forEach((btn) => btn.addEventListener("click", async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
btn.disabled = true; const orig = btn.textContent; btn.textContent = "Checking…";
|
||||||
|
try {
|
||||||
|
const res = await C.checkAddonUpdates();
|
||||||
|
const rep = (res && res.report || []).find((r) => r.id === btn.dataset.check);
|
||||||
|
addonUpdates.report = addonUpdates.report || {};
|
||||||
|
for (const r of (res && res.report || [])) addonUpdates.report[r.id] = r;
|
||||||
|
void rep;
|
||||||
|
} catch {}
|
||||||
|
await loadAddonUpdates(); loadAddons();
|
||||||
|
btn.disabled = false; btn.textContent = orig;
|
||||||
|
}));
|
||||||
root.querySelectorAll('button[data-more]').forEach((btn) => btn.addEventListener("click", (e) => {
|
root.querySelectorAll('button[data-more]').forEach((btn) => btn.addEventListener("click", (e) => {
|
||||||
e.stopPropagation(); const r = btn.getBoundingClientRect(); openAddonMenu(btn.dataset.more, r.right, r.bottom + 4, true);
|
e.stopPropagation(); const r = btn.getBoundingClientRect(); openAddonMenu(btn.dataset.more, r.right, r.bottom + 4, true);
|
||||||
}));
|
}));
|
||||||
|
|
@ -1463,16 +1486,21 @@
|
||||||
box.hidden = false;
|
box.hidden = false;
|
||||||
const caps = (a.capabilities || []);
|
const caps = (a.capabilities || []);
|
||||||
const kind = a.bundled ? "Built into Theseus — ships with every release; turning it off hides it, a newer Theseus reseeds it" : "Installed extension — remove it from the ⋯ menu";
|
const kind = a.bundled ? "Built into Theseus — ships with every release; turning it off hides it, a newer Theseus reseeds it" : "Installed extension — remove it from the ⋯ menu";
|
||||||
const upd = updateLineFor(a).replace("margin-top:4px", "margin:0") || '<div class="d" style="margin:0;color:var(--dim)">' + (a.bundled ? "Updates arrive with Theseus releases or its signed channel." : "Checked with the rest at Check for updates.") + '</div>';
|
const st = addonUpdates.staged[a.id];
|
||||||
|
const upd = (st
|
||||||
|
? '<div class="d" style="margin:0;color:var(--acid-text)">v' + escapeHtml(st.version) + ' is ready.</div>'
|
||||||
|
: updateLineFor(a).replace("margin-top:4px", "margin:0") || '<div class="d" style="margin:0;color:var(--dim)">' + (a.bundled ? "Checked at start and every few hours; Check now asks the channel right away." : "Checked at start and every few hours; Check now asks the channel right away.") + '</div>')
|
||||||
|
+ (st ? '<button class="btn xupd" data-apply="' + escapeAttr(a.id) + '">Update to ' + escapeHtml(st.version) + '</button>' : '<button class="btn" data-check="' + escapeAttr(a.id) + '">Check now</button>');
|
||||||
box.innerHTML =
|
box.innerHTML =
|
||||||
'<div class="xhead"><button class="xback" id="xback" title="Back to the list" aria-label="Back">‹</button>'
|
'<div class="xhead"><button class="xback" id="xback" title="Back to the list" aria-label="Back">‹ Back</button>'
|
||||||
+ '<span class="xi">' + addonIconHtml(a.icon) + '</span>'
|
+ '<span class="xi">' + addonIconHtml(a.icon) + '</span>'
|
||||||
+ '<div class="xtitle"><div class="t">' + escapeHtml(a.name) + (a.bundled ? '<span class="xbadge">BUILT-IN</span>' : '') + (a.enabled ? '' : '<span class="xbadge off">OFF</span>') + '<span class="xv">' + escapeHtml(a.version) + '</span></div>'
|
+ '<div class="xtitle"><div class="t">' + escapeHtml(a.name) + (a.bundled ? '<span class="xbadge">BUILT-IN</span>' : '') + (a.enabled ? '' : '<span class="xbadge off">OFF</span>') + '<span class="xv">' + escapeHtml(a.version) + '</span></div>'
|
||||||
+ '<div class="d">' + escapeHtml(a.description || "No description.") + '</div></div>'
|
+ '<div class="d">' + escapeHtml(a.description || "No description.") + '</div></div>'
|
||||||
+ '<span class="xctl"><label class="sw sm" title="' + (a.enabled ? "On" : "Off") + '"><input type="checkbox" data-toggle="' + escapeAttr(a.id) + '" ' + (a.enabled ? "checked" : "") + '><span class="track"><span class="knob"></span></span></label>'
|
+ '<span class="xctl"><label class="sw sm" title="' + (a.enabled ? "On" : "Off") + '"><input type="checkbox" data-toggle="' + escapeAttr(a.id) + '" ' + (a.enabled ? "checked" : "") + '><span class="track"><span class="knob"></span></span></label>'
|
||||||
+ '<button class="xmore" data-more="' + escapeAttr(a.id) + '" title="More options" aria-label="More options">⋯</button></span></div>'
|
+ '<button class="xmore" data-more="' + escapeAttr(a.id) + '" title="More options" aria-label="More options">⋯</button>'
|
||||||
|
+ '<button class="xmore" id="xclose" title="Close" aria-label="Close">✕</button></span></div>'
|
||||||
+ '<div class="xdt">'
|
+ '<div class="xdt">'
|
||||||
+ '<div class="xr"><span class="k">Updates</span><span class="v"><span>' + upd + '</span></span></div>'
|
+ '<div class="xr"><span class="k">Updates</span><span class="v">' + upd + '</span></div>'
|
||||||
+ '<div class="xr"><span class="k">Author</span><span class="v">' + escapeHtml(a.author || "—") + '</span></div>'
|
+ '<div class="xr"><span class="k">Author</span><span class="v">' + escapeHtml(a.author || "—") + '</span></div>'
|
||||||
+ '<div class="xr"><span class="k">Version</span><span class="v">' + escapeHtml(a.version) + '</span></div>'
|
+ '<div class="xr"><span class="k">Version</span><span class="v">' + escapeHtml(a.version) + '</span></div>'
|
||||||
+ '<div class="xr"><span class="k">Type</span><span class="v">' + escapeHtml(kind) + '</span></div>'
|
+ '<div class="xr"><span class="k">Type</span><span class="v">' + escapeHtml(kind) + '</span></div>'
|
||||||
|
|
@ -1486,6 +1514,7 @@
|
||||||
+ '<div class="d" style="color:var(--dim);margin-top:10px">Every extension runs inside Theseus with the same access as the browser itself; these entries are the extension points it declared, not a sandbox.</div>'
|
+ '<div class="d" style="color:var(--dim);margin-top:10px">Every extension runs inside Theseus with the same access as the browser itself; these entries are the extension points it declared, not a sandbox.</div>'
|
||||||
+ (a.bundled ? '' : '<div style="margin-top:1.2rem;display:flex;justify-content:flex-end"><button class="btn xdanger" id="xremove">Remove extension…</button></div>');
|
+ (a.bundled ? '' : '<div style="margin-top:1.2rem;display:flex;justify-content:flex-end"><button class="btn xdanger" id="xremove">Remove extension…</button></div>');
|
||||||
document.getElementById("xback").onclick = closeAddonDetail;
|
document.getElementById("xback").onclick = closeAddonDetail;
|
||||||
|
document.getElementById("xclose").onclick = closeAddonDetail;
|
||||||
const rm = document.getElementById("xremove"); if (rm) rm.onclick = () => removeAddon(a);
|
const rm = document.getElementById("xremove"); if (rm) rm.onclick = () => removeAddon(a);
|
||||||
wireAddonControls(box);
|
wireAddonControls(box);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue