Theseus: switch sidebar apps and extension panels on and off

Settings › Sidebar had a Hide button for extension panels and only Remove
for web apps. Every row now has an on/off switch: off takes the app off
the strip and closes its panel, but keeps it in the list with its sign-in
and width. Web apps and panels share the store (leftPanelsHidden), and a
link that would open in a switched-off app's panel opens in a tab.
This commit is contained in:
Local Dev 2026-10-08 01:43:03 +02:00
parent 1ad2b5d647
commit f6d71bd48f
2 changed files with 35 additions and 16 deletions

16
main.js
View file

@ -9172,6 +9172,7 @@ ipcMain.handle("settings-set", (_e, key, val) => {
activeQuickLinkId = null;
}
if (leftPanelId && leftPanelHidden(leftPanelId)) closeLeftPanel();
if (activeQuickLinkId && quickLinkHidden(activeQuickLinkId)) closeQuickPanel();
if (key === "quickLinksShow" && !settings.quickLinksShow) closeLeftPanel();
layout();
emitQuickLinksState();
@ -9191,7 +9192,7 @@ ipcMain.handle("settings-set", (_e, key, val) => {
// The strip, top to bottom. A saved order keeps what the user dragged; an
// item it does not know yet (a new web app or extension) goes at the end.
function stripOrder() {
const links = (settings.quickLinks || []).map((L) => "q:" + L.id);
const links = visibleQuickLinks().map((L) => "q:" + L.id);
const panels = leftPanels().filter((p) => !leftPanelHidden(p.panelId)).map((p) => "p:" + p.panelId);
const all = new Set([...links, ...panels]);
const saved = Array.isArray(settings.stripOrder) ? settings.stripOrder.filter((k) => all.has(k)) : [];
@ -9219,7 +9220,7 @@ ipcMain.handle("quicklinks-reorder", (e, order) => {
function quickLinksPayload() {
return {
order: stripOrder(),
links: settings.quickLinks || [],
links: visibleQuickLinks(),
openId: leftPanelId ? "p:" + leftPanelId : activeQuickLinkId,
panels: leftPanels().filter((p) => !leftPanelHidden(p.panelId)).map((p) => ({ id: "p:" + p.panelId, title: p.addonName || p.title, icon: p.icon || "" })),
};
@ -9382,7 +9383,16 @@ function quickLinkFor(url) {
if (!settings.quickLinksShow) return null;
let host;
try { host = new URL(url).host; } catch { return null; }
return (settings.quickLinks || []).find((L) => { try { return new URL(L.url).host === host; } catch { return false; } }) || null;
return visibleQuickLinks().find((L) => { try { return new URL(L.url).host === host; } catch { return false; } }) || null;
}
// A web app switched off in Settings › Sidebar stays in the list (with its
// sign-in and width) but is off the strip. Same store as extension panels:
// leftPanelsHidden, keyed "q:<id>" / "p:<panelId>".
function quickLinkHidden(id) {
return Array.isArray(settings.leftPanelsHidden) && settings.leftPanelsHidden.includes("q:" + id);
}
function visibleQuickLinks() {
return (settings.quickLinks || []).filter((L) => L && !quickLinkHidden(L.id));
}
// at: an address on the app's site to show (a share screen, a chat), even

View file

@ -955,7 +955,7 @@
</div>
</div>
<h2 class="sub">Web apps</h2>
<p class="subd">Sites that open in the panel. A site you installed as an app can be added from Apps.</p>
<p class="subd">Sites that open in the panel. Switch one off to take it off the strip; it stays here, still signed in, for switching back on. A site you installed as an app can be added from Apps.</p>
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
<div id="qlList" style="display:flex;flex-direction:column;gap:6px"></div>
<div style="display:flex;gap:6px;flex-wrap:wrap">
@ -965,7 +965,7 @@
</div>
</div>
<h2 class="sub">Extension panels</h2>
<p class="subd">Extensions that open from the strip. Hide one to take it off the strip; the extension keeps working.</p>
<p class="subd">Extensions that open from the strip. Switch one off to take it off the strip; the extension keeps working.</p>
<div class="row" style="flex-direction:column;align-items:stretch;gap:6px">
<div id="sbPanels" style="display:flex;flex-direction:column;gap:6px"></div>
</div>
@ -1304,6 +1304,16 @@
const esc = (v) => String(v || "").replace(/[&<>"']/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;" })[c]);
// A row's width shares its key with the strip: "q:<id>".
const widthBtnHtml = (w, key) => w ? `<button class="btn ghost" data-width="${esc(key)}" title="Panel width ${w} px">Reset width</button>` : "";
// On/off: whether the app or extension panel is on the strip. Off keeps
// it here, with its sign-in and width, for switching back on.
const onStripHtml = (on, key) => `<label class="sw" title="${on ? "On the strip" : "Off the strip"}"><input type="checkbox" data-onstrip="${esc(key)}"${on ? " checked" : ""}><span class="track"><span class="knob"></span></span></label>`;
function wireOnStrip(root) {
root.querySelectorAll("[data-onstrip]").forEach((i) => i.onchange = async () => {
const h = new Set((await C.get()).leftPanelsHidden || []);
if (i.checked) h.delete(i.dataset.onstrip); else h.add(i.dataset.onstrip);
C.set("leftPanelsHidden", [...h]);
});
}
let cur = s;
function render(links) {
const widths = cur.leftPanelWidths || {};
@ -1322,10 +1332,12 @@
row.draggable = true;
row.dataset.id = L.id;
row.style.cssText = "display:flex;align-items:center;gap:8px;flex-wrap:wrap;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:8px 12px";
row.innerHTML = `<span class="grip" title="Drag to reorder">⠿</span>${iconBox(L)}
<div style="flex:1;min-width:140px"><div style="font-weight:600">${esc(L.title || L.url)}</div><div style="font-size:12px;color:var(--mut);word-break:break-all">${esc(L.url)}${widths[key] ? " · " + Number(widths[key]) + " px" : ""}</div></div>
const on = !(cur.leftPanelsHidden || []).includes(key);
row.innerHTML = `<span class="grip" title="Drag to reorder">⠿</span><span style="display:contents;${on ? "" : "opacity:.5"}">${iconBox(L)}</span>
<div style="flex:1;min-width:140px;${on ? "" : "opacity:.5"}"><div style="font-weight:600">${esc(L.title || L.url)}</div><div style="font-size:12px;color:var(--mut);word-break:break-all">${on ? "" : "Off the strip · "}${esc(L.url)}${widths[key] ? " · " + Number(widths[key]) + " px" : ""}</div></div>
${widthBtnHtml(widths[key], key)}
<button class="btn ghost" data-del="${esc(L.id)}">Remove</button>`;
<button class="btn ghost" data-del="${esc(L.id)}">Remove</button>
${onStripHtml(on, key)}`;
list.appendChild(row);
});
list.querySelectorAll("[data-del]").forEach((b) => b.onclick = async () => {
@ -1351,6 +1363,7 @@
});
});
wireWidth(list);
wireOnStrip(list);
// A site without a bundled icon whose favicon does not load: its letter.
list.querySelectorAll("img[data-fav]").forEach((img) => { img.onerror = () => { const d = img.parentElement; img.remove(); d.textContent = d.dataset.letter || "?"; }; });
}
@ -1373,7 +1386,7 @@
C.set("leftPanelWidths", w);
});
}
window.__sbButtons = { widthBtnHtml };
window.__sbButtons = { widthBtnHtml, onStripHtml, wireOnStrip };
render(s.quickLinks || []);
// Typing a well-known app's name shows the rest of it in grey, and its
// address in the address field; → at the end of a field takes the grey
@ -1463,15 +1476,11 @@
const icon = /^data:image\//.test(p.icon) ? `<img src="${esc(p.icon)}" alt="" style="width:20px;height:20px">` : esc(p.icon || (p.title || "?").slice(0, 1));
return `<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:8px 12px">
<div style="width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,.06);display:grid;place-items:center;flex:none">${icon}</div>
<div style="flex:1;min-width:140px"><div style="font-weight:600">${esc(p.title)}</div><div style="font-size:12px;color:var(--mut)">${p.hidden ? "Not on the strip" : "On the strip"}${widths[p.id] ? " · " + Number(widths[p.id]) + " px" : ""}</div></div>
<div style="flex:1;min-width:140px;${p.hidden ? "opacity:.5" : ""}"><div style="font-weight:600">${esc(p.title)}</div><div style="font-size:12px;color:var(--mut)">${p.hidden ? "Off the strip" : "On the strip"}${widths[p.id] ? " · " + Number(widths[p.id]) + " px" : ""}</div></div>
${B ? B.widthBtnHtml(widths[p.id], p.id) : ""}
<button class="btn ghost" data-show="${esc(p.id)}">${p.hidden ? "Show" : "Hide"}</button></div>`;
${B ? B.onStripHtml(!p.hidden, p.id) : ""}</div>`;
}).join("");
box.querySelectorAll("[data-show]").forEach((b) => b.onclick = async () => {
const h = new Set((await C.get()).leftPanelsHidden || []);
if (h.has(b.dataset.show)) h.delete(b.dataset.show); else h.add(b.dataset.show);
C.set("leftPanelsHidden", [...h]);
});
if (B) B.wireOnStrip(box);
if (window.__sbWireWidth) window.__sbWireWidth(box);
}
render();