Theseus: extension panels on the left edge
Extensions could only put panels in the right sidebar. An add-on can now register a panel with side: "left": its icon joins the quick-links strip (above the web-app links), and it opens in the strip's panel slot in its own view with the sidebar preload, so the add-on bridge, events and the widen/narrow controls work as on the right. A left panel and a quick-link web app never share the slot; reopening keeps the panel's page and state. Left panels drop out of the right sidebar and its dock. With the strip turned off they fall back to the right sidebar so they stay reachable. Pithos is the first: bundled copy rebuilt with side: "left".
This commit is contained in:
parent
a3d7c90b78
commit
7087ab57ca
4 changed files with 135 additions and 20 deletions
|
|
@ -466,13 +466,15 @@ class AddonHost {
|
||||||
// one of the add-on's HTML pages. `page` is a path RELATIVE to the
|
// one of the add-on's HTML pages. `page` is a path RELATIVE to the
|
||||||
// add-on folder. `title` shows in the sidebar tab strip. `icon` is
|
// add-on folder. `title` shows in the sidebar tab strip. `icon` is
|
||||||
// a short emoji/glyph.
|
// a short emoji/glyph.
|
||||||
registerSidebarPanel: ({ id, title, icon = manifest.icon, page }) => {
|
// `side: "left"` puts the panel in the quick-links strip on the left
|
||||||
|
// edge instead of the right sidebar (hosts without left panels ignore it).
|
||||||
|
registerSidebarPanel: ({ id, title, icon = manifest.icon, page, side }) => {
|
||||||
if (!id || !title || !page) throw new Error(`registerSidebarPanel needs {id, title, page}`);
|
if (!id || !title || !page) throw new Error(`registerSidebarPanel needs {id, title, page}`);
|
||||||
const abs = path.join(folder, String(page).replace(/^[\\/]/, ""));
|
const abs = path.join(folder, String(page).replace(/^[\\/]/, ""));
|
||||||
if (!fs.existsSync(abs)) throw new Error(`sidebar panel page not found: ${abs}`);
|
if (!fs.existsSync(abs)) throw new Error(`sidebar panel page not found: ${abs}`);
|
||||||
// Namespaced id so two add-ons can't collide.
|
// Namespaced id so two add-ons can't collide.
|
||||||
const panelId = `${manifest.id}:${id}`;
|
const panelId = `${manifest.id}:${id}`;
|
||||||
active.sidebarPanels.push({ panelId, title, icon, pageFile: abs, addonId: manifest.id });
|
active.sidebarPanels.push({ panelId, title, icon, pageFile: abs, addonId: manifest.id, side: side === "left" ? "left" : "right" });
|
||||||
this.log(`[${manifest.id}] registered sidebar panel: ${panelId}`);
|
this.log(`[${manifest.id}] registered sidebar panel: ${panelId}`);
|
||||||
},
|
},
|
||||||
// Programmatically open the sidebar and switch to one of THIS add-on's
|
// Programmatically open the sidebar and switch to one of THIS add-on's
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,9 @@ module.exports = {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
api.registerSidebarPanel({ id: "main", title: "Pithos", page: "ui/index.html" });
|
// Left edge, beside the quick links. Theseus builds without left panels
|
||||||
|
// ignore `side` and show it in the right sidebar.
|
||||||
|
api.registerSidebarPanel({ id: "main", title: "Pithos", page: "ui/index.html", side: "left" });
|
||||||
|
|
||||||
// The host has no quit hook for add-ons; without this an s3d we started
|
// The host has no quit hook for add-ons; without this an s3d we started
|
||||||
// would outlive Theseus.
|
// would outlive Theseus.
|
||||||
|
|
|
||||||
116
main.js
116
main.js
|
|
@ -2132,6 +2132,14 @@ const QUICKLINKS_W = 44; // fixed width in px — just big enough for 32-i
|
||||||
let quickPanel; // the mini dedicated view for the active quick-link
|
let quickPanel; // the mini dedicated view for the active quick-link
|
||||||
const QUICK_PANEL_W = 380; // Opera's sidebar panel sits around this width
|
const QUICK_PANEL_W = 380; // Opera's sidebar panel sits around this width
|
||||||
let activeQuickLinkId = null; // id of the quick-link whose panel is open, or null
|
let activeQuickLinkId = null; // id of the quick-link whose panel is open, or null
|
||||||
|
// Add-on panels registered with side: "left" open from the same strip, in
|
||||||
|
// their own view (sidebar-preload, so the add-on bridge works) in the slot
|
||||||
|
// the quick-link panel uses; the two are never open together.
|
||||||
|
let leftPanel;
|
||||||
|
let leftPanelId = null; // panelId shown, or null when closed
|
||||||
|
let leftPanelLoadedId = null; // what leftPanel has loaded (kept while closed, so reopening keeps state)
|
||||||
|
let leftPanelMax = false; // widened by the panel (silentmode.sidebar.toggleMax)
|
||||||
|
const LEFT_PANEL_W = 400;
|
||||||
// Sidebar width is user-adjustable via a drag grip on the panel's left edge.
|
// Sidebar width is user-adjustable via a drag grip on the panel's left edge.
|
||||||
// The value below is the default; settings.sidebarWidth overrides it once
|
// The value below is the default; settings.sidebarWidth overrides it once
|
||||||
// loadSettings() runs and persists any drag adjustment made by the user.
|
// loadSettings() runs and persists any drag adjustment made by the user.
|
||||||
|
|
@ -2499,6 +2507,9 @@ function initAddons() {
|
||||||
if (t.addonId !== addonId) continue;
|
if (t.addonId !== addonId) continue;
|
||||||
try { t.view?.webContents?.send("addon-event", msg, payload); } catch {}
|
try { t.view?.webContents?.send("addon-event", msg, payload); } catch {}
|
||||||
}
|
}
|
||||||
|
if (leftPanel && leftPanelLoadedId && leftPanelLoadedId.startsWith(addonId + ":")) {
|
||||||
|
try { leftPanel.webContents.send("addon-event", msg, payload); } catch {}
|
||||||
|
}
|
||||||
if (!sidebar || !sidebarActivePanelId || !sidebarActivePanelId.startsWith(addonId + ":")) return;
|
if (!sidebar || !sidebarActivePanelId || !sidebarActivePanelId.startsWith(addonId + ":")) return;
|
||||||
try { sidebar.webContents.send("addon-event", msg, payload); } catch {}
|
try { sidebar.webContents.send("addon-event", msg, payload); } catch {}
|
||||||
},
|
},
|
||||||
|
|
@ -2947,11 +2958,20 @@ function layout() {
|
||||||
}
|
}
|
||||||
// Quick-link panel: a narrow mini-view just right of the strip, showing the
|
// Quick-link panel: a narrow mini-view just right of the strip, showing the
|
||||||
// active quick-link's web app. Only takes space when something is open.
|
// active quick-link's web app. Only takes space when something is open.
|
||||||
const panelW = (quickPanel && activeQuickLinkId && !fsTab) ? QUICK_PANEL_W : 0;
|
const quickW = (quickPanel && activeQuickLinkId && !fsTab) ? QUICK_PANEL_W : 0;
|
||||||
if (quickPanel) {
|
if (quickPanel) {
|
||||||
quickPanel.setBounds({ x: leftW, y: chromeH, width: panelW, height: bodyH });
|
quickPanel.setBounds({ x: leftW, y: chromeH, width: quickW, height: bodyH });
|
||||||
try { quickPanel.setVisible(panelW > 0); } catch {}
|
try { quickPanel.setVisible(quickW > 0); } catch {}
|
||||||
}
|
}
|
||||||
|
// An add-on's left panel; widened, it leaves the tabs a strip, like the
|
||||||
|
// right sidebar maximized.
|
||||||
|
const addonLeftW = (leftPanel && leftPanelId && !fsTab)
|
||||||
|
? (leftPanelMax ? Math.max(LEFT_PANEL_W, width - leftW - 120) : LEFT_PANEL_W) : 0;
|
||||||
|
if (leftPanel) {
|
||||||
|
leftPanel.setBounds({ x: leftW, y: chromeH, width: addonLeftW, height: bodyH });
|
||||||
|
try { leftPanel.setVisible(addonLeftW > 0); } catch {}
|
||||||
|
}
|
||||||
|
const panelW = quickW || addonLeftW;
|
||||||
// Sidebar (when visible) claims a fixed slice on the right; the tab views
|
// Sidebar (when visible) claims a fixed slice on the right; the tab views
|
||||||
// shrink to fit alongside it. When hidden, tabs get the full width.
|
// shrink to fit alongside it. When hidden, tabs get the full width.
|
||||||
const sideW = sidebarVisible && !fsTab ? sidebarW : 0;
|
const sideW = sidebarVisible && !fsTab ? sidebarW : 0;
|
||||||
|
|
@ -3074,6 +3094,12 @@ function showLinkStatus(url) {
|
||||||
function toggleSidebar() { setSidebar(!sidebarVisible); }
|
function toggleSidebar() { setSidebar(!sidebarVisible); }
|
||||||
// Everything the chrome needs to draw the extension dock + plug-in row:
|
// Everything the chrome needs to draw the extension dock + plug-in row:
|
||||||
// panels, toolbar menus, which panel is open, and the user's dock prefs.
|
// panels, toolbar menus, which panel is open, and the user's dock prefs.
|
||||||
|
// Panels that asked for the left side live in the quick-links strip. With
|
||||||
|
// the strip turned off they fall back to the right sidebar so they stay
|
||||||
|
// reachable.
|
||||||
|
const isLeftPanel = (p) => p.side === "left" && !!settings.quickLinksShow;
|
||||||
|
const rightPanels = () => (addonHost ? addonHost.getSidebarPanels().filter((p) => !isLeftPanel(p)) : []);
|
||||||
|
const leftPanels = () => (addonHost ? addonHost.getSidebarPanels().filter(isLeftPanel) : []);
|
||||||
function sidebarStatePayload() {
|
function sidebarStatePayload() {
|
||||||
// Runs on the pull path too (the chrome asks at boot), not only on pushes —
|
// Runs on the pull path too (the chrome asks at boot), not only on pushes —
|
||||||
// a fresh profile otherwise showed the quiet add-ons until something re-emitted.
|
// a fresh profile otherwise showed the quiet add-ons until something re-emitted.
|
||||||
|
|
@ -3081,7 +3107,7 @@ function sidebarStatePayload() {
|
||||||
return {
|
return {
|
||||||
visible: sidebarVisible,
|
visible: sidebarVisible,
|
||||||
active: sidebarActivePanelId,
|
active: sidebarActivePanelId,
|
||||||
panels: addonHost ? addonHost.getSidebarPanels() : [],
|
panels: rightPanels(),
|
||||||
toolbarMenus: addonHost ? addonHost.getToolbarMenus() : [],
|
toolbarMenus: addonHost ? addonHost.getToolbarMenus() : [],
|
||||||
dock: {
|
dock: {
|
||||||
order: Array.isArray(settings.dockOrder) ? settings.dockOrder : [],
|
order: Array.isArray(settings.dockOrder) ? settings.dockOrder : [],
|
||||||
|
|
@ -3111,7 +3137,8 @@ function emitSidebarState() {
|
||||||
}
|
}
|
||||||
function setSidebar(show, panelId) {
|
function setSidebar(show, panelId) {
|
||||||
if (!sidebar) return;
|
if (!sidebar) return;
|
||||||
const panels = addonHost ? addonHost.getSidebarPanels() : [];
|
if (show && panelId && leftPanels().some((p) => p.panelId === panelId)) { openLeftPanel(panelId); return; }
|
||||||
|
const panels = rightPanels();
|
||||||
if (show && panels.length === 0) {
|
if (show && panels.length === 0) {
|
||||||
// No add-on offers a sidebar panel — silently ignore. Settings surfaces
|
// No add-on offers a sidebar panel — silently ignore. Settings surfaces
|
||||||
// the "install one" path.
|
// the "install one" path.
|
||||||
|
|
@ -4178,6 +4205,16 @@ function createWindow() {
|
||||||
win.contentView.addChildView(quickPanel);
|
win.contentView.addChildView(quickPanel);
|
||||||
styleScrollbars(quickPanel.webContents);
|
styleScrollbars(quickPanel.webContents);
|
||||||
quickPanel.setVisible(false);
|
quickPanel.setVisible(false);
|
||||||
|
// Left add-on panels: same preload and IPC surface as the right sidebar.
|
||||||
|
leftPanel = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "sidebar-preload.js") } });
|
||||||
|
leftPanel.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
if (url && /^(?:https?|bns):/i.test(url)) createTab(url);
|
||||||
|
return { action: "deny" };
|
||||||
|
});
|
||||||
|
try { leftPanel.setBackgroundColor(nativeTheme.shouldUseDarkColors ? "#0b0e14" : "#ffffff"); } catch {}
|
||||||
|
win.contentView.addChildView(leftPanel);
|
||||||
|
styleScrollbars(leftPanel.webContents);
|
||||||
|
leftPanel.setVisible(false);
|
||||||
// Add-on approval overlay (approval-modal capability). Transparent view
|
// Add-on approval overlay (approval-modal capability). Transparent view
|
||||||
// over the tab area, loaded once, shown per request.
|
// over the tab area, loaded once, shown per request.
|
||||||
approvalPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "approval-preload.js") } });
|
approvalPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "approval-preload.js") } });
|
||||||
|
|
@ -4234,7 +4271,7 @@ function createWindow() {
|
||||||
// pumpApproval (and every later dapp request) until a full restart.
|
// pumpApproval (and every later dapp request) until a full restart.
|
||||||
if (approvalCurrent) { const c = approvalCurrent; approvalCurrent = null; try { c.resolve("cancel"); } catch {} }
|
if (approvalCurrent) { const c = approvalCurrent; approvalCurrent = null; try { c.resolve("cancel"); } catch {} }
|
||||||
for (const q of approvalQueue.splice(0)) { try { q.resolve("cancel"); } catch {} }
|
for (const q of approvalQueue.splice(0)) { try { q.resolve("cancel"); } catch {} }
|
||||||
addressPicker = null; pwFillPop = null; linkStatus = null; sidebar = null; approvalPop = null; unlockPop = null; jsDialogPop = null;
|
addressPicker = null; pwFillPop = null; linkStatus = null; sidebar = null; leftPanel = null; approvalPop = null; unlockPop = null; jsDialogPop = null;
|
||||||
linkStatusVisible = false;
|
linkStatusVisible = false;
|
||||||
});
|
});
|
||||||
layout();
|
layout();
|
||||||
|
|
@ -4943,7 +4980,7 @@ ipcMain.handle("sidebar-drag", (_e, deltaPx) => {
|
||||||
return sidebarW;
|
return sidebarW;
|
||||||
});
|
});
|
||||||
ipcMain.handle("sidebar-open", (_e, panelId) => { setSidebar(true, panelId); return sidebarVisible; });
|
ipcMain.handle("sidebar-open", (_e, panelId) => { setSidebar(true, panelId); return sidebarVisible; });
|
||||||
ipcMain.handle("sidebar-close", () => { setSidebar(false); return false; });
|
ipcMain.handle("sidebar-close", (e) => { if (isLeftSender(e)) { closeLeftPanel(); return false; } setSidebar(false); return false; });
|
||||||
// Panel-driven sidebar maximize: fills the window with the sidebar (tab
|
// Panel-driven sidebar maximize: fills the window with the sidebar (tab
|
||||||
// area shrinks to zero-width), remembering the pre-max width so restore
|
// area shrinks to zero-width), remembering the pre-max width so restore
|
||||||
// returns cleanly. Doesn't persist across sessions — a fresh launch
|
// returns cleanly. Doesn't persist across sessions — a fresh launch
|
||||||
|
|
@ -4963,10 +5000,17 @@ function setSidebarMaximized(next) {
|
||||||
try { sidebar?.webContents.send("sidebar-max-change", sidebarMaximized); } catch {}
|
try { sidebar?.webContents.send("sidebar-max-change", sidebarMaximized); } catch {}
|
||||||
return sidebarMaximized;
|
return sidebarMaximized;
|
||||||
}
|
}
|
||||||
ipcMain.handle("sidebar-maximize", () => setSidebarMaximized(true));
|
const isLeftSender = (e) => !!leftPanel && e.sender === leftPanel.webContents;
|
||||||
ipcMain.handle("sidebar-restore", () => setSidebarMaximized(false));
|
function setLeftPanelMax(next) {
|
||||||
ipcMain.handle("sidebar-toggle-max", () => setSidebarMaximized(!sidebarMaximized));
|
leftPanelMax = !!next;
|
||||||
ipcMain.handle("sidebar-is-max", () => sidebarMaximized);
|
layout();
|
||||||
|
try { leftPanel?.webContents.send("sidebar-max-change", leftPanelMax); } catch {}
|
||||||
|
return leftPanelMax;
|
||||||
|
}
|
||||||
|
ipcMain.handle("sidebar-maximize", (e) => isLeftSender(e) ? setLeftPanelMax(true) : setSidebarMaximized(true));
|
||||||
|
ipcMain.handle("sidebar-restore", (e) => isLeftSender(e) ? setLeftPanelMax(false) : setSidebarMaximized(false));
|
||||||
|
ipcMain.handle("sidebar-toggle-max", (e) => isLeftSender(e) ? setLeftPanelMax(!leftPanelMax) : setSidebarMaximized(!sidebarMaximized));
|
||||||
|
ipcMain.handle("sidebar-is-max", (e) => isLeftSender(e) ? leftPanelMax : sidebarMaximized);
|
||||||
ipcMain.handle("sidebar-state", () => sidebarStatePayload());
|
ipcMain.handle("sidebar-state", () => sidebarStatePayload());
|
||||||
// ---- extension dock: reorder by drag, right-click menu --------------------
|
// ---- extension dock: reorder by drag, right-click menu --------------------
|
||||||
// Keys match the chrome's: "p:<panelId>" for a sidebar panel button,
|
// Keys match the chrome's: "p:<panelId>" for a sidebar panel button,
|
||||||
|
|
@ -4976,7 +5020,7 @@ ipcMain.handle("sidebar-state", () => sidebarStatePayload());
|
||||||
function dockKeys() {
|
function dockKeys() {
|
||||||
const keys = [];
|
const keys = [];
|
||||||
if (addonHost) {
|
if (addonHost) {
|
||||||
for (const p of addonHost.getSidebarPanels()) if (!p.plugin) keys.push("p:" + p.panelId);
|
for (const p of rightPanels()) if (!p.plugin) keys.push("p:" + p.panelId);
|
||||||
for (const m of addonHost.getToolbarMenus()) if (!m.plugin) keys.push("m:" + m.addonId);
|
for (const m of addonHost.getToolbarMenus()) if (!m.plugin) keys.push("m:" + m.addonId);
|
||||||
}
|
}
|
||||||
const order = Array.isArray(settings.dockOrder) ? settings.dockOrder : [];
|
const order = Array.isArray(settings.dockOrder) ? settings.dockOrder : [];
|
||||||
|
|
@ -5170,7 +5214,8 @@ function setAddonEnabled(id, enabled) {
|
||||||
if (addonHost) addonHost.discoverAndActivate();
|
if (addonHost) addonHost.discoverAndActivate();
|
||||||
// Sidebar may need to close if its current panel came from an add-on we
|
// Sidebar may need to close if its current panel came from an add-on we
|
||||||
// just disabled.
|
// just disabled.
|
||||||
const panels = addonHost ? addonHost.getSidebarPanels() : [];
|
if (leftPanelId && !leftPanels().some((p) => p.panelId === leftPanelId)) closeLeftPanel();
|
||||||
|
const panels = rightPanels();
|
||||||
if (sidebarVisible && sidebarActivePanelId && !panels.find((p) => p.panelId === sidebarActivePanelId)) {
|
if (sidebarVisible && sidebarActivePanelId && !panels.find((p) => p.panelId === sidebarActivePanelId)) {
|
||||||
sidebarActivePanelId = null;
|
sidebarActivePanelId = null;
|
||||||
setSidebar(false);
|
setSidebar(false);
|
||||||
|
|
@ -7131,8 +7176,10 @@ ipcMain.handle("settings-set", (_e, key, val) => {
|
||||||
if (activeQuickLinkId && !(settings.quickLinks || []).some((L) => L.id === activeQuickLinkId)) {
|
if (activeQuickLinkId && !(settings.quickLinks || []).some((L) => L.id === activeQuickLinkId)) {
|
||||||
activeQuickLinkId = null;
|
activeQuickLinkId = null;
|
||||||
}
|
}
|
||||||
|
if (key === "quickLinksShow" && !settings.quickLinksShow) closeLeftPanel();
|
||||||
layout();
|
layout();
|
||||||
emitQuickLinksState();
|
emitQuickLinksState();
|
||||||
|
emitSidebarState();
|
||||||
}
|
}
|
||||||
// Broadcast to every renderer that watches the live settings — the chrome
|
// Broadcast to every renderer that watches the live settings — the chrome
|
||||||
// (toolbar sizes, URL-bar language chip) and every open settings tab (so
|
// (toolbar sizes, URL-bar language chip) and every open settings tab (so
|
||||||
|
|
@ -7145,13 +7192,43 @@ ipcMain.handle("settings-set", (_e, key, val) => {
|
||||||
// toggle — click to open the panel on that link, click the active one again
|
// 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
|
// to close it, click a different one to switch. The strip subscribes via
|
||||||
// onState so it can highlight the active icon.
|
// onState so it can highlight the active icon.
|
||||||
|
function quickLinksPayload() {
|
||||||
|
return {
|
||||||
|
links: settings.quickLinks || [],
|
||||||
|
openId: leftPanelId ? "p:" + leftPanelId : activeQuickLinkId,
|
||||||
|
panels: leftPanels().map((p) => ({ id: "p:" + p.panelId, title: p.addonName || p.title, icon: p.icon || "" })),
|
||||||
|
};
|
||||||
|
}
|
||||||
function emitQuickLinksState() {
|
function emitQuickLinksState() {
|
||||||
const payload = { links: settings.quickLinks || [], openId: activeQuickLinkId };
|
try { quicklinks?.webContents.send("quicklinks-state", quickLinksPayload()); } catch {}
|
||||||
try { quicklinks?.webContents.send("quicklinks-state", payload); } catch {}
|
}
|
||||||
|
function openLeftPanel(panelId) {
|
||||||
|
const p = leftPanels().find((x) => x.panelId === panelId);
|
||||||
|
if (!leftPanel || !p) return false;
|
||||||
|
activeQuickLinkId = null;
|
||||||
|
leftPanelId = panelId;
|
||||||
|
if (leftPanelLoadedId !== panelId) {
|
||||||
|
leftPanelLoadedId = panelId;
|
||||||
|
leftPanelMax = false;
|
||||||
|
try { leftPanel.webContents.loadFile(p.pageFile); } catch (e) { console.warn("left panel loadFile failed:", e?.message); }
|
||||||
|
}
|
||||||
|
try { win.contentView.removeChildView(leftPanel); win.contentView.addChildView(leftPanel); } catch {}
|
||||||
|
layout();
|
||||||
|
emitQuickLinksState();
|
||||||
|
try { leftPanel.webContents.send("sidebar-visibility", true); } catch {}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function closeLeftPanel() {
|
||||||
|
if (!leftPanelId) return;
|
||||||
|
leftPanelId = null;
|
||||||
|
layout();
|
||||||
|
emitQuickLinksState();
|
||||||
|
try { leftPanel?.webContents.send("sidebar-visibility", false); } catch {}
|
||||||
}
|
}
|
||||||
function openQuickPanel(id) {
|
function openQuickPanel(id) {
|
||||||
const L = (settings.quickLinks || []).find((x) => x.id === id);
|
const L = (settings.quickLinks || []).find((x) => x.id === id);
|
||||||
if (!quickPanel || !L || !L.url) return;
|
if (!quickPanel || !L || !L.url) return;
|
||||||
|
if (leftPanelId) closeLeftPanel();
|
||||||
activeQuickLinkId = id;
|
activeQuickLinkId = id;
|
||||||
try {
|
try {
|
||||||
const cur = quickPanel.webContents.getURL();
|
const cur = quickPanel.webContents.getURL();
|
||||||
|
|
@ -7169,14 +7246,19 @@ function closeQuickPanel() {
|
||||||
layout();
|
layout();
|
||||||
emitQuickLinksState();
|
emitQuickLinksState();
|
||||||
}
|
}
|
||||||
ipcMain.handle("quicklinks-get-state", () => ({ links: settings.quickLinks || [], openId: activeQuickLinkId }));
|
ipcMain.handle("quicklinks-get-state", () => quickLinksPayload());
|
||||||
ipcMain.handle("quicklinks-open", (_e, id) => {
|
ipcMain.handle("quicklinks-open", (_e, id) => {
|
||||||
if (!id) return false;
|
if (!id) return false;
|
||||||
|
if (String(id).startsWith("p:")) {
|
||||||
|
const panelId = String(id).slice(2);
|
||||||
|
if (panelId === leftPanelId) closeLeftPanel(); else openLeftPanel(panelId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (id === activeQuickLinkId) { closeQuickPanel(); return true; }
|
if (id === activeQuickLinkId) { closeQuickPanel(); return true; }
|
||||||
openQuickPanel(String(id));
|
openQuickPanel(String(id));
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
ipcMain.handle("quicklinks-close", () => { closeQuickPanel(); return true; });
|
ipcMain.handle("quicklinks-close", () => { closeQuickPanel(); closeLeftPanel(); return true; });
|
||||||
ipcMain.handle("quicklinks-add", () => { openSettingsTab("general"); return true; });
|
ipcMain.handle("quicklinks-add", () => { openSettingsTab("general"); return true; });
|
||||||
function broadcastSettings() {
|
function broadcastSettings() {
|
||||||
try { chrome?.webContents.send("settings-update", settings); } catch {}
|
try { chrome?.webContents.send("settings-update", settings); } catch {}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@
|
||||||
pointer-events: none; opacity: 0; transition: opacity .1s; z-index: 10; }
|
pointer-events: none; opacity: 0; transition: opacity .1s; z-index: 10; }
|
||||||
.icon:hover .tt { opacity: 1; transition-delay: .4s; }
|
.icon:hover .tt { opacity: 1; transition-delay: .4s; }
|
||||||
.add { color: var(--mut); font: 500 20px/1 system-ui, sans-serif; }
|
.add { color: var(--mut); font: 500 20px/1 system-ui, sans-serif; }
|
||||||
|
.sep { width: 22px; height: 1px; background: var(--line2); margin: 4px 0; }
|
||||||
|
.icon.emoji { font-size: 18px; }
|
||||||
.add:hover { color: var(--acid); }
|
.add:hover { color: var(--acid); }
|
||||||
</style></head>
|
</style></head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -68,6 +70,33 @@
|
||||||
const links = (data && data.links) || [];
|
const links = (data && data.links) || [];
|
||||||
const openId = (data && data.openId) || null;
|
const openId = (data && data.openId) || null;
|
||||||
icons.innerHTML = "";
|
icons.innerHTML = "";
|
||||||
|
// Extension panels (registered with side: "left") come first.
|
||||||
|
const panels = (data && data.panels) || [];
|
||||||
|
for (const P of panels) {
|
||||||
|
const b = document.createElement("button");
|
||||||
|
b.className = "icon";
|
||||||
|
if (openId === P.id) b.classList.add("on");
|
||||||
|
b.title = P.title;
|
||||||
|
if (/^data:image\//.test(P.icon)) {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = P.icon;
|
||||||
|
img.alt = "";
|
||||||
|
b.appendChild(img);
|
||||||
|
} else {
|
||||||
|
b.classList.add(P.icon ? "emoji" : "letter");
|
||||||
|
b.textContent = P.icon || (P.title || "?").slice(0, 1).toUpperCase();
|
||||||
|
}
|
||||||
|
const tt = document.createElement("span");
|
||||||
|
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);
|
||||||
|
}
|
||||||
for (const L of links) {
|
for (const L of links) {
|
||||||
const b = document.createElement("button");
|
const b = document.createElement("button");
|
||||||
b.className = "icon";
|
b.className = "icon";
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue