fix(theseus/settings): real search-engine favicons instead of emoji fallback
DuckDuckGo's icons.duckduckgo.com/ip3/… service was returning 404 for Brave, Bing, Yandex and a few others in the SEARCH_ENGINES catalog — so the settings row would fall through to the hardcoded emoji sym (🦁 lion, 🔍 magnifier, etc.) instead of the real brand mark. Two-part fix: 1) main.js: faviconUrl() switched from DDG's icons.duckduckgo.com to Google's www.google.com/s2/favicons?domain=…&sz=32 as the primary source. Google's service is materially more reliable — returns a real 32×32 PNG for essentially every host. 2) settings.html: the engIcon renderer now stacks a two-source fallback. If Google's PNG fails, retry with DDG's ico URL; if that also fails, THEN drop to the emoji sym. Row is never blank, and real brand favicons win over emoji whenever either service resolves. The <option> in the dropdown still uses emoji because <option> can't render <img> — that's a native <select> limitation, not fixable here.
This commit is contained in:
parent
2e42783dfe
commit
1886b76fb2
2 changed files with 67 additions and 8 deletions
29
main.js
29
main.js
|
|
@ -91,7 +91,13 @@ const SEARCH_ENGINES = {
|
|||
const DEFAULT_ENABLED = ["startpage", "duckduckgo", "google", "brave", "bing"];
|
||||
// DuckDuckGo's icon service reliably returns a favicon for ANY domain from one
|
||||
// privacy-respecting host — far more robust than guessing /favicon.ico per site.
|
||||
const faviconUrl = (domain) => (domain ? `https://icons.duckduckgo.com/ip3/${domain}.ico` : null);
|
||||
// Search-engine favicon source. DuckDuckGo's icons.duckduckgo.com/ip3/…
|
||||
// service was returning inconsistent results (Brave, Bing, Yandex etc.
|
||||
// came back 404 → the settings row fell through to an emoji). Google's
|
||||
// /s2/favicons service is materially more reliable, returns a real 32×32
|
||||
// PNG for essentially every host, and doesn't require login. Kept as a
|
||||
// single point so the fallback source can be swapped again in one place.
|
||||
const faviconUrl = (domain) => (domain ? `https://www.google.com/s2/favicons?domain=${domain}&sz=32` : null);
|
||||
function customFavicon(url) { try { return faviconUrl(new URL(String(url).replace("%s", "x")).hostname); } catch { return null; } }
|
||||
function isEnabled(id) { return (settings.enabledEngines || DEFAULT_ENABLED).includes(id); }
|
||||
// Two-tier state: an engine is INSTALLED if it's in the user's Additional
|
||||
|
|
@ -2630,7 +2636,26 @@ ipcMain.handle("addon-tab-close", (e) => {
|
|||
return true;
|
||||
});
|
||||
// Read-side of Settings' Add-ons tab.
|
||||
ipcMain.handle("addons-list", () => addonHost ? addonHost.snapshot() : { installed: [], sidebarPanels: [] });
|
||||
ipcMain.handle("addons-list", () => {
|
||||
if (!addonHost) return { installed: [], sidebarPanels: [] };
|
||||
const snap = addonHost.snapshot();
|
||||
// Mark bundled add-ons so the extensions UI can render them differently
|
||||
// (Aegis + friends look built-in rather than removable extensions). A
|
||||
// sibling folder in bundled-addons/ with the same manifest id is proof
|
||||
// the addon ships with Theseus; seedBundledAddons keeps them in sync.
|
||||
let bundledIds = new Set();
|
||||
try {
|
||||
for (const e of fs.readdirSync(bundledAddonsDir(), { withFileTypes: true })) {
|
||||
if (!e.isDirectory()) continue;
|
||||
try {
|
||||
const m = JSON.parse(fs.readFileSync(path.join(bundledAddonsDir(), e.name, "addon.json"), "utf8"));
|
||||
if (m && m.id) bundledIds.add(String(m.id));
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
snap.installed = snap.installed.map((a) => ({ ...a, bundled: a.id ? bundledIds.has(a.id) : false }));
|
||||
return snap;
|
||||
});
|
||||
// Toggle an add-on's enabled state. Discovery re-runs so newly-enabled
|
||||
// add-ons activate immediately and newly-disabled ones drop out — no
|
||||
// restart required.
|
||||
|
|
|
|||
|
|
@ -593,9 +593,24 @@
|
|||
// search engines: default picker + custom-engine list + add form
|
||||
const sel = document.getElementById("searchEngine");
|
||||
const esc = (s) => String(s || "").replace(/</g, "<");
|
||||
const engIcon = (e) => e.favicon
|
||||
? `<img class="ei" src="${esc(e.favicon)}" onerror="this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'${e.sym || "🔍"}'}))">`
|
||||
: `<span class="es">${e.sym || "🔍"}</span>`;
|
||||
// Try the primary favicon URL (Google's /s2/favicons — set in main.js);
|
||||
// fall back to DuckDuckGo's icons.duckduckgo.com service if that 404s,
|
||||
// then to the emoji sym as final fallback so the row is never blank.
|
||||
const engIcon = (e) => {
|
||||
let alt = "";
|
||||
try {
|
||||
const u = new URL(e.favicon || "https://x");
|
||||
const domain = u.searchParams.get("domain") || u.hostname;
|
||||
if (domain && domain !== "x") alt = `https://icons.duckduckgo.com/ip3/${domain}.ico`;
|
||||
} catch {}
|
||||
const sym = (e.sym || "🔍").replace(/'/g, "'");
|
||||
const fallbackChain = alt
|
||||
? `this.onerror=null;this.src='${alt}';this.setAttribute('data-tried','1');this.onerror=function(){this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'${sym}'}))};`
|
||||
: `this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'${sym}'}))`;
|
||||
return e.favicon
|
||||
? `<img class="ei" src="${esc(e.favicon)}" onerror="${fallbackChain}">`
|
||||
: `<span class="es">${sym}</span>`;
|
||||
};
|
||||
const ENGINE_KINDS = [
|
||||
{ key: "search", label: "Search engines" },
|
||||
{ key: "llm", label: "AI answer engines" },
|
||||
|
|
@ -1121,13 +1136,32 @@
|
|||
addonsList.innerHTML = '<div class="d" style="color:var(--dim)">No extensions installed. Drop a folder into the extensions directory to install one.</div>';
|
||||
return;
|
||||
}
|
||||
addonsList.innerHTML = items.map((a) => {
|
||||
// Split into built-in (ship with Theseus, reseeded on version bump) vs
|
||||
// sideloaded (dropped in by the user). Built-in ones can still be
|
||||
// disabled — the toggle just means "load me at startup" — but they
|
||||
// can't be permanently deleted since reseedBundledAddons runs on the
|
||||
// next launch. Rendering the two groups apart makes that clear.
|
||||
const bundled = items.filter((a) => a.bundled);
|
||||
const sideloaded = items.filter((a) => !a.bundled);
|
||||
const rowFor = (a, isBundled) => {
|
||||
if (a.error) {
|
||||
return '<div class="row"><div class="txt"><div class="t">⚠ Load failed <span style="color:var(--dim);font-weight:400">' + escapeHtml(a.folder) + '</span></div><div class="d" style="color:#f6768a">' + escapeHtml(a.error) + '</div></div><div><button class="btn" data-reveal="' + escapeAttr(a.folder) + '">Show folder</button></div></div>';
|
||||
}
|
||||
const caps = (a.capabilities || []).length ? '<span style="color:var(--dim);font-size:11.5px;margin-left:8px">' + a.capabilities.map(escapeHtml).join(", ") + '</span>' : "";
|
||||
return '<div class="row"><div class="txt"><div class="t">' + a.icon + ' ' + escapeHtml(a.name) + ' <span style="color:var(--dim);font-weight:400">v' + escapeHtml(a.version) + '</span>' + caps + '</div><div class="d">' + escapeHtml(a.description || "") + (a.author ? ' <span style="color:var(--dim)">— ' + escapeHtml(a.author) + '</span>' : '') + '</div></div><div style="display:flex;gap:8px;align-items:center"><button class="btn" data-reveal="' + escapeAttr(a.folder) + '">Show folder</button><label class="sw"><input type="checkbox" data-toggle="' + escapeAttr(a.id) + '" ' + (a.enabled ? "checked" : "") + '><span class="track"><span class="knob"></span></span></label></div></div>';
|
||||
}).join("");
|
||||
const builtInBadge = isBundled
|
||||
? '<span style="display:inline-block;font-size:10px;letter-spacing:.05em;padding:1px 6px;border-radius:3px;background:rgba(214,255,61,.14);color:var(--acid);font-weight:700;margin-left:6px;vertical-align:middle">BUILT-IN</span>'
|
||||
: "";
|
||||
return '<div class="row"><div class="txt"><div class="t">' + a.icon + ' ' + escapeHtml(a.name) + builtInBadge + ' <span style="color:var(--dim);font-weight:400">v' + escapeHtml(a.version) + '</span>' + caps + '</div><div class="d">' + escapeHtml(a.description || "") + (a.author ? ' <span style="color:var(--dim)">— ' + escapeHtml(a.author) + '</span>' : '') + '</div></div><div style="display:flex;gap:8px;align-items:center"><button class="btn" data-reveal="' + escapeAttr(a.folder) + '">Show folder</button><label class="sw"><input type="checkbox" data-toggle="' + escapeAttr(a.id) + '" ' + (a.enabled ? "checked" : "") + '><span class="track"><span class="knob"></span></span></label></div></div>';
|
||||
};
|
||||
const sectionHeader = (label, hint) =>
|
||||
'<div class="d" style="color:var(--dim);margin:12px 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.04em">' + escapeHtml(label) + '</div>'
|
||||
+ (hint ? '<div class="d" style="color:var(--dim);margin:-4px 0 8px">' + escapeHtml(hint) + '</div>' : '');
|
||||
let html = "";
|
||||
if (bundled.length) html += sectionHeader("Built into Theseus", "Ship with every Theseus install. Disabling one hides its UI but the folder stays; a fresh install of a newer Theseus reseeds it.")
|
||||
+ bundled.map((a) => rowFor(a, true)).join("");
|
||||
if (sideloaded.length) html += sectionHeader("Sideloaded", sideloaded.length && bundled.length ? "Third-party or hand-installed. Remove by deleting the folder." : "")
|
||||
+ sideloaded.map((a) => rowFor(a, false)).join("");
|
||||
addonsList.innerHTML = html;
|
||||
addonsList.querySelectorAll('input[data-toggle]').forEach((cb) => {
|
||||
cb.addEventListener("change", async () => {
|
||||
await C.setAddonEnabled(cb.dataset.toggle, cb.checked);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue