YouTube No-Shorts

隐藏所有 Shorts/短视频,不支持旧浏览器。添加了开关功能记忆功能和可选图标显示。

目前為 2025-08-25 提交的版本,檢視 最新版本

// ==UserScript==
// @name        YouTube No-Shorts
// @namespace    http://tampermonkey.net/
// @version     1.1
// @description 隐藏所有 Shorts/短视频,不支持旧浏览器。添加了开关功能记忆功能和可选图标显示。
// @description Hide all Shorts/Short Videos, older browsers are not supported. Added toggle function memory and optional icon display.
// @match       https://www.youtube.com/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_registerMenuCommand
// @run-at      document-start
// @license     MIT
// ==/UserScript==

(function () {
  "use strict";

  // 调试模式开关,设为false可减少日志输出
  const DEBUG = false;

  // 调试日志函数
  function debugLog(message) {
    if (DEBUG) console.log(`[Anti-Shorts] ${message}`);
  }

  // 检测浏览器语言并返回相应的菜单命令文本
  function getMenuCommandTexts() {
    const browserLang = navigator.language || '';
    debugLog(`检测到浏览器语言: ${browserLang}`);

    const isChinese = browserLang.startsWith('zh');

    return isChinese ? {
      enableIcon: "启动图标",
      disableIcon: "禁用图标"
    } : {
      enableIcon: "Show icon",
      disableIcon: "Hide icon"
    };
  }

  // 获取菜单命令文本
  const menuTexts = getMenuCommandTexts();

  // 检查油猴选项是否启用
  const enableAntiShorts = GM_getValue("enableAntiShorts", true);

  // 如果选项关闭,直接应用CSS隐藏shorts并不显示按钮
  if (!enableAntiShorts) {
    debugLog("油猴选项已关闭,默认隐藏所有Shorts");

    // 立即应用CSS规则
    const style = document.createElement("style");
    style.id = "__anti_shorts_css_forced";
    style.textContent = `
      ytd-rich-grid-media:has(a[href*="/shorts/"]),
      ytd-video-renderer:has(a[href*="/shorts/"]),
      ytd-grid-video-renderer:has(a[href*="/shorts/"]),
      ytd-rich-item-renderer:has(a[href*="/shorts/"]),
      ytd-compact-video-renderer:has(a[href*="/shorts/"]),
      ytd-playlist-video-renderer:has(a[href*="/shorts/"]),
      ytd-rich-grid-row:has(a[href*="/shorts/"]),
      grid-shelf-view-model:has(ytm-shorts-lockup-view-model),
      grid-shelf-view-model:has(ytm-shorts-lockup-view-model-v2),
      ytd-reel-shelf-renderer,
      ytd-rich-shelf-renderer[is-shorts],
      ytd-shelf-renderer {
        display: none !important;
      }
    `;

    // 使用更早的插入方式
    if (document.head) {
      document.head.appendChild(style);
    } else {
      // 如果head还不存在,等待它可用
      const observer = new MutationObserver(() => {
        if (document.head) {
          document.head.appendChild(style);
          observer.disconnect();
        }
      });
      observer.observe(document, { childList: true, subtree: true });
    }

    // 注册(不可用)菜单命令,允许用户重新启用图标
    GM_registerMenuCommand(menuTexts.enableIcon, () => {
      GM_setValue("enableAntiShorts", true);
      location.reload();
    });

    return; // 不继续执行其余代码
  }

  // CSS规则,用于隐藏Shorts
  const cssHas = `
    ytd-rich-grid-media:has(a[href*="/shorts/"]),
    ytd-video-renderer:has(a[href*="/shorts/"]),
    ytd-grid-video-renderer:has(a[href*="/shorts/"]),
    ytd-rich-item-renderer:has(a[href*="/shorts/"]),
    ytd-compact-video-renderer:has(a[href*="/shorts/"]),
    ytd-playlist-video-renderer:has(a[href*="/shorts/"]),
    ytd-rich-grid-row:has(a[href*="/shorts/"]),
    grid-shelf-view-model:has(ytm-shorts-lockup-view-model),
    grid-shelf-view-model:has(ytm-shorts-lockup-view-model-v2),
    ytd-reel-shelf-renderer,
    ytd-rich-shelf-renderer[is-shorts],
    ytd-shelf-renderer {
        display: none !important;
    }
  `;

  // 创建样式元素
  const styleElement = document.createElement("style");
  styleElement.id = "__anti_shorts_css";
  styleElement.textContent = cssHas;

  // 按钮样式和通知样式(合并到一个样式元素中)
  const uiStyleElement = document.createElement("style");
  uiStyleElement.textContent = `
    #anti-shorts-toggle {
      margin-left: 6px;
      padding: 0 8px;
      height: 28px;
      border-radius: 14px;
      border: 1px solid transparent;
      background-color: transparent;
      font-size: 13px;
      font-weight: 500;
      cursor: pointer;
      display: flex;
      align-items: center;
      transition: all 0.2s;
      white-space: nowrap;
      min-width: 85px;
      justify-content: center;
      color: var(--yt-spec-text-primary);
    }

    #anti-shorts-toggle:hover {
      background-color: var(--yt-spec-badge-chip-background-hover);
    }

    #anti-shorts-toggle.on {
      background-color: transparent;
      color: var(--yt-spec-text-primary);
    }

    #anti-shorts-toggle.on:hover {
      background-color: var(--yt-spec-badge-chip-background-hover);
    }

    .anti-shorts-notification {
      position: fixed;
      bottom: 20px;
      right: 20px;
      background-color: rgba(0, 0, 0, 0.8);
      color: white;
      padding: 12px 20px;
      border-radius: 8px;
      font-size: 14px;
      z-index: 9999;
      opacity: 0;
      transition: opacity 0.3s;
    }

    .anti-shorts-notification.show {
      opacity: 1;
    }
  `;

  // 缓存和状态管理
  const state = {
    button: null,
    notification: null,
    settings: {},
    lastUrl: location.href,
    buttonCheckInterval: null,
    spaObserver: null,
    domObserver: null,
    pageType: null,
    isInitialized: false
  };

  // 等待head元素可用
  function waitForHead(callback) {
    if (document.head) {
      callback();
    } else {
      const observer = new MutationObserver(() => {
        if (document.head) {
          observer.disconnect();
          callback();
        }
      });
      observer.observe(document, { childList: true, subtree: true });
    }
  }

  // 初始化样式
  waitForHead(() => {
    document.head.appendChild(uiStyleElement);
  });

  // 注册(不可用)菜单命令,允许用户禁用图标
  GM_registerMenuCommand(menuTexts.disableIcon, () => {
    GM_setValue("enableAntiShorts", false);
    location.reload();
  });

  // 显示通知(复用通知元素)
  function showNotification(message) {
    // 如果通知已存在,先移除
    if (state.notification) {
      state.notification.classList.remove('show');
      setTimeout(() => {
        if (state.notification && state.notification.parentNode) {
          state.notification.parentNode.removeChild(state.notification);
          state.notification = null;
        }
      }, 300);
    }

    // 创建新通知
    state.notification = document.createElement('div');
    state.notification.className = 'anti-shorts-notification';
    state.notification.textContent = message;
    document.body.appendChild(state.notification);

    // 显示通知
    requestAnimationFrame(() => {
      if (state.notification) {
        state.notification.classList.add('show');
      }
    });

    // 3秒后隐藏通知
    setTimeout(() => {
      if (state.notification) {
        state.notification.classList.remove('show');
        setTimeout(() => {
          if (state.notification && state.notification.parentNode) {
            state.notification.parentNode.removeChild(state.notification);
            state.notification = null;
          }
        }, 300);
      }
    }, 3000);
  }

  // 检测当前页面类型(缓存结果)
  function getPageType() {
    if (state.pageType) return state.pageType;

    const path = window.location.pathname;
    const search = window.location.search;

    if (path === "/" || path === "/feed/subscriptions") {
      state.pageType = "home";
    } else if (path === "/results" || search.includes("search_query")) {
      state.pageType = "search";
    } else {
      state.pageType = "other";
    }

    return state.pageType;
  }

  // 从localStorage获取设置,使用缓存减少读取
  function getHideShortsSetting(pageType) {
    // 如果缓存中有设置,直接返回
    if (state.settings[pageType] !== undefined) {
      return state.settings[pageType];
    }

    const key = `hideShorts_${pageType}`;
    const storedValue = localStorage.getItem(key);

    // 如果没有存储过值,默认为false(显示Shorts,即off状态)
    if (storedValue === null) {
      debugLog(`首次访问${pageType}页面,默认显示Shorts (off状态)`);
      state.settings[pageType] = false;
      return false;
    }

    // 解析存储的值并缓存
    const hideShorts = storedValue === "true";
    state.settings[pageType] = hideShorts;
    debugLog(`从记忆中读取${pageType}页面设置: ${hideShorts ? '隐藏' : '显示'} Shorts`);
    return hideShorts;
  }

  // 保存设置到localStorage并更新缓存
  function saveHideShortsSetting(pageType, hide) {
    const key = `hideShorts_${pageType}`;
    localStorage.setItem(key, hide);
    state.settings[pageType] = hide; // 更新缓存
    debugLog(`已保存${pageType}页面设置: ${hide ? '隐藏' : '显示'} Shorts`);

    // 显示保存成功的通知
    const pageTypeName = pageType === "home" ? "主页" : "搜索页";
    showNotification(`已保存设置:${pageTypeName} Shorts ${hide ? '已关闭' : '已开启'}`);
  }

  // 应用或移除CSS规则
  function applyShortsSetting() {
    const pageType = getPageType();
    const hideShorts = getHideShortsSetting(pageType);

    if (hideShorts) {
      if (!document.getElementById("__anti_shorts_css")) {
        document.documentElement.appendChild(styleElement);
        debugLog(`已应用CSS规则,${pageType}页面Shorts已隐藏`);
      }
    } else {
      const existingStyle = document.getElementById("__anti_shorts_css");
      if (existingStyle) {
        existingStyle.remove();
        debugLog(`已移除CSS规则,${pageType}页面Shorts已显示`);
      }
    }

    // 更新按钮文本
    updateButtonText();
  }

  // 更新按钮文本和样式
  function updateButtonText() {
    // 使用缓存的按钮元素
    const button = state.button || document.getElementById("anti-shorts-toggle");
    if (button) {
      state.button = button; // 缓存按钮元素
      const pageType = getPageType();
      const hideShorts = getHideShortsSetting(pageType);

      // 统一使用"shorts: on/off"格式
      const buttonText = hideShorts ? "shorts: off" : "shorts: on";
      button.textContent = buttonText;

      // 根据状态添加或移除on类
      button.classList.toggle("on", hideShorts);
    }
  }

  // 创建开关按钮
  function createToggleButton() {
    const button = document.createElement("button");
    button.id = "anti-shorts-toggle";

    // 设置初始文本
    const pageType = getPageType();
    const hideShorts = getHideShortsSetting(pageType);
    const buttonText = hideShorts ? "shorts: off" : "shorts: on";

    button.textContent = buttonText;
    button.classList.toggle("on", hideShorts);

    button.addEventListener("click", () => {
      const pageType = getPageType();
      const hideShorts = getHideShortsSetting(pageType);
      saveHideShortsSetting(pageType, !hideShorts);
      applyShortsSetting();
    });

    debugLog(`已创建控制按钮,初始文本: ${buttonText}`);
    return button;
  }

  // 查找并添加按钮到YouTube顶栏
  function addButtonToHeader() {
    // 如果按钮已存在,直接返回
    if (document.getElementById("anti-shorts-toggle")) {
      return true;
    }

    // 尝试多种可能的选择器,按优先级排序
    const possibleSelectors = [
      "#end",
      "#buttons",
      "ytd-masthead #end",
      "ytd-masthead #buttons"
    ];

    for (const selector of possibleSelectors) {
      const header = document.querySelector(selector);
      if (header) {
        const button = createToggleButton();
        header.appendChild(button);
        state.button = button; // 缓存按钮元素
        debugLog(`已添加控制按钮到YouTube顶栏 (${selector})`);
        return true;
      }
    }

    return false;
  }

  // 清理资源
  function cleanup() {
    if (state.buttonCheckInterval) {
      clearInterval(state.buttonCheckInterval);
      state.buttonCheckInterval = null;
    }

    if (state.spaObserver) {
      state.spaObserver.disconnect();
      state.spaObserver = null;
    }

    if (state.domObserver) {
      state.domObserver.disconnect();
      state.domObserver = null;
    }
  }

  // 处理页面导航
  function handleNavigation() {
    // 重置页面类型缓存
    state.pageType = null;

    // 应用设置
    applyShortsSetting();

    // 检查按钮是否存在,如果不存在则重新添加
    if (!document.getElementById("anti-shorts-toggle")) {
      debugLog("导航后按钮不存在,尝试重新添加");
      addButtonToHeader();
    }
  }

  // 初始化
  function init() {
    // 防止重复初始化
    if (state.isInitialized) return;
    state.isInitialized = true;

    debugLog("开始初始化");
    debugLog(`使用浏览器语言: ${navigator.language}`);

    // 应用初始设置
    applyShortsSetting();

    // 尝试添加按钮
    let buttonAdded = addButtonToHeader();

    // 如果按钮未添加成功,观察DOM变化
    if (!buttonAdded) {
      debugLog("按钮未添加成功,将观察DOM变化");

      state.domObserver = new MutationObserver(() => {
        if (!document.getElementById("anti-shorts-toggle") && addButtonToHeader()) {
          debugLog("通过DOM观察成功添加按钮");
          state.domObserver.disconnect();
          state.domObserver = null;
        }
      });

      // 只观察body的子元素变化,而不是整个子树
      state.domObserver.observe(document.body, {
        childList: true
      });

      // 30秒后停止观察
      setTimeout(() => {
        if (state.domObserver) {
          state.domObserver.disconnect();
          state.domObserver = null;
          if (!document.getElementById("anti-shorts-toggle")) {
            debugLog("30秒内未找到合适位置添加按钮");
          }
        }
      }, 30000);
    }

    // 监听URL变化,以便在页面导航时更新设置
    window.addEventListener("popstate", handleNavigation);

    // 监听YouTube的SPA导航
    state.spaObserver = new MutationObserver(() => {
      const url = location.href;
      if (url !== state.lastUrl) {
        state.lastUrl = url;
        debugLog("检测到YouTube SPA导航,更新设置");

        // 使用requestAnimationFrame确保DOM已更新
        requestAnimationFrame(handleNavigation);
      }
    });

    // 只观察documentElement的子元素变化,减少观察范围
    state.spaObserver.observe(document.documentElement, {
      childList: true,
      subtree: false
    });

    // 减少按钮检查频率,从5秒改为10秒
    state.buttonCheckInterval = setInterval(() => {
      const button = document.getElementById("anti-shorts-toggle");
      if (button && (!button.textContent || button.textContent.trim() === "")) {
        debugLog("检测到按钮文本为空,更新");
        updateButtonText();
      }
    }, 10000);

    // 页面卸载时清理资源
    window.addEventListener("beforeunload", cleanup);

    debugLog("初始化完成,记忆功能已启用");
  }

  // 确保DOM加载完成后初始化
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }

  debugLog("现代浏览器模式已启用 (CSS :has())");
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址