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;
|
activeQuickLinkId = null;
|
||||||
}
|
}
|
||||||
if (leftPanelId && leftPanelHidden(leftPanelId)) closeLeftPanel();
|
if (leftPanelId && leftPanelHidden(leftPanelId)) closeLeftPanel();
|
||||||
|
if (activeQuickLinkId && quickLinkHidden(activeQuickLinkId)) closeQuickPanel();
|
||||||
if (key === "quickLinksShow" && !settings.quickLinksShow) closeLeftPanel();
|
if (key === "quickLinksShow" && !settings.quickLinksShow) closeLeftPanel();
|
||||||
layout();
|
layout();
|
||||||
emitQuickLinksState();
|
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
|
// 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.
|
// item it does not know yet (a new web app or extension) goes at the end.
|
||||||
function stripOrder() {
|
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 panels = leftPanels().filter((p) => !leftPanelHidden(p.panelId)).map((p) => "p:" + p.panelId);
|
||||||
const all = new Set([...links, ...panels]);
|
const all = new Set([...links, ...panels]);
|
||||||
const saved = Array.isArray(settings.stripOrder) ? settings.stripOrder.filter((k) => all.has(k)) : [];
|
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() {
|
function quickLinksPayload() {
|
||||||
return {
|
return {
|
||||||
order: stripOrder(),
|
order: stripOrder(),
|
||||||
links: settings.quickLinks || [],
|
links: visibleQuickLinks(),
|
||||||
openId: leftPanelId ? "p:" + leftPanelId : activeQuickLinkId,
|
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 || "" })),
|
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;
|
if (!settings.quickLinksShow) return null;
|
||||||
let host;
|
let host;
|
||||||
try { host = new URL(url).host; } catch { return null; }
|
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
|
// at: an address on the app's site to show (a share screen, a chat), even
|
||||||
|
|
|
||||||
|
|
@ -955,7 +955,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="sub">Web apps</h2>
|
<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 class="row" style="flex-direction:column;align-items:stretch;gap:8px">
|
||||||
<div id="qlList" style="display:flex;flex-direction:column;gap:6px"></div>
|
<div id="qlList" style="display:flex;flex-direction:column;gap:6px"></div>
|
||||||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||||
|
|
@ -965,7 +965,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="sub">Extension panels</h2>
|
<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 class="row" style="flex-direction:column;align-items:stretch;gap:6px">
|
||||||
<div id="sbPanels" style="display:flex;flex-direction:column;gap:6px"></div>
|
<div id="sbPanels" style="display:flex;flex-direction:column;gap:6px"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1304,6 +1304,16 @@
|
||||||
const esc = (v) => String(v || "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">","\"":""","'":"'" })[c]);
|
const esc = (v) => String(v || "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">","\"":""","'":"'" })[c]);
|
||||||
// A row's width shares its key with the strip: "q:<id>".
|
// 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>` : "";
|
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;
|
let cur = s;
|
||||||
function render(links) {
|
function render(links) {
|
||||||
const widths = cur.leftPanelWidths || {};
|
const widths = cur.leftPanelWidths || {};
|
||||||
|
|
@ -1322,10 +1332,12 @@
|
||||||
row.draggable = true;
|
row.draggable = true;
|
||||||
row.dataset.id = L.id;
|
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.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)}
|
const on = !(cur.leftPanelsHidden || []).includes(key);
|
||||||
<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>
|
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)}
|
${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.appendChild(row);
|
||||||
});
|
});
|
||||||
list.querySelectorAll("[data-del]").forEach((b) => b.onclick = async () => {
|
list.querySelectorAll("[data-del]").forEach((b) => b.onclick = async () => {
|
||||||
|
|
@ -1351,6 +1363,7 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
wireWidth(list);
|
wireWidth(list);
|
||||||
|
wireOnStrip(list);
|
||||||
// A site without a bundled icon whose favicon does not load: its letter.
|
// 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 || "?"; }; });
|
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);
|
C.set("leftPanelWidths", w);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
window.__sbButtons = { widthBtnHtml };
|
window.__sbButtons = { widthBtnHtml, onStripHtml, wireOnStrip };
|
||||||
render(s.quickLinks || []);
|
render(s.quickLinks || []);
|
||||||
// Typing a well-known app's name shows the rest of it in grey, and its
|
// 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
|
// 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));
|
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">
|
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="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) : ""}
|
${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("");
|
}).join("");
|
||||||
box.querySelectorAll("[data-show]").forEach((b) => b.onclick = async () => {
|
if (B) B.wireOnStrip(box);
|
||||||
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 (window.__sbWireWidth) window.__sbWireWidth(box);
|
if (window.__sbWireWidth) window.__sbWireWidth(box);
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue