Theseus: drag sidebar icons into any order
The strip always drew extension panels first and web apps after them, in a fixed order. Web apps and panels now share one order (stripOrder) that the icons can be dragged into; reordering web apps in Settings moves them on the strip too, and a new app or extension goes to the end. Until the strip is rearranged, Proton Mail leads, above the panels.
This commit is contained in:
parent
a0e4641ba2
commit
59d1c5ad56
3 changed files with 79 additions and 8 deletions
40
main.js
40
main.js
|
|
@ -600,6 +600,9 @@ const SETTINGS_DEFAULTS = {
|
|||
// and a click in the page closes the panel; "push" moves the page aside and
|
||||
// the panel stays until it is closed.
|
||||
sidebarPanelMode: "overlay",
|
||||
// The strip's order, web apps ("q:<id>") and extension panels ("p:<panelId>")
|
||||
// mixed, as the user dragged them. Empty: Proton Mail, the panels, the rest.
|
||||
stripOrder: [],
|
||||
leftPanelsHidden: [],
|
||||
sidebarHideOnNewTab: true,
|
||||
sidebarHideOnRightPanel: true,
|
||||
|
|
@ -9156,6 +9159,14 @@ ipcMain.handle("settings-set", (_e, key, val) => {
|
|||
// show/hide and link edits take effect without a relaunch. If the active
|
||||
// panel's link was removed, close the panel.
|
||||
if (key === "sidebarPanelMode") raisePanelGrip();
|
||||
if (key === "quickLinks" && Array.isArray(settings.stripOrder) && settings.stripOrder.length) {
|
||||
const slots = settings.stripOrder.map((k, i) => (k.startsWith("q:") ? i : -1)).filter((i) => i >= 0);
|
||||
const next = settings.stripOrder.slice();
|
||||
const ids = (settings.quickLinks || []).map((L) => "q:" + L.id);
|
||||
slots.forEach((slot, n) => { next[slot] = ids[n]; });
|
||||
settings.stripOrder = next.filter(Boolean);
|
||||
saveSettings();
|
||||
}
|
||||
if (key === "quickLinksShow" || key === "quickLinks" || key === "leftPanelWidths" || key === "sidebarPanelMode" || key === "leftPanelsHidden") {
|
||||
if (activeQuickLinkId && !(settings.quickLinks || []).some((L) => L.id === activeQuickLinkId)) {
|
||||
activeQuickLinkId = null;
|
||||
|
|
@ -9177,8 +9188,37 @@ ipcMain.handle("settings-set", (_e, key, val) => {
|
|||
// toggle — click to open the panel on that link, click the active one again
|
||||
// to close it, click a different one to switch. The strip subscribes via
|
||||
// onState so it can highlight the active icon.
|
||||
// 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 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)) : [];
|
||||
if (saved.length) {
|
||||
for (const k of [...links, ...panels]) if (!saved.includes(k)) saved.push(k);
|
||||
return [...new Set(saved)];
|
||||
}
|
||||
const top = links.includes("q:proton") ? ["q:proton"] : [];
|
||||
return [...top, ...panels, ...links.filter((k) => !top.includes(k))];
|
||||
}
|
||||
ipcMain.handle("quicklinks-reorder", (e, order) => {
|
||||
if (!quicklinks || e.sender !== quicklinks.webContents || !Array.isArray(order)) return false;
|
||||
const current = stripOrder();
|
||||
const want = order.map(String).filter((k, i, a) => current.includes(k) && a.indexOf(k) === i);
|
||||
if (want.length !== current.length) return false;
|
||||
settings.stripOrder = want;
|
||||
// Settings › Sidebar lists the web apps in quickLinks order: keep it the same.
|
||||
const pos = new Map(want.filter((k) => k.startsWith("q:")).map((k, i) => [k.slice(2), i]));
|
||||
settings.quickLinks = (settings.quickLinks || []).slice().sort((a, b) => (pos.get(a.id) ?? 1e9) - (pos.get(b.id) ?? 1e9));
|
||||
saveSettings();
|
||||
emitQuickLinksState();
|
||||
broadcastSettings();
|
||||
return true;
|
||||
});
|
||||
function quickLinksPayload() {
|
||||
return {
|
||||
order: stripOrder(),
|
||||
links: settings.quickLinks || [],
|
||||
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 || "" })),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ contextBridge.exposeInMainWorld("quicklinks", {
|
|||
close: () => ipcRenderer.invoke("quicklinks-close"),
|
||||
// The gear: rect is the button's box in the strip, for placing the menu.
|
||||
menu: (rect) => ipcRenderer.invoke("quicklinks-menu", rect),
|
||||
// The strip's new order after a drag: keys "q:<link id>" / "p:<panelId>".
|
||||
reorder: (order) => ipcRenderer.invoke("quicklinks-reorder", order),
|
||||
add: () => ipcRenderer.invoke("quicklinks-add"),
|
||||
onState: (cb) => ipcRenderer.on("quicklinks-state", (_e, d) => cb(d)),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@
|
|||
.icon:hover { background: var(--line2); }
|
||||
.icon:active { transform: scale(0.94); }
|
||||
.icon.on { background: var(--acid-tint); box-shadow: inset 2px 0 0 var(--acid); }
|
||||
.icon.dragging { opacity: .4; }
|
||||
.icon.dropbefore { box-shadow: inset 0 2px 0 var(--acid); }
|
||||
.icon.dropafter { box-shadow: inset 0 -2px 0 var(--acid); }
|
||||
.icon svg, .icon img { width: 20px; height: 20px; display: block; }
|
||||
.icon.letter { font: 600 13px/1 system-ui, sans-serif; color: var(--mut); }
|
||||
.icon .tt { position: absolute; left: 44px; top: 50%; transform: translateY(-50%);
|
||||
|
|
@ -95,12 +98,39 @@
|
|||
}
|
||||
// Any other site: its own favicon, and the first letter if it has none.
|
||||
function letterIcon(b, L) { b.classList.add("letter"); b.prepend(document.createTextNode((L.title || "?").slice(0, 1).toUpperCase())); }
|
||||
// One order for web apps and extension panels (main keeps it); drag an
|
||||
// icon up or down the strip to move it.
|
||||
let order = [];
|
||||
let dragKey = null;
|
||||
function wireDrag(b, key) {
|
||||
b.draggable = true;
|
||||
b.dataset.key = key;
|
||||
b.addEventListener("dragstart", (e) => { dragKey = key; b.classList.add("dragging"); try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("application/x-theseus-strip", key); } catch {} });
|
||||
b.addEventListener("dragend", () => { dragKey = null; b.classList.remove("dragging"); icons.querySelectorAll(".icon").forEach((x) => x.classList.remove("dropbefore", "dropafter")); });
|
||||
b.addEventListener("dragover", (e) => {
|
||||
if (!dragKey || dragKey === key) return;
|
||||
e.preventDefault(); e.dataTransfer.dropEffect = "move";
|
||||
const r = b.getBoundingClientRect(); const before = (e.clientY - r.top) < r.height / 2;
|
||||
b.classList.toggle("dropbefore", before); b.classList.toggle("dropafter", !before);
|
||||
});
|
||||
b.addEventListener("dragleave", () => b.classList.remove("dropbefore", "dropafter"));
|
||||
b.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
if (!dragKey || dragKey === key) return;
|
||||
const r = b.getBoundingClientRect(); const before = (e.clientY - r.top) < r.height / 2;
|
||||
const next = order.filter((k) => k !== dragKey);
|
||||
next.splice(next.indexOf(key) + (before ? 0 : 1), 0, dragKey);
|
||||
dragKey = null;
|
||||
Q && Q.reorder && Q.reorder(next);
|
||||
});
|
||||
}
|
||||
function render(data) {
|
||||
const links = (data && data.links) || [];
|
||||
const openId = (data && data.openId) || null;
|
||||
icons.innerHTML = "";
|
||||
// Extension panels (registered with side: "left") come first.
|
||||
const panels = (data && data.panels) || [];
|
||||
order = Array.isArray(data && data.order) ? data.order : [...panels.map((P) => P.id), ...links.map((L) => "q:" + L.id)];
|
||||
const made = new Map();
|
||||
for (const P of panels) {
|
||||
const b = document.createElement("button");
|
||||
b.className = "icon";
|
||||
|
|
@ -119,12 +149,8 @@
|
|||
tt.className = "tt"; tt.textContent = P.title;
|
||||
b.appendChild(tt);
|
||||
b.onclick = () => Q && Q.open(P.id);
|
||||
icons.appendChild(b);
|
||||
}
|
||||
if (panels.length && links.length) {
|
||||
const sep = document.createElement("div");
|
||||
sep.className = "sep";
|
||||
icons.appendChild(sep);
|
||||
wireDrag(b, P.id);
|
||||
made.set(P.id, b);
|
||||
}
|
||||
for (const L of links) {
|
||||
const b = document.createElement("button");
|
||||
|
|
@ -148,8 +174,11 @@
|
|||
tt.className = "tt"; tt.textContent = L.title || L.url;
|
||||
b.appendChild(tt);
|
||||
b.onclick = () => Q && Q.open(L.id);
|
||||
icons.appendChild(b);
|
||||
wireDrag(b, "q:" + L.id);
|
||||
made.set("q:" + L.id, b);
|
||||
}
|
||||
for (const k of order) { const b = made.get(k); if (b) { icons.appendChild(b); made.delete(k); } }
|
||||
for (const b of made.values()) icons.appendChild(b);
|
||||
}
|
||||
Q && Q.getState && Q.getState().then(render).catch(() => render(null));
|
||||
Q && Q.onState && Q.onState(render);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue