From a0e4641ba299cad351996b036017e9ec7fd340d9 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Thu, 8 Oct 2026 01:28:31 +0200 Subject: [PATCH] Theseus: sidebar links you add get their icon; Proton Mail on top A link added by hand gets an id like "youtube-k3f9", and the strip only looked icons up by id, so YouTube or Discord added in Settings showed a letter. The bundled marks are now also found by the site's address, and any other site shows its own favicon (the letter when it has none). Proton Mail leads the web apps, for new strips and where it is added to an existing one. --- main.js | 5 +++-- quicklinks.html | 29 ++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/main.js b/main.js index 0c3799d5..620af390 100644 --- a/main.js +++ b/main.js @@ -585,11 +585,11 @@ const SETTINGS_DEFAULTS = { // service shortcuts; click opens the URL in a new tab. Toggle via settings. quickLinksShow: true, quickLinks: [ + { id: "proton", url: "https://mail.proton.me/", title: "Proton Mail" }, { id: "telegram", url: "https://web.telegram.org/k/", title: "Telegram" }, { id: "whatsapp", url: "https://web.whatsapp.com/", title: "WhatsApp" }, { id: "x", url: "https://x.com/", title: "X" }, { id: "youtube", url: "https://www.youtube.com/", title: "YouTube" }, - { id: "proton", url: "https://mail.proton.me/", title: "Proton Mail" }, ], // Settings › Sidebar. Per-app panel widths and pins (keys "q:" / // "p:"), extension panels the user took off the strip, and when @@ -679,7 +679,8 @@ function loadSettings() { if (offered.includes(id)) continue; offered.push(id); changed = true; const def = SETTINGS_DEFAULTS.quickLinks.find((L) => L.id === id); - if (have && def && !have.some((L) => L && L.id === id)) have.push({ ...def }); + // Proton Mail goes to the top of the web apps; any later one at the end. + if (have && def && !have.some((L) => L && L.id === id)) { if (id === "proton") have.unshift({ ...def }); else have.push({ ...def }); } } if (changed) { settings.quickLinksOffered = offered; saveSettings(); } } catch (e) { console.warn("quickLinks offer:", e?.message); } diff --git a/quicklinks.html b/quicklinks.html index 5f4eadcd..bad0d11e 100644 --- a/quicklinks.html +++ b/quicklinks.html @@ -78,6 +78,23 @@ discord: '', }; function brandFor(id) { return BRAND[String(id || "").toLowerCase()] || null; } + // A link added by hand gets an id like "youtube-k3f9", so the mark is also + // found by the site's address. + const HOST_BRAND = [ + [/(^|\.)(x|twitter)\.com$/, "x"], [/(^|\.)telegram\.org$/, "telegram"], [/(^|\.)whatsapp\.com$/, "whatsapp"], + [/(^|\.)messenger\.com$/, "messenger"], [/(^|\.)(youtube\.com|youtu\.be)$/, "youtube"], [/(^|\.)spotify\.com$/, "spotify"], + [/(^|\.)discord(app)?\.(com|gg)$/, "discord"], [/(^|\.)(proton\.me|protonmail\.com)$/, "proton"], + ]; + const hostOf = (url) => { try { return new URL(url).hostname.toLowerCase(); } catch { return ""; } }; + function brandForLink(L) { + const byId = brandFor(L.id); + if (byId) return byId; + const h = hostOf(L.url); + for (const [re, key] of HOST_BRAND) if (re.test(h)) return BRAND[key]; + return null; + } + // 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())); } function render(data) { const links = (data && data.links) || []; const openId = (data && data.openId) || null; @@ -114,12 +131,18 @@ b.className = "icon"; if (openId && L.id === openId) b.classList.add("on"); b.title = L.title || L.url; - const svg = brandFor(L.id); + const svg = brandForLink(L); + const host = hostOf(L.url); if (svg) { b.innerHTML = svg; + } else if (host && /^https?:/.test(L.url)) { + const img = document.createElement("img"); + img.alt = ""; + img.onerror = () => { img.remove(); letterIcon(b, L); }; + img.src = new URL("/favicon.ico", L.url).href; + b.appendChild(img); } else { - b.classList.add("letter"); - b.textContent = (L.title || "?").slice(0, 1).toUpperCase(); + letterIcon(b, L); } const tt = document.createElement("span"); tt.className = "tt"; tt.textContent = L.title || L.url;