Theseus 0.3.68: quick-links strip opens services in a dedicated side panel
Clicking an icon on the left strip now opens the service inside a dedicated 380-px mini-view (quickPanel) next to the strip, Opera-style, instead of a new full-sized tab. Click the active icon again to close the panel; click a different one to switch. If the panel is already pointed at the same host, we skip the loadURL so scroll position, open chat and login state survive a close+reopen round-trip. Icons now paint as real brand SVGs (Messenger, WhatsApp, Telegram, X, YouTube, Spotify) with their official colours, bundled inside quicklinks.html so no external favicon fetch leaks the fact that the strip is loaded. Unknown ids fall back to a letter chip. The strip vertically centres the icons between two flex spacers to match Opera's layout. Defaults ship Messenger, WhatsApp, Telegram, X, YouTube and Spotify. The Settings › General › Quick links section still lists / adds / removes entries and toggles the strip.
This commit is contained in:
parent
08ecd093d2
commit
166e220343
4 changed files with 131 additions and 75 deletions
103
main.js
103
main.js
|
|
@ -503,9 +503,12 @@ const SETTINGS_DEFAULTS = {
|
|||
// service shortcuts; click opens the URL in a new tab. Toggle via settings.
|
||||
quickLinksShow: true,
|
||||
quickLinks: [
|
||||
{ id: "x", url: "https://x.com/", title: "X" },
|
||||
{ id: "telegram", url: "https://web.telegram.org/", title: "Telegram" },
|
||||
{ id: "messenger", url: "https://www.messenger.com/", title: "Messenger" },
|
||||
{ id: "whatsapp", url: "https://web.whatsapp.com/", title: "WhatsApp" },
|
||||
{ id: "telegram", url: "https://web.telegram.org/k/", title: "Telegram" },
|
||||
{ id: "x", url: "https://x.com/", title: "X" },
|
||||
{ id: "youtube", url: "https://www.youtube.com/", title: "YouTube" },
|
||||
{ id: "spotify", url: "https://open.spotify.com/", title: "Spotify" },
|
||||
],
|
||||
// Toolbar bar sizes. The URL bar is flex:1 by default so it eats all
|
||||
// remaining space; `compact`/`medium` cap it so the extension dock has
|
||||
|
|
@ -1922,6 +1925,9 @@ let linkStatusW = 100, linkStatusH = 22;
|
|||
let sidebar, sidebarVisible = false, sidebarActivePanelId = null;
|
||||
let quicklinks; // left-edge quick-links strip (Opera-style)
|
||||
const QUICKLINKS_W = 44; // fixed width in px — just big enough for 32-icon tiles
|
||||
let quickPanel; // the mini dedicated view for the active quick-link
|
||||
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
|
||||
// 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
|
||||
// loadSettings() runs and persists any drag adjustment made by the user.
|
||||
|
|
@ -2736,16 +2742,24 @@ function layout() {
|
|||
quicklinks.setBounds({ x: 0, y: chromeH, width: leftW, height: bodyH });
|
||||
try { quicklinks.setVisible(leftW > 0); } catch {}
|
||||
}
|
||||
// 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.
|
||||
const panelW = (quickPanel && activeQuickLinkId && !fsTab) ? QUICK_PANEL_W : 0;
|
||||
if (quickPanel) {
|
||||
quickPanel.setBounds({ x: leftW, y: chromeH, width: panelW, height: bodyH });
|
||||
try { quickPanel.setVisible(panelW > 0); } catch {}
|
||||
}
|
||||
// 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.
|
||||
const sideW = sidebarVisible && !fsTab ? sidebarW : 0;
|
||||
const tabW = Math.max(0, width - leftW - sideW);
|
||||
for (const t of tabs) t.view.setBounds({ x: leftW, y: chromeH, width: tabW, height: bodyH });
|
||||
if (sidebar) sidebar.setBounds({ x: leftW + tabW, y: chromeH, width: sideW, height: bodyH });
|
||||
const tabX = leftW + panelW;
|
||||
const tabW = Math.max(0, width - tabX - sideW);
|
||||
for (const t of tabs) t.view.setBounds({ x: tabX, y: chromeH, width: tabW, height: bodyH });
|
||||
if (sidebar) sidebar.setBounds({ x: tabX + tabW, y: chromeH, width: sideW, height: bodyH });
|
||||
// Approval overlay sits exactly over the tab area — the page underneath
|
||||
// keeps running; only pointer input is intercepted.
|
||||
if (approvalPop) approvalPop.setBounds({ x: leftW, y: chromeH, width: tabW, height: bodyH });
|
||||
if (jsDialogPop) jsDialogPop.setBounds({ x: leftW, y: chromeH, width: tabW, height: bodyH });
|
||||
if (approvalPop) approvalPop.setBounds({ x: tabX, y: chromeH, width: tabW, height: bodyH });
|
||||
if (jsDialogPop) jsDialogPop.setBounds({ x: tabX, y: chromeH, width: tabW, height: bodyH });
|
||||
positionPopover();
|
||||
positionEnginePicker();
|
||||
positionDownloads();
|
||||
|
|
@ -3788,12 +3802,23 @@ function createWindow() {
|
|||
sidebar.setVisible(false);
|
||||
// Opera-style quick-links strip on the left edge: a thin vertical column
|
||||
// with icons for a few web apps (X, Telegram, WhatsApp by default). Clicking
|
||||
// one opens its URL in a new tab (or focuses an existing tab for that URL).
|
||||
// one opens that service in a dedicated mini-tab (the quickPanel next door)
|
||||
// rather than a full-sized tab.
|
||||
quicklinks = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "quicklinks-preload.js") } });
|
||||
try { quicklinks.setBackgroundColor(nativeTheme.shouldUseDarkColors ? "#0e131c" : "#eceff3"); } catch {}
|
||||
win.contentView.addChildView(quicklinks);
|
||||
styleScrollbars(quicklinks.webContents);
|
||||
quicklinks.webContents.loadFile("quicklinks.html");
|
||||
// Dedicated mini-view that renders the currently-active quick-link in its
|
||||
// own narrow column, Opera-style. Lives permanently in the window; shown /
|
||||
// hidden via activeQuickLinkId. Separate browsing context from any tab,
|
||||
// so a Facebook sidebar visit doesn't share cookies with a Facebook tab
|
||||
// the user opened — matching how Opera's sidebar panels feel.
|
||||
quickPanel = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "home-preload.js") } });
|
||||
try { quickPanel.setBackgroundColor(nativeTheme.shouldUseDarkColors ? "#0b0e14" : "#ffffff"); } catch {}
|
||||
win.contentView.addChildView(quickPanel);
|
||||
styleScrollbars(quickPanel.webContents);
|
||||
quickPanel.setVisible(false);
|
||||
// Add-on approval overlay (approval-modal capability). Transparent view
|
||||
// over the tab area, loaded once, shown per request.
|
||||
approvalPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "approval-preload.js") } });
|
||||
|
|
@ -6282,11 +6307,15 @@ ipcMain.handle("settings-set", (_e, key, val) => {
|
|||
const wc = t && !t.settings && !t.addonId && t.url ? t.view.webContents : null;
|
||||
try { wc && wc.reload(); } catch {}
|
||||
}
|
||||
// Quick-links strip: re-layout + push the new list to the strip's view so
|
||||
// show/hide and link edits take effect without a relaunch.
|
||||
// Quick-links strip: re-layout + push the new state to the strip's view so
|
||||
// show/hide and link edits take effect without a relaunch. If the active
|
||||
// panel's link was removed, close the panel.
|
||||
if (key === "quickLinksShow" || key === "quickLinks") {
|
||||
if (activeQuickLinkId && !(settings.quickLinks || []).some((L) => L.id === activeQuickLinkId)) {
|
||||
activeQuickLinkId = null;
|
||||
}
|
||||
layout();
|
||||
try { quicklinks?.webContents.send("quicklinks-update", settings.quickLinks || []); } catch {}
|
||||
emitQuickLinksState();
|
||||
}
|
||||
// Broadcast to every renderer that watches the live settings — the chrome
|
||||
// (toolbar sizes, URL-bar language chip) and every open settings tab (so
|
||||
|
|
@ -6295,29 +6324,43 @@ ipcMain.handle("settings-set", (_e, key, val) => {
|
|||
broadcastSettings();
|
||||
return settings;
|
||||
});
|
||||
// Quick-links IPC: read the saved list, open one in a new tab (focus an
|
||||
// existing tab for that host if present), or prompt for a URL to add.
|
||||
ipcMain.handle("quicklinks-get", () => settings.quickLinks || []);
|
||||
ipcMain.handle("quicklinks-open", (_e, id) => {
|
||||
// Quick-links IPC. getState: list + whichever one is currently open. open: a
|
||||
// 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.
|
||||
function emitQuickLinksState() {
|
||||
const payload = { links: settings.quickLinks || [], openId: activeQuickLinkId };
|
||||
try { quicklinks?.webContents.send("quicklinks-state", payload); } catch {}
|
||||
}
|
||||
function openQuickPanel(id) {
|
||||
const L = (settings.quickLinks || []).find((x) => x.id === id);
|
||||
if (!L || !L.url) return false;
|
||||
if (!quickPanel || !L || !L.url) return;
|
||||
activeQuickLinkId = id;
|
||||
try {
|
||||
// Focus an existing tab whose current URL is on the same host — avoids
|
||||
// stacking a new tab every click.
|
||||
const u = new URL(L.url);
|
||||
const existing = tabs.find((t) => { try { return t.url && new URL(t.url).host === u.host; } catch { return false; } });
|
||||
if (existing) { setActive(existing.id); return true; }
|
||||
} catch {}
|
||||
createTab(L.url);
|
||||
return true;
|
||||
});
|
||||
ipcMain.handle("quicklinks-add", async () => {
|
||||
// Minimal add flow for now: prompt via a native input dialog isn't ideal
|
||||
// in Electron, so route the user to Settings where they can edit the list
|
||||
// directly. (A proper picker UI can land later.)
|
||||
openSettingsTab("general");
|
||||
const cur = quickPanel.webContents.getURL();
|
||||
// Only re-navigate when the panel isn't already showing this origin —
|
||||
// avoids blowing away the user's state (open chat, scroll position)
|
||||
// when they click the icon to reopen the panel they just closed.
|
||||
const sameHost = (() => { try { return cur && new URL(cur).host === new URL(L.url).host; } catch { return false; } })();
|
||||
if (!sameHost) quickPanel.webContents.loadURL(L.url);
|
||||
} catch (e) { console.warn("[quicklinks] loadURL failed:", e?.message); }
|
||||
layout();
|
||||
emitQuickLinksState();
|
||||
}
|
||||
function closeQuickPanel() {
|
||||
activeQuickLinkId = null;
|
||||
layout();
|
||||
emitQuickLinksState();
|
||||
}
|
||||
ipcMain.handle("quicklinks-get-state", () => ({ links: settings.quickLinks || [], openId: activeQuickLinkId }));
|
||||
ipcMain.handle("quicklinks-open", (_e, id) => {
|
||||
if (!id) return false;
|
||||
if (id === activeQuickLinkId) { closeQuickPanel(); return true; }
|
||||
openQuickPanel(String(id));
|
||||
return true;
|
||||
});
|
||||
ipcMain.handle("quicklinks-close", () => { closeQuickPanel(); return true; });
|
||||
ipcMain.handle("quicklinks-add", () => { openSettingsTab("general"); return true; });
|
||||
function broadcastSettings() {
|
||||
try { chrome?.webContents.send("settings-update", settings); } catch {}
|
||||
for (const t of tabs) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "theseus-navigator",
|
||||
"version": "0.3.67",
|
||||
"version": "0.3.68",
|
||||
"license": "MPL-2.0",
|
||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||
"author": "Silent Mode",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
contextBridge.exposeInMainWorld("quicklinks", {
|
||||
getLinks: () => ipcRenderer.invoke("quicklinks-get"),
|
||||
getState: () => ipcRenderer.invoke("quicklinks-get-state"),
|
||||
open: (id) => ipcRenderer.invoke("quicklinks-open", String(id || "")),
|
||||
close: () => ipcRenderer.invoke("quicklinks-close"),
|
||||
add: () => ipcRenderer.invoke("quicklinks-add"),
|
||||
onLinks: (cb) => ipcRenderer.on("quicklinks-update", (_e, d) => cb(d)),
|
||||
onState: (cb) => ipcRenderer.on("quicklinks-state", (_e, d) => cb(d)),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,78 +5,90 @@
|
|||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg:#0e131c; --ink:#e7eaf1; --mut:#8b98a9; --line2:rgba(255,255,255,.13);
|
||||
--acid:#d6ff3d;
|
||||
--acid:#d6ff3d; --acid-tint: rgb(from var(--acid) r g b / .14);
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root { --bg:#eceff3; --ink:#1a1f28; --mut:#5c6676; --line2:rgba(0,0,0,.10); --acid:#0AC18E; }
|
||||
:root { --bg:#eceff3; --ink:#1a1f28; --mut:#5c6676; --line2:rgba(0,0,0,.10); --acid:#0AC18E; --acid-tint: rgb(from var(--acid) r g b / .14); }
|
||||
}
|
||||
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; overflow: hidden;
|
||||
user-select: none; }
|
||||
/* Icons sit in a vertically-centered column: the two spacers split the
|
||||
remaining space above and below so the stack reads as "quick access"
|
||||
centered in the strip, Opera-style. */
|
||||
.strip { display: flex; flex-direction: column; align-items: center;
|
||||
padding: 10px 0; gap: 4px; height: 100%; box-sizing: border-box;
|
||||
padding: 6px 0; height: 100%; box-sizing: border-box;
|
||||
border-right: 1px solid var(--line2); }
|
||||
.icon { width: 36px; height: 36px; border-radius: 8px; display: grid; place-items: center;
|
||||
cursor: pointer; background: transparent; border: none; padding: 0; color: var(--mut);
|
||||
.icons { display: flex; flex-direction: column; align-items: center; gap: 4px;
|
||||
flex: none; }
|
||||
.spacer { flex: 1 1 auto; min-height: 10px; }
|
||||
.icon { width: 34px; height: 34px; border-radius: 8px; display: grid; place-items: center;
|
||||
cursor: pointer; background: transparent; border: none; padding: 0; color: var(--ink);
|
||||
position: relative; }
|
||||
.icon:hover { background: var(--line2); color: var(--ink); }
|
||||
.icon:hover { background: var(--line2); }
|
||||
.icon:active { transform: scale(0.94); }
|
||||
.icon svg { width: 20px; height: 20px; display: block; }
|
||||
.icon img { width: 22px; height: 22px; display: block; border-radius: 4px; }
|
||||
.icon.on { background: var(--acid-tint); box-shadow: inset 2px 0 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%);
|
||||
background: var(--bg); color: var(--ink); padding: 4px 8px; border-radius: 6px;
|
||||
border: 1px solid var(--line2); white-space: nowrap; font-size: 12px;
|
||||
pointer-events: none; opacity: 0; transition: opacity .1s; z-index: 10; }
|
||||
.icon:hover .tt { opacity: 1; transition-delay: .4s; }
|
||||
.sep { width: 24px; height: 1px; background: var(--line2); margin: 6px 0; flex: none; }
|
||||
.spacer { flex: 1 1 auto; }
|
||||
/* Add button — small plus to add a custom link. Prompts for URL via main. */
|
||||
.add { color: var(--mut); font: 500 20px/1 system-ui, sans-serif; }
|
||||
.add:hover { color: var(--acid); }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="strip" id="strip"></div>
|
||||
<div class="strip">
|
||||
<div class="spacer"></div>
|
||||
<div class="icons" id="icons"></div>
|
||||
<div class="spacer"></div>
|
||||
<button class="icon add" id="addBtn" title="Add link">+<span class="tt">Add link</span></button>
|
||||
</div>
|
||||
<script>
|
||||
const Q = window.quicklinks;
|
||||
const strip = document.getElementById("strip");
|
||||
// The links come from main (saved in settings.quickLinks). Each entry is
|
||||
// { id, url, title, icon } where icon is either a letter, an emoji, or a
|
||||
// data: URL for a bundled brand SVG.
|
||||
function render(links) {
|
||||
strip.innerHTML = "";
|
||||
for (const L of (links || [])) {
|
||||
const icons = document.getElementById("icons");
|
||||
const addBtn = document.getElementById("addBtn");
|
||||
addBtn.onclick = () => Q && Q.add();
|
||||
// Bundled brand marks. Each SVG is tight (20×20 viewBox, no padding), the
|
||||
// colour is baked in so icons read at a glance in both themes. Unknown ids
|
||||
// fall back to a letter chip. All marks are purely to identify services.
|
||||
const BRAND = {
|
||||
x: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2H21.5l-7.63 8.72L22.5 22h-7.09l-5.54-7.26L3.5 22H.25l8.18-9.35L1.5 2h7.26l5.01 6.63L18.244 2zm-2.49 18.3h1.84L7.37 3.6H5.4l10.354 16.7z"/></svg>',
|
||||
twitter: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2H21.5l-7.63 8.72L22.5 22h-7.09l-5.54-7.26L3.5 22H.25l8.18-9.35L1.5 2h7.26l5.01 6.63L18.244 2zm-2.49 18.3h1.84L7.37 3.6H5.4l10.354 16.7z"/></svg>',
|
||||
telegram: '<svg viewBox="0 0 24 24" fill="#229ED9"><path d="M9.78 15.65 9.53 19.1c.37 0 .54-.16.73-.35l1.76-1.65 3.65 2.63c.67.37 1.15.18 1.33-.61l2.42-11.35h.01c.21-.98-.36-1.37-1.01-1.13L3.87 10.84c-.95.38-.94.92-.17 1.16l3.73 1.16L16 7.52c.4-.27.77-.12.47.15l-6.69 7.98z"/></svg>',
|
||||
whatsapp: '<svg viewBox="0 0 24 24" fill="#25D366"><path d="M19.11 4.86A9.78 9.78 0 0 0 12.04 2 9.86 9.86 0 0 0 3.6 16.84L2.05 22l5.3-1.39A9.84 9.84 0 0 0 12.04 22h.01a9.86 9.86 0 0 0 9.85-9.86 9.78 9.78 0 0 0-2.79-7.28zM12.05 20.3h-.01a8.17 8.17 0 0 1-4.16-1.14l-.3-.18-3.14.82.84-3.06-.2-.32a8.2 8.2 0 1 1 15.19-4.3 8.2 8.2 0 0 1-8.22 8.18zm4.49-6.14c-.25-.12-1.46-.72-1.69-.8-.23-.09-.4-.12-.56.12-.17.25-.65.8-.8.97-.14.17-.29.19-.54.06-.25-.12-1.05-.39-2-1.23a7.5 7.5 0 0 1-1.38-1.72c-.14-.25-.02-.38.11-.5.11-.11.25-.29.37-.43.12-.14.17-.25.25-.42.08-.17.04-.31-.02-.43-.06-.12-.56-1.35-.77-1.85-.2-.49-.4-.42-.56-.43l-.47-.01c-.17 0-.43.06-.65.31-.22.25-.86.84-.86 2.05 0 1.21.88 2.38 1 2.55.12.17 1.74 2.65 4.21 3.72.59.25 1.05.4 1.41.52.59.19 1.13.16 1.56.1.47-.07 1.46-.6 1.66-1.18.21-.57.21-1.07.14-1.17-.06-.1-.23-.17-.48-.3z"/></svg>',
|
||||
messenger: '<svg viewBox="0 0 24 24"><defs><linearGradient id="mg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#00B2FF"/><stop offset="1" stop-color="#006AFF"/></linearGradient></defs><path fill="url(#mg)" d="M12 2C6.48 2 2 6.14 2 11.26c0 2.77 1.3 5.24 3.37 6.95V22l3.08-1.69c.82.23 1.69.35 2.55.35 5.52 0 10-4.14 10-9.4C22 6.14 17.52 2 12 2zm1 12.63-2.56-2.72-4.99 2.72 5.49-5.84 2.62 2.72 4.93-2.72-5.49 5.84z"/></svg>',
|
||||
youtube: '<svg viewBox="0 0 24 24" fill="#FF0000"><path d="M23.5 6.5a3 3 0 0 0-2.1-2.12C19.46 4 12 4 12 4s-7.46 0-9.4.38A3 3 0 0 0 .5 6.5 31.3 31.3 0 0 0 .11 12a31.3 31.3 0 0 0 .4 5.5 3 3 0 0 0 2.1 2.12C4.54 20 12 20 12 20s7.46 0 9.4-.38a3 3 0 0 0 2.1-2.12A31.3 31.3 0 0 0 23.9 12a31.3 31.3 0 0 0-.4-5.5zM9.75 15.57V8.43L15.82 12l-6.07 3.57z"/></svg>',
|
||||
spotify: '<svg viewBox="0 0 24 24" fill="#1DB954"><path d="M12 0a12 12 0 1 0 0 24 12 12 0 0 0 0-24zm5.5 17.3a.75.75 0 0 1-1.03.25c-2.82-1.72-6.37-2.11-10.56-1.15a.75.75 0 1 1-.34-1.46c4.58-1.05 8.5-.6 11.68 1.33a.75.75 0 0 1 .25 1.03zm1.47-3.26a.94.94 0 0 1-1.29.3c-3.23-1.98-8.14-2.56-11.95-1.4a.94.94 0 1 1-.55-1.8c4.36-1.33 9.78-.68 13.5 1.6a.94.94 0 0 1 .3 1.3zm.13-3.4C15.26 8.4 8.72 8.2 5.1 9.34a1.12 1.12 0 1 1-.66-2.15c4.16-1.27 11.37-1.02 15.86 1.65a1.12 1.12 0 1 1-1.14 1.93z"/></svg>',
|
||||
discord: '<svg viewBox="0 0 24 24" fill="#5865F2"><path d="M20.3 4.5A17.6 17.6 0 0 0 15.9 3l-.2.4a16.2 16.2 0 0 1 3.7 1.2 20 20 0 0 0-5.6-1 18.4 18.4 0 0 0-5.7 1A16.2 16.2 0 0 1 11.8 3.4L11.6 3a17.6 17.6 0 0 0-4.4 1.5A20.2 20.2 0 0 0 3.2 15.5a16.4 16.4 0 0 0 5 2.5l.6-1a11.3 11.3 0 0 1-2.9-1.4l.5-.3c3 1.5 9.1 1.5 12 0l.5.3c-.9.5-1.9 1-2.9 1.4l.6 1a16.4 16.4 0 0 0 5-2.5A20.2 20.2 0 0 0 20.3 4.5zM9 13c-1 0-1.8-1-1.8-2.2S8 8.6 9 8.6c1 0 1.8 1 1.8 2.2S10 13 9 13zm6 0c-1 0-1.8-1-1.8-2.2S14 8.6 15 8.6c1 0 1.8 1 1.8 2.2S16 13 15 13z"/></svg>',
|
||||
};
|
||||
function brandFor(id) { return BRAND[String(id || "").toLowerCase()] || null; }
|
||||
function render(data) {
|
||||
const links = (data && data.links) || [];
|
||||
const openId = (data && data.openId) || null;
|
||||
icons.innerHTML = "";
|
||||
for (const L of links) {
|
||||
const b = document.createElement("button");
|
||||
b.className = "icon";
|
||||
if (openId && L.id === openId) b.classList.add("on");
|
||||
b.title = L.title || L.url;
|
||||
// Icon: a data URL (img) or a short text badge.
|
||||
if (L.icon && /^(data:|https?:|file:)/.test(L.icon)) {
|
||||
const img = document.createElement("img");
|
||||
img.src = L.icon; img.alt = "";
|
||||
img.onerror = () => { b.textContent = (L.title || "?").slice(0, 1).toUpperCase(); };
|
||||
b.appendChild(img);
|
||||
} else if (L.svg) {
|
||||
b.innerHTML = L.svg;
|
||||
const svg = brandFor(L.id);
|
||||
if (svg) {
|
||||
b.innerHTML = svg;
|
||||
} else {
|
||||
b.classList.add("letter");
|
||||
b.textContent = (L.title || "?").slice(0, 1).toUpperCase();
|
||||
}
|
||||
const tt = document.createElement("span");
|
||||
tt.className = "tt"; tt.textContent = L.title || L.url;
|
||||
b.appendChild(tt);
|
||||
b.onclick = () => Q && Q.open(L.id);
|
||||
strip.appendChild(b);
|
||||
icons.appendChild(b);
|
||||
}
|
||||
// Spacer pushes the + to the bottom.
|
||||
const sp = document.createElement("div"); sp.className = "spacer";
|
||||
strip.appendChild(sp);
|
||||
const add = document.createElement("button");
|
||||
add.className = "icon add"; add.textContent = "+"; add.title = "Add link";
|
||||
const addTt = document.createElement("span"); addTt.className = "tt"; addTt.textContent = "Add link";
|
||||
add.appendChild(addTt);
|
||||
add.onclick = () => Q && Q.add();
|
||||
strip.appendChild(add);
|
||||
}
|
||||
Q && Q.getLinks && Q.getLinks().then(render).catch(() => render([]));
|
||||
Q && Q.onLinks && Q.onLinks(render);
|
||||
Q && Q.getState && Q.getState().then(render).catch(() => render(null));
|
||||
Q && Q.onState && Q.onState(render);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue