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:
parent
1ad2b5d647
commit
f6d71bd48f
2 changed files with 35 additions and 16 deletions
16
main.js
16
main.js
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) => ({ "&":"&","<":"<",">":">","\"":""","'":"'" })[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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue