feat(theseus/settings): Extensions as compact rows with a detail view
The Extensions list was a stack of tall cards — description, author, capabilities and buttons on every one — so seven add-ons filled the page before the user found the toggle. Rows are now one line each, Firefox-style: icon, name, built-in badge, version, a short update status, the on/off switch and a ⋯ menu, grouped Enabled / Disabled / Failed to load. Clicking a row opens the detail view in place: back arrow, description, update status, author, version, type, folder with Show folder, and a Permissions block that explains each declared capability in plain words. The ⋯ menu (and right-click) offers Turn on/off, Details, Show folder and, for non-bundled extensions, Remove — a new addons-remove IPC that deletes the folder under the extensions directory, refuses bundled add-ons (they would only be reseeded), and clears the dock prefs it left behind.
This commit is contained in:
parent
238c81f341
commit
13d0aa178a
3 changed files with 207 additions and 36 deletions
26
main.js
26
main.js
|
|
@ -3857,6 +3857,32 @@ ipcMain.handle("addons-reveal", (_e, folder) => {
|
|||
ipcMain.handle("addons-open-dir", () => {
|
||||
try { shell.openPath(addonsUserDir()); return true; } catch { return false; }
|
||||
});
|
||||
// Remove an installed (non-bundled) extension: delete its folder under the
|
||||
// extensions directory and drop its prefs. Bundled add-ons are refused — a
|
||||
// deleted folder would just be reseeded on the next launch, so "turn off"
|
||||
// is the honest control for those. The per-extension data store is kept.
|
||||
ipcMain.handle("addons-remove", (_e, id) => {
|
||||
if (!addonHost || typeof id !== "string" || !id) return { ok: false, error: "bad id" };
|
||||
const a = addonHost.snapshot().installed.find((x) => x.id === id);
|
||||
if (!a || !a.folder) return { ok: false, error: "not installed" };
|
||||
const norm = path.normalize(a.folder), base = addonsUserDir();
|
||||
if (!norm.toLowerCase().startsWith(base.toLowerCase() + path.sep)) return { ok: false, error: "not in the extensions directory" };
|
||||
try {
|
||||
const bundledManifest = path.join(bundledAddonsDir(), path.basename(norm), "addon.json");
|
||||
if (fs.existsSync(bundledManifest)) return { ok: false, error: "built into Theseus — turn it off instead" };
|
||||
} catch {}
|
||||
try { fs.rmSync(norm, { recursive: true, force: true }); }
|
||||
catch (e) { return { ok: false, error: e?.message || String(e) }; }
|
||||
settings.disabledAddons = (settings.disabledAddons || []).filter((x) => x !== id);
|
||||
settings.dockOrder = (settings.dockOrder || []).filter((k) => dockAddonId(k) !== id);
|
||||
settings.dockHidden = (settings.dockHidden || []).filter((k) => dockAddonId(k) !== id);
|
||||
saveSettings();
|
||||
addonHost.discoverAndActivate();
|
||||
if (sidebarVisible && sidebarActivePanelId && sidebarActivePanelId.startsWith(id + ":")) { sidebarActivePanelId = null; setSidebar(false); }
|
||||
emitSidebarState();
|
||||
console.log(`[addons] removed ${id} (${norm})`);
|
||||
return { ok: true };
|
||||
});
|
||||
// Manual "Check for updates" from Settings > Extensions. Runs the same
|
||||
// checkAndStageUpdates the boot timer runs; returns a snapshot of the
|
||||
// staged dir so the UI can render "Update to <ver> — restart to apply".
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ contextBridge.exposeInMainWorld("cfg", {
|
|||
listAddons: () => ipcRenderer.invoke("addons-list"),
|
||||
setAddonEnabled: (id, enabled) => ipcRenderer.invoke("addons-set-enabled", id, !!enabled),
|
||||
revealAddon: (folder) => ipcRenderer.invoke("addons-reveal", folder),
|
||||
// Delete a non-bundled extension's folder (Settings › Extensions › ⋯ › Remove).
|
||||
removeAddon: (id) => ipcRenderer.invoke("addons-remove", id),
|
||||
openAddonsDir: () => ipcRenderer.invoke("addons-open-dir"),
|
||||
reloadAddons: () => ipcRenderer.invoke("addons-reload"),
|
||||
// Community extensions from theseus.x/extensions: the catalog (with what
|
||||
|
|
|
|||
215
settings.html
215
settings.html
|
|
@ -131,6 +131,40 @@
|
|||
.track{position:absolute;inset:0;background:#2b3444;border:1px solid var(--line);border-radius:999px;transition:.15s}
|
||||
.knob{position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#8b98a9;transition:.15s}
|
||||
.sw input:checked + .track{background:rgb(from var(--acid) r g b / .25);border-color:color-mix(in srgb, var(--acid) 33%, transparent)}
|
||||
/* Extensions: compact one-line rows (Firefox about:addons style); click a
|
||||
row for the detail view, controls on the right stay clickable in place. */
|
||||
.xgrp{font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);margin:14px 0 6px}
|
||||
.xrow{display:flex;align-items:center;gap:12px;background:var(--panel);border:1px solid var(--line);border-radius:10px;
|
||||
padding:9px 12px;margin-bottom:6px;cursor:pointer;min-height:44px}
|
||||
.xrow:hover{background:#18202c}
|
||||
.xrow:focus-visible{outline:2px solid color-mix(in srgb, var(--acid) 50%, transparent);outline-offset:1px}
|
||||
.xrow .xi{width:22px;height:22px;flex:none;display:inline-grid;place-items:center;font-size:17px;line-height:1}
|
||||
.xrow .xi img,.xhead .xi img{width:100%;height:100%;object-fit:contain;display:block}
|
||||
.xrow .xn{flex:1;min-width:0;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.xrow .xv,.xhead .xv{color:var(--dim);font-weight:400;font-size:12.5px;margin-left:6px}
|
||||
.xrow .xs{color:var(--dim);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:38%}
|
||||
.xrow .xs.warn{color:#f6768a} .xrow .xs.upd{color:var(--acid-text)}
|
||||
.xrow .xctl,.xhead .xctl{display:flex;align-items:center;gap:6px;flex:none}
|
||||
.xbadge{display:inline-block;font-size:10px;letter-spacing:.05em;padding:1px 6px;border-radius:3px;background:rgba(214,255,61,.14);color:var(--acid-text);font-weight:700;margin-left:6px;vertical-align:middle}
|
||||
.xbadge.off{background:rgba(255,255,255,.06);color:var(--dim)}
|
||||
.xmore{width:30px;height:30px;border-radius:7px;border:none;background:transparent;color:var(--mut);font-size:18px;line-height:1;cursor:pointer}
|
||||
.xmore:hover{background:#ffffff14;color:var(--ink)}
|
||||
.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 .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:hover{background:#ffffff14}
|
||||
.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:first-child{border-top:none}
|
||||
.xdt .k{flex:0 0 160px;color:var(--mut)} .xdt .v{flex:1;min-width:0;overflow-wrap:anywhere;color:var(--ink);display:flex;align-items:center;gap:10px;justify-content:space-between}
|
||||
.xdt .v code{word-break:break-all}
|
||||
.xdt .v .btn{padding:5px 10px;font-size:12px;flex:none}
|
||||
.xperm{list-style:none;margin:0;padding:0} .xperm li{padding:9px 14px;border-top:1px solid var(--line);font-size:13.5px;display:flex;gap:10px}
|
||||
.xperm li:first-child{border-top:none} .xperm li code{flex:0 0 150px;align-self:flex-start}
|
||||
.xperm li span{color:var(--mut)}
|
||||
.xdanger{border-color:rgba(246,118,138,.4);color:#f6768a;background:rgba(246,118,138,.08)} .xdanger:hover{background:rgba(246,118,138,.16)}
|
||||
@media (prefers-color-scheme: light){ .xrow:hover{background:#eef2f6} .xmore:hover,.xback:hover{background:rgba(0,0,0,.06)} }
|
||||
.sw input:checked + .track .knob{transform:translateX(20px);background:var(--acid)}
|
||||
.note{color:var(--dim);font-size:12.5px;margin-top:1.4rem;border-top:1px solid var(--line);padding-top:1rem}
|
||||
/* light theme (placed last so these win over the dark base rules) */
|
||||
|
|
@ -597,6 +631,7 @@
|
|||
<code style="background:transparent;border:none;padding:0" id="addonsPathHint">%APPDATA%\Theseus\extensions\</code>. Drop a folder in, restart, it's live.
|
||||
Bundled reference extensions (like the Notepad) are copied there on first run — you can edit or remove them
|
||||
without losing anything the browser needs.</p>
|
||||
<div id="addonsMain">
|
||||
<div class="row" style="justify-content:flex-end;gap:8px">
|
||||
<a class="btn" style="text-decoration:none;display:inline-flex;align-items:center" href="https://theseus.x/extensions/" target="_blank" rel="noopener" title="What ships, current signed versions, and how updates are verified">Extensions ↗</a>
|
||||
<button id="addonsCheckUpdates" class="btn" type="button">Check for updates</button>
|
||||
|
|
@ -612,6 +647,8 @@
|
|||
owner on chain before installing — the relay can't substitute code under a trusted name. Extensions run
|
||||
with full app access — treat installing one like installing an unsigned executable, and only load
|
||||
extensions whose source you trust.</div>
|
||||
</div>
|
||||
<div id="addonDetail" hidden></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1299,54 +1336,160 @@
|
|||
}
|
||||
return '<div class="d" style="' + cls + ';margin-top:4px">' + msg + '</div>';
|
||||
}
|
||||
// ---- Extensions list: compact rows + detail view ----------------------
|
||||
let lastAddonSnap = null; // last snapshot, so the detail view can re-render
|
||||
let addonDetailId = null; // id shown in the detail view, or null (list)
|
||||
const addonIconHtml = (icon) => /^data:image\//i.test(icon || "")
|
||||
? '<img src="' + escapeAttr(icon) + '" alt="">'
|
||||
: escapeHtml(icon || "•");
|
||||
// Short status for the row: staged update or the last check's problem.
|
||||
function addonShortStatus(a) {
|
||||
const st = addonUpdates.staged[a.id]; if (st) return { cls: "upd", text: "↻ v" + st.version + " staged — restart to apply" };
|
||||
const rep = addonUpdates.report[a.id]; if (!rep) return null;
|
||||
if (["fetch-failed", "signature-invalid", "sha256-mismatch", "extract-failed", "manifest-mismatch"].includes(rep.status)) return { cls: "warn", text: "Update check failed" };
|
||||
if (rep.status === "staged" || rep.status === "already-staged") return { cls: "upd", text: "↻ v" + (rep.newVer || "?") + " staged — restart to apply" };
|
||||
return null;
|
||||
}
|
||||
const CAP_TEXT = {
|
||||
"sidebar-panel": "Adds a panel to the sidebar.",
|
||||
"toolbar-menu": "Adds a dropdown menu to its toolbar button.",
|
||||
"context-menu-item": "Adds entries to the page right-click menu.",
|
||||
"open-tab": "Can open its own pages as full tabs.",
|
||||
"page-inject": "Runs code inside the web pages it declares — it can read and change what those pages show.",
|
||||
"capture-tab": "Can take screenshots of the current tab.",
|
||||
"vault-derive": "Derives its own keys from your password vault. It never sees the vault itself.",
|
||||
"approval-modal": "Can ask you to approve an action in a dialog over the page.",
|
||||
"session-proxy": "Can route the browser's traffic through a proxy it controls.",
|
||||
};
|
||||
function renderAddons(snap) {
|
||||
lastAddonSnap = snap;
|
||||
const items = (snap && snap.installed) || [];
|
||||
if (!items.length) {
|
||||
addonsList.innerHTML = '<div class="d" style="color:var(--dim)">No extensions installed. Drop a folder into the extensions directory to install one.</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;
|
||||
}
|
||||
// Split into built-in (ship with Theseus, reseeded on version bump) vs
|
||||
// sideloaded (dropped in by the user). Built-in ones can still be
|
||||
// disabled — the toggle just means "load me at startup" — but they
|
||||
// can't be permanently deleted since reseedBundledAddons runs on the
|
||||
// next launch. Rendering the two groups apart makes that clear.
|
||||
const bundled = items.filter((a) => a.bundled);
|
||||
const sideloaded = items.filter((a) => !a.bundled);
|
||||
const rowFor = (a, isBundled) => {
|
||||
const rowFor = (a) => {
|
||||
if (a.error) {
|
||||
return '<div class="row"><div class="txt"><div class="t">⚠ Load failed <span style="color:var(--dim);font-weight:400">' + escapeHtml(a.folder) + '</span></div><div class="d" style="color:#f6768a">' + escapeHtml(a.error) + '</div></div><div><button class="btn" data-reveal="' + escapeAttr(a.folder) + '">Show folder</button></div></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 caps = (a.capabilities || []).length ? '<span style="color:var(--dim);font-size:11.5px;margin-left:8px">' + a.capabilities.map(escapeHtml).join(", ") + '</span>' : "";
|
||||
const builtInBadge = isBundled
|
||||
? '<span style="display:inline-block;font-size:10px;letter-spacing:.05em;padding:1px 6px;border-radius:3px;background:rgba(214,255,61,.14);color:var(--acid);font-weight:700;margin-left:6px;vertical-align:middle">BUILT-IN</span>'
|
||||
: "";
|
||||
// Manifest icons may be an emoji ("🛡"), a "•" fallback, or a
|
||||
// data: URI to a branded SVG/PNG — render as <img> for URIs,
|
||||
// as escaped text otherwise so the emoji case keeps working.
|
||||
const iconHtml = /^data:image\//i.test(a.icon || "")
|
||||
? '<img src="' + escapeAttr(a.icon) + '" alt="" style="width:16px;height:16px;vertical-align:middle;margin-right:4px">'
|
||||
: escapeHtml(a.icon || "•");
|
||||
return '<div class="row"><div class="txt"><div class="t">' + iconHtml + ' ' + escapeHtml(a.name) + builtInBadge + ' <span style="color:var(--dim);font-weight:400">v' + escapeHtml(a.version) + '</span>' + caps + '</div><div class="d">' + escapeHtml(a.description || "") + (a.author ? ' <span style="color:var(--dim)">— ' + escapeHtml(a.author) + '</span>' : '') + '</div>' + updateLineFor(a) + '</div><div style="display:flex;gap:8px;align-items:center"><button class="btn" data-reveal="' + escapeAttr(a.folder) + '">Show folder</button><label class="sw"><input type="checkbox" data-toggle="' + escapeAttr(a.id) + '" ' + (a.enabled ? "checked" : "") + '><span class="track"><span class="knob"></span></span></label></div></div>';
|
||||
const s = addonShortStatus(a);
|
||||
return '<div class="xrow" data-id="' + escapeAttr(a.id) + '" tabindex="0" title="' + escapeAttr(a.description || "") + '">'
|
||||
+ '<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>'
|
||||
+ (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>'
|
||||
+ '<button class="xmore" data-more="' + escapeAttr(a.id) + '" title="More options" aria-label="More options">⋯</button></span>'
|
||||
+ '</div>';
|
||||
};
|
||||
const sectionHeader = (label, hint) =>
|
||||
'<div class="d" style="color:var(--dim);margin:12px 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.04em">' + escapeHtml(label) + '</div>'
|
||||
+ (hint ? '<div class="d" style="color:var(--dim);margin:-4px 0 8px">' + escapeHtml(hint) + '</div>' : '');
|
||||
const enabled = items.filter((a) => !a.error && a.enabled);
|
||||
const disabled = items.filter((a) => !a.error && !a.enabled);
|
||||
const failed = items.filter((a) => a.error);
|
||||
let html = "";
|
||||
if (bundled.length) html += sectionHeader("Built into Theseus", "Ship with every Theseus install. Disabling one hides its UI but the folder stays; a fresh install of a newer Theseus reseeds it.")
|
||||
+ bundled.map((a) => rowFor(a, true)).join("");
|
||||
if (sideloaded.length) html += sectionHeader("Sideloaded", sideloaded.length && bundled.length ? "Third-party or hand-installed. Remove by deleting the folder." : "")
|
||||
+ sideloaded.map((a) => rowFor(a, false)).join("");
|
||||
if (enabled.length) html += '<div class="xgrp">Enabled</div>' + enabled.map(rowFor).join("");
|
||||
if (disabled.length) html += '<div class="xgrp">Disabled</div>' + disabled.map(rowFor).join("");
|
||||
if (failed.length) html += '<div class="xgrp">Failed to load</div>' + failed.map(rowFor).join("");
|
||||
addonsList.innerHTML = html;
|
||||
addonsList.querySelectorAll('input[data-toggle]').forEach((cb) => {
|
||||
cb.addEventListener("change", async () => {
|
||||
await C.setAddonEnabled(cb.dataset.toggle, cb.checked);
|
||||
loadAddons();
|
||||
});
|
||||
});
|
||||
addonsList.querySelectorAll('button[data-reveal]').forEach((btn) => {
|
||||
btn.addEventListener("click", () => C.revealAddon(btn.dataset.reveal));
|
||||
wireAddonControls(addonsList);
|
||||
addonsList.querySelectorAll('.xrow[data-id]').forEach((row) => {
|
||||
const open = () => { if (row.dataset.id) openAddonDetail(row.dataset.id); };
|
||||
row.addEventListener("click", (e) => { if (e.target.closest(".xctl")) return; open(); });
|
||||
row.addEventListener("keydown", (e) => { if ((e.key === "Enter" || e.key === " ") && !e.target.closest(".xctl")) { e.preventDefault(); open(); } });
|
||||
row.addEventListener("contextmenu", (e) => { e.preventDefault(); if (row.dataset.id) openAddonMenu(row.dataset.id, e.clientX, e.clientY); });
|
||||
});
|
||||
if (addonDetailId) renderAddonDetail();
|
||||
}
|
||||
// Toggle / ⋯ / Show folder — shared by the list rows and the detail header.
|
||||
function wireAddonControls(root) {
|
||||
root.querySelectorAll('input[data-toggle]').forEach((cb) => {
|
||||
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-more]').forEach((btn) => btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation(); const r = btn.getBoundingClientRect(); openAddonMenu(btn.dataset.more, r.right, r.bottom + 4, true);
|
||||
}));
|
||||
}
|
||||
const addonById = (id) => ((lastAddonSnap && lastAddonSnap.installed) || []).find((a) => a.id === id) || null;
|
||||
// Floating menu (same look as the search-engine row menu). alignRight
|
||||
// anchors the menu's right edge at x — for the ⋯ button.
|
||||
let addonMenuEl = null;
|
||||
function closeAddonMenu() { if (addonMenuEl) { addonMenuEl.remove(); addonMenuEl = null; } }
|
||||
function openAddonMenu(id, x, y, alignRight) {
|
||||
closeAddonMenu();
|
||||
const a = addonById(id); if (!a) return;
|
||||
const items = [
|
||||
{ label: a.enabled ? "Turn off" : "Turn on", act: () => C.setAddonEnabled(a.id, !a.enabled).then(loadAddons) },
|
||||
{ label: "Details", act: () => openAddonDetail(a.id) },
|
||||
{ label: "Show folder", act: () => C.revealAddon(a.folder) },
|
||||
];
|
||||
if (!a.bundled) items.push({ label: "Remove…", danger: true, act: () => removeAddon(a) });
|
||||
const m = document.createElement("div");
|
||||
m.className = "ctxmenu";
|
||||
m.innerHTML = items.map((it, i) => '<div class="mi' + (it.danger ? " danger" : "") + '" data-i="' + i + '">' + escapeHtml(it.label) + '</div>').join("");
|
||||
document.body.appendChild(m);
|
||||
const rect = m.getBoundingClientRect();
|
||||
const vw = document.documentElement.clientWidth, vh = document.documentElement.clientHeight;
|
||||
const left = alignRight ? x - rect.width : x;
|
||||
m.style.left = Math.max(6, Math.min(left, vw - rect.width - 6)) + "px";
|
||||
m.style.top = Math.min(y, vh - rect.height - 6) + "px";
|
||||
m.querySelectorAll(".mi").forEach((el) => el.onclick = () => { closeAddonMenu(); items[Number(el.dataset.i)].act(); });
|
||||
addonMenuEl = m;
|
||||
setTimeout(() => {
|
||||
const off = (ev) => { if (!m.contains(ev.target)) { closeAddonMenu(); document.removeEventListener("mousedown", off); document.removeEventListener("keydown", esc); } };
|
||||
const esc = (ev) => { if (ev.key === "Escape") { closeAddonMenu(); document.removeEventListener("mousedown", off); document.removeEventListener("keydown", esc); } };
|
||||
document.addEventListener("mousedown", off);
|
||||
document.addEventListener("keydown", esc);
|
||||
}, 0);
|
||||
}
|
||||
async function removeAddon(a) {
|
||||
if (!confirm('Remove "' + a.name + '"?\n\nIts folder under the extensions directory is deleted. Data it kept in its own store stays until you delete it from the extensions folder.')) return;
|
||||
if (!C.removeAddon) { C.revealAddon(a.folder); return; }
|
||||
const r = await C.removeAddon(a.id);
|
||||
if (!r || !r.ok) alert("Could not remove: " + (r && r.error || "unknown error"));
|
||||
if (addonDetailId === a.id) closeAddonDetail();
|
||||
loadAddons();
|
||||
}
|
||||
// Detail view: replaces the list in place (back arrow returns).
|
||||
function openAddonDetail(id) { addonDetailId = id; renderAddonDetail(); }
|
||||
function closeAddonDetail() {
|
||||
addonDetailId = null;
|
||||
document.getElementById("addonDetail").hidden = true;
|
||||
document.getElementById("addonsMain").hidden = false;
|
||||
}
|
||||
function renderAddonDetail() {
|
||||
const a = addonById(addonDetailId);
|
||||
const box = document.getElementById("addonDetail");
|
||||
if (!a) { closeAddonDetail(); return; }
|
||||
document.getElementById("addonsMain").hidden = true;
|
||||
box.hidden = false;
|
||||
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 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>';
|
||||
box.innerHTML =
|
||||
'<div class="xhead"><button class="xback" id="xback" title="Back to the list" aria-label="Back">‹</button>'
|
||||
+ '<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="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>'
|
||||
+ '<button class="xmore" data-more="' + escapeAttr(a.id) + '" title="More options" aria-label="More options">⋯</button></span></div>'
|
||||
+ '<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">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">Type</span><span class="v">' + escapeHtml(kind) + '</span></div>'
|
||||
+ '<div class="xr"><span class="k">Folder</span><span class="v"><code style="background:transparent;border:none;padding:0;color:var(--mut)">' + escapeHtml(a.folder) + '</code><button class="btn" data-reveal="' + escapeAttr(a.folder) + '">Show folder</button></span></div>'
|
||||
+ '</div>'
|
||||
+ '<h2 class="sub" style="margin-top:1.4rem">Permissions</h2>'
|
||||
+ '<div class="xdt">' + (caps.length
|
||||
? '<ul class="xperm">' + caps.map((c) => '<li><code>' + escapeHtml(c) + '</code><span>' + escapeHtml(CAP_TEXT[c] || "Framework capability.") + '</span></li>').join("") + '</ul>'
|
||||
: '<div class="xr"><span class="v" style="color:var(--dim)">No special capabilities — it only uses the basic add-on API.</span></div>')
|
||||
+ '</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>');
|
||||
document.getElementById("xback").onclick = closeAddonDetail;
|
||||
const rm = document.getElementById("xremove"); if (rm) rm.onclick = () => removeAddon(a);
|
||||
wireAddonControls(box);
|
||||
}
|
||||
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && addonDetailId && !addonMenuEl) closeAddonDetail(); });
|
||||
// Discovery of new community extensions happens at theseus.x/extensions —
|
||||
// Settings only shows what's installed on this machine. The install button
|
||||
// on that page invokes the same "addons-install-community" IPC and Theseus
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue