FaviconsPlox

在可見連結旁顯示網站圖示。包含選單選項,如圖示無法載入則使用dummyimage。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         FaviconsPlox
// @name:es      FaviconsPlox
// @name:en      FaviconsPlox
// @name:fr      FaviconsPlox
// @name:de      FaviconsPlox
// @name:it      FaviconsPlox
// @name:pt      FaviconsPlox
// @name:ru      FaviconsPlox
// @name:zh      FaviconsPlox
// @name:ja      FaviconsPlox
// @name:ko      FaviconsPlox
// @name:zh-TW   FaviconsPlox
// @name:zh-CN   FaviconsPlox
// @namespace    favicons-plox
// @version      0.0.1
// @description  Muestra favicons junto a los enlaces visibles. Incluye opciones de menú y usa dummyimage como fallback si el favicon no carga.
// @description:es Muestra favicons junto a los enlaces visibles. Incluye opciones de menú y usa dummyimage como fallback si el favicon no carga.
// @description:en Shows favicons next to visible links. Includes menu options and uses dummyimage as fallback if the favicon fails to load.
// @description:fr Affiche les favicons à côté des liens visibles. Inclut des options de menu et utilise dummyimage comme fallback si le favicon ne charge pas.
// @description:de Zeigt Favicons neben sichtbaren Links an. Beinhaltet Menüoptionen und nutzt dummyimage als Fallback, wenn das Favicon nicht lädt.
// @description:it Mostra i favicon accanto ai link visibili. Include opzioni di menu e usa dummyimage come fallback se il favicon non viene caricato.
// @description:pt Mostra favicons junto aos links visíveis. Inclui opções de menu e usa dummyimage como fallback se o favicon não carregar.
// @description:ru Показывает фавиконы рядом с видимыми ссылками. Включает опции меню и использует dummyimage как запасной вариант, если фавикон не загружается.
// @description:zh 在可见链接旁显示网站图标。包含菜单选项,如果图标未加载则使用dummyimage。
// @description:ja 可視リンクの横にファビコンを表示。メニューオプションを含み、ファビコンが読み込めない場合はdummyimageを使用。
// @description:ko 보이는 링크 옆에 파비콘 표시. 메뉴 옵션 포함, 파비콘 로드 실패 시 dummyimage 사용.
// @description:zh-TW 在可見連結旁顯示網站圖示。包含選單選項,如圖示無法載入則使用dummyimage。
// @description:zh-CN 在可见链接旁显示网站图标。包含菜单选项,如果图标未加载则使用dummyimage。
// @author       Alplox
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';

  /********************************************
   * ⚙️ OPCIONES Y ESTADO
   ********************************************/
  const SETTINGS = {
    enabled: "enabled",
    externalOnly: "externalOnly"
  };

  const getSetting = (key, def = true) => GM_getValue(key, def);
  const setSetting = (key, val) => GM_setValue(key, val);

  function toggleSetting(key) {
    const newVal = !getSetting(key);
    setSetting(key, newVal);
    refreshMenus();
    removeFavicons();
    if (getSetting(SETTINGS.enabled)) observeLinks();
  }

  /********************************************
   * 🧭 MENÚ DINÁMICO
   ********************************************/
  let menuIds = [];

  function refreshMenus() {
    for (const id of menuIds) {
      try { GM_unregisterMenuCommand(id); } catch {}
    }
    menuIds = [];

    const enabled = getSetting(SETTINGS.enabled);
    const externalOnly = getSetting(SETTINGS.externalOnly);

    menuIds.push(
      GM_registerMenuCommand(
        `${enabled ? "☑️" : "⬜"} Activar script (clic para ${enabled ? "desactivar" : "activar"})`,
        () => toggleSetting(SETTINGS.enabled)
      )
    );

    menuIds.push(
      GM_registerMenuCommand(
        `${externalOnly ? "☑️" : "⬜"} Solo enlaces externos`,
        () => toggleSetting(SETTINGS.externalOnly)
      )
    );
  }

  refreshMenus();

  /********************************************
   * 🧩 FUNCIONALIDAD PRINCIPAL
   ********************************************/

  const DUMMY_FAVICON = "https://dummyimage.com/16x16/888/fff.png&text=?";

  const getFavicon = (domain) => `https://www.google.com/s2/favicons?sz=16&domain=${domain}`;

  function addFavicon(link) {
    if (link.dataset.faviconAdded) return;
    link.dataset.faviconAdded = "true";

    let url;
    try {
      url = new URL(link.href);
    } catch {
      return;
    }

    const img = document.createElement("img");
    img.src = getFavicon(url.hostname);
    img.alt = "favicon";
    img.className = "user-favicon-icon";
    img.style.cssText = `
      width:16px;
      height:16px;
      margin-right:4px;
      vertical-align:middle;
    `;

    // Si el favicon no carga → fallback dummyimage
    img.onerror = () => {
      img.onerror = null; // evitar bucles infinitos
      img.src = DUMMY_FAVICON;
    };

    link.prepend(img);
  }

  function removeFavicons() {
    document.querySelectorAll("img.user-favicon-icon").forEach((img) => img.remove());
    document.querySelectorAll("a[data-favicon-added]").forEach((a) => delete a.dataset.faviconAdded);
    if (observer) observer.disconnect();
  }

  /********************************************
   * 👀 OBSERVADOR DE VISIBILIDAD
   ********************************************/
  let observer;

  function observeLinks() {
    if (!getSetting(SETTINGS.enabled)) return;

    if (observer) observer.disconnect();

    const allLinks = Array.from(document.querySelectorAll('a[href]'));
    const externalOnly = getSetting(SETTINGS.externalOnly);
    const currentHost = location.hostname;

    const links = allLinks.filter(link => {
      if (!/^https?:/i.test(link.href)) return false;
      if (externalOnly) {
        try {
          const url = new URL(link.href);
          return url.hostname !== currentHost;
        } catch { return false; }
      }
      return true;
    });

    if (!links.length) return;

    const options = {
      root: null,
      rootMargin: "100px",
      threshold: 0.1
    };

    observer = new IntersectionObserver((entries) => {
      for (const entry of entries) {
        if (entry.isIntersecting) {
          addFavicon(entry.target);
          observer.unobserve(entry.target);
        }
      }
    }, options);

    links.forEach(link => observer.observe(link));
  }

  /********************************************
   * 🔄 OBSERVADOR DE CAMBIOS DINÁMICOS
   ********************************************/
  const mutationObserver = new MutationObserver(() => observeLinks());
  mutationObserver.observe(document.body, { childList: true, subtree: true });

  // Inicializar si está activo
  if (getSetting(SETTINGS.enabled)) observeLinks();

})();