YouTube No Saturated Hover

Removes YouTube's 2025 saturated hover effects.

当前为 2025-11-15 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

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