FaviconsPlox

在可见链接旁显示网站图标。包含菜单选项,如果图标未加载则使用dummyimage。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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();

})();