FaviconsPlox

Affiche les favicons à côté des liens visibles. Inclut des options de menu et utilise dummyimage comme fallback si le favicon ne charge pas.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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();

})();