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;