YouTube No Saturated Hover

Removes YouTube's 2025 saturated hover effects.

目前為 2025-11-15 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube No Saturated Hover
// @namespace    https://greasyfork.org/users/1476331-jon78
// @version      1.2
// @description  Removes YouTube's 2025 saturated hover effects.
// @author       jon78
// @license      cc0
// @match        *://*.youtube.com/*
// @icon         https://www.youtube.com/favicon.ico
// @run-at       document-start
// @grant        none
// ==/UserScript==

(() => {
  "use strict";

  const ID = "no-saturated-hover";
  let styleEl = null;
  let dark = undefined;
  let rafPending = false;
  let panelObserver = null;

  /* ---------------------------------------------------------
     Detect YouTube Light/Dark mode
     - Uses attributes/classes first, then falls back to measuring
       --yt-spec-base-background brightness.
     --------------------------------------------------------- */
  const detectDark = () => {
    const html = document.documentElement;

    // Explicit theme attributes / classes
    if (html.hasAttribute("dark") || html.classList.contains("dark-theme")) return true;
    if (html.hasAttribute("light") || html.classList.contains("light-theme")) return false;

    // Fallback: measure --yt-spec-base-background brightness (best-effort)
    try {
      const cs = getComputedStyle(html);
      const bg = (cs && cs.getPropertyValue("--yt-spec-base-background") || "").trim();
      if (bg.startsWith("rgb(") || bg.startsWith("rgba(")) {
        const nums = bg.match(/\d+/g);
        if (nums && nums.length >= 3) {
          const r = Number(nums[0]), g = Number(nums[1]), b = Number(nums[2]);
          // simple luminance-ish average check
          return ((r + g + b) / 3) < 60;
        }
      }
    } catch (e) {
      // ignore and fall through
    }
    return false;
  };

  /* ---------------------------------------------------------
     Core CSS — parameterized by dark (boolean)
     --------------------------------------------------------- */
  const buildCss = d => {
    return (`
html {
  /* Custom theme base variables */
  --ytc-base-background:${d ? "#0f0f0f" : "#fff"};
  --ytc-additive-background:${d ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.05)"};
  --ytc-text-primary:${d ? "#f1f1f1" : "#0f0f0f"};
  --ytc-text-secondary:${d ? "#aaa" : "#606060"};

  /* Mirror YT native variables if missing */
  --yt-spec-base-background:var(--yt-spec-base-background,var(--ytc-base-background));
  --yt-spec-additive-background:var(--yt-spec-additive-background,var(--ytc-additive-background));
  --yt-spec-text-primary:var(--yt-spec-text-primary,var(--ytc-text-primary));
  --yt-spec-text-secondary:var(--yt-spec-text-secondary,var(--ytc-text-secondary));

  /* Playlist panel overrides */
  --yt-active-playlist-panel-background-color: var(--yt-spec-additive-background);
  --yt-lightsource-primary-title-color: var(--ytc-text-primary);
  --yt-lightsource-secondary-title-color: var(--ytc-text-secondary);
  --iron-icon-fill-color: var(--yt-lightsource-primary-title-color);
}

/* Disable saturated hover feedback UI */
.yt-spec-touch-feedback-shape__hover-effect,
.yt-spec-touch-feedback-shape__stroke,
.yt-spec-touch-feedback-shape__fill {
  display: none !important;
  opacity: 0 !important;
  pointer-events: none !important;
}

/* Remove highlight from promoted videos */
ytd-rich-item-renderer.ytd-rich-item-renderer-highlight {
  background: transparent !important;
  box-shadow: none !important;
  --yt-spec-outline: transparent !important;
}

/* Primary title colors */
ytd-rich-grid-renderer #video-title,
.yt-lockup-metadata-view-model__title,
.yt-lockup-metadata-view-model__title a {
  color: var(--yt-spec-text-primary, var(--ytc-text-primary)) !important;
}

/* Metadata / secondary text colors */
.yt-lockup-metadata-view-model__metadata,
.yt-lockup-metadata-view-model__metadata span,
#metadata-line span {
  color: var(--yt-spec-text-secondary, var(--ytc-text-secondary)) !important;
}

/* Watch page text colors */
ytd-watch-metadata #description,
ytd-video-secondary-info-renderer #description,
ytd-watch-info-text,
#metadata.ytd-watch-info-text,
#metadata-line.ytd-video-primary-info-renderer span,
#snippet-text,
#snippet-text *,
#attributed-snippet-text,
#attributed-snippet-text * {
  color: var(--yt-spec-text-primary, var(--ytc-text-primary)) !important;
}

#snippet-text:hover,
#snippet-text *:hover,
#attributed-snippet-text:hover,
#attributed-snippet-text *:hover,
ytd-watch-info-text *:hover {
  color: var(--yt-spec-text-primary, var(--ytc-text-primary)) !important;
  filter: none !important;
  opacity: 1 !important;
}

/* Highlighted links */
.yt-core-attributed-string--highlight-text-decorator a.yt-core-attributed-string__link--call-to-action-color,
.yt-core-attributed-string--link-inherit-color .yt-core-attributed-string--highlight-text-decorator a.yt-core-attributed-string__link--call-to-action-color {
  color: var(--yt-spec-text-primary, var(--ytc-text-primary)) !important;
}

/* CTA links (hashtags, blue links) */
ytd-watch-metadata :not(.yt-core-attributed-string--highlight-text-decorator) > .yt-core-attributed-string__link--call-to-action-color,
#snippet-text :not(.yt-core-attributed-string--highlight-text-decorator) > .yt-core-attributed-string__link--call-to-action-color,
#attributed-snippet-text :not(.yt-core-attributed-string--highlight-text-decorator) > .yt-core-attributed-string__link--call-to-action-color {
  color: var(--yt-spec-call-to-action, #3ea6ff) !important;
}

/* Reset saturation variables on watch page */
ytd-watch-metadata, .ytd-watch-metadata {
  --yt-saturated-base-background: var(--ytc-base-background);
  --yt-saturated-raised-background: var(--yt-spec-additive-background,var(--ytc-additive-background));
  --yt-saturated-additive-background: var(--yt-spec-additive-background,var(--ytc-additive-background));
  --yt-saturated-text-primary: var(--yt-spec-text-primary,var(--ytc-text-primary));
  --yt-saturated-text-secondary: var(--yt-spec-text-secondary,var(--ytc-text-secondary));
  --yt-saturated-overlay-background: var(--yt-spec-additive-background,var(--ytc-additive-background));
  --yt-spec-overlay-background: var(--yt-spec-additive-background,var(--ytc-additive-background));
  --yt-spec-static-overlay-background-light: var(--yt-spec-additive-background,var(--ytc-additive-background));

  /* Playlist overrides here too */
  --yt-active-playlist-panel-background-color: var(--yt-spec-additive-background);
  --yt-lightsource-primary-title-color: var(--ytc-text-primary);
  --yt-lightsource-secondary-title-color: var(--ytc-text-secondary);
  --iron-icon-fill-color: var(--yt-lightsource-primary-title-color);
}

/* Highlighted text background cleanup */
.yt-core-attributed-string--highlight-text-decorator {
  background-color: var(--yt-spec-static-overlay-background-light, ${d ? "rgba(255,255,255,0.102)" : "rgba(0,0,0,0.051)"}) !important;
  border-radius: 8px !important;
  padding-bottom: 1px !important;
}
`).trim();
  };

  // Precompute both CSS variants so we avoid rebuilding strings repeatedly
  const CSS_CACHE = {
    dark: buildCss(true),
    light: buildCss(false)
  };

  /* ---------------------------------------------------------
     Insert or update the style element
     - Only replace when theme (dark/light) actually changes
     --------------------------------------------------------- */
  const applyStyle = isDark => {
    if (!styleEl) {
      styleEl = document.getElementById(ID);
      if (!styleEl) {
        styleEl = document.createElement("style");
        styleEl.id = ID;
        // add as late as possible but safe: if head not ready, append to documentElement
        const target = document.head || document.documentElement;
        target.appendChild(styleEl);
      }
    }
    // choose cached css
    const newCss = isDark ? CSS_CACHE.dark : CSS_CACHE.light;
    if (styleEl.textContent !== newCss) {
      styleEl.textContent = newCss;
    }
  };

  /* ---------------------------------------------------------
     Playlist panel override: apply variables directly to panel(s)
     - fast and localized; does not clobber global styles
     --------------------------------------------------------- */
  const updatePlaylistColors = () => {
    document.querySelectorAll("ytd-playlist-panel-renderer").forEach(panel => {
      try {
        panel.style.setProperty("--yt-active-playlist-panel-background-color", "var(--yt-spec-additive-background)");
        panel.style.setProperty("--yt-lightsource-primary-title-color", "var(--ytc-text-primary)");
        panel.style.setProperty("--yt-lightsource-secondary-title-color", "var(--ytc-text-secondary)");
        panel.style.setProperty("--iron-icon-fill-color", "var(--yt-lightsource-primary-title-color)");
      } catch (e) {
        // defensive: some nodes may be in closed shadow or removed quickly
      }
    });
  };

  const scheduleUpdate = () => {
    if (rafPending) return;
    rafPending = true;
    requestAnimationFrame(() => {
      rafPending = false;
      updatePlaylistColors();
    });
  };

  const waitForPlaylistPanelAndObserve = () => {
    const tryFind = () => {
      const panel = document.querySelector("ytd-playlist-panel-renderer");
      if (panel) {
        // initial update
        scheduleUpdate();

        // If there's already an observer attached, disconnect first
        if (panelObserver) {
          try { panelObserver.disconnect(); } catch (e) {}
          panelObserver = null;
        }

        // Observe only the playlist panel for child changes (efficient)
        panelObserver = new MutationObserver(scheduleUpdate);
        try {
          panelObserver.observe(panel, { childList: true, subtree: true });
        } catch (e) {
          // if observe fails (rare), fall back to a single scheduled update
          scheduleUpdate();
        }
      } else {
        // not found yet — try again in next frame (cheap)
        requestAnimationFrame(tryFind);
      }
    };
    tryFind();
  };

  /* ---------------------------------------------------------
     Refresh: apply styles if theme changed and ensure playlist observer
     --------------------------------------------------------- */
  const refresh = () => {
    const isDark = detectDark();
    if (isDark !== dark) {
      dark = isDark;
      applyStyle(dark);
    }
    // ensure playlist panel observation/updates are active
    waitForPlaylistPanelAndObserve();
    // also schedule an immediate update
    scheduleUpdate();
  };

  /* ---------------------------------------------------------
     Init: run at DOM ready / document.head present
     --------------------------------------------------------- */
  const init = () => {
    dark = detectDark();
    applyStyle(dark);
    waitForPlaylistPanelAndObserve();

    // SPA navigation / theme events - kept minimal to avoid duplicates
    addEventListener("yt-navigate-finish", refresh, { passive: true });
    addEventListener("yt-dark-mode-toggled", refresh, { passive: true });

    // Cleanup when page is being unloaded / navigated away from
    addEventListener("pagehide", () => {
      if (panelObserver) {
        try { panelObserver.disconnect(); } catch (e) {}
        panelObserver = null;
      }
    }, { once: true });
  };

  if (document.documentElement && (document.head || document.readyState === "complete")) {
    init();
  } else {
    addEventListener("DOMContentLoaded", init, { once: true });
  }
})();