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;