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.
This commit is contained in:
Local Dev 2026-10-08 01:28:31 +02:00
parent d4d297b69e
commit a0e4641ba2
2 changed files with 29 additions and 5 deletions

View file

@ -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:<link id>" /
// "p:<panelId>"), 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); }

View file

@ -78,6 +78,23 @@
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; }
// 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;