YouTube Always Show More (Subscriptions)

Automatically expands the "Show more" link in the left sidebar Subscriptions on YouTube, and keeps it expanded across navigations.

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

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

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

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

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

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YouTube Always Show More (Subscriptions)
// @namespace    https://yourscripts.example
// @version      1.2.0
// @license      MIT
// @description  Automatically expands the "Show more" link in the left sidebar Subscriptions on YouTube, and keeps it expanded across navigations.
// @author       ezzdev
// @match        https://www.youtube.com/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function() {
  'use strict';

  // Debounce helper
  const debounce = (fn, wait = 200) => {
    let t;
    return (...args) => {
      clearTimeout(t);
      t = setTimeout(() => fn(...args), wait);
    };
  };

  // Utility: find potential "Show more" toggles inside the left guide
  function findShowMoreButtons(guideRoot) {
    if (!guideRoot) return [];

    const candidates = [];

    // 1) Buttons/controls that are collapsed via aria-expanded
    candidates.push(...guideRoot.querySelectorAll(
      'button[aria-expanded="false"], [role="button"][aria-expanded="false"]'
    ));

    // 2) Buttons with an aria-label or title that mentions "Show more" (case-insensitive)
    candidates.push(...Array.from(guideRoot.querySelectorAll('button, [role="button"], yt-button-shape, tp-yt-paper-item')).filter(el => {
      const label = (el.getAttribute?.('aria-label') || el.getAttribute?.('title') || '').trim().toLowerCase();
      return label.includes('show more') || label.includes('more');
    }));

    // 3) Fallback: elements whose visible text says "Show more" (case-insensitive)
    candidates.push(...Array.from(guideRoot.querySelectorAll('button, [role="button"], yt-button-shape, tp-yt-paper-item')).filter(el => {
      const text = (el.textContent || '').trim().toLowerCase();
      // keep it loose but not too loose
      return text === 'show more' || text === 'more' || text.startsWith('show more');
    }));

    // Only keep ones that are actually visible
    const visible = candidates.filter(el => {
      const rect = el.getBoundingClientRect?.();
      const style = window.getComputedStyle?.(el);
      return rect && rect.width > 0 && rect.height > 0 && style && style.visibility !== 'hidden' && style.display !== 'none';
    });

    // Dedupe
    return Array.from(new Set(visible));
  }

  // Try to identify the left guide root (desktop #guide; fallback to drawer guide)
  function getGuideRoot() {
    return document.querySelector('#guide') ||
           document.querySelector('ytd-guide-renderer') ||
           document.querySelector('ytd-mini-guide-renderer') ||
           null;
  }

  let lastClickTs = 0;

  function ensureExpanded() {
    const now = Date.now();
    // throttle: no more than once every 500ms
    if (now - lastClickTs < 500) return;

    const guideRoot = getGuideRoot();
    if (!guideRoot) return;

    // We only want the Subscriptions "Show more" in the left guide.
    // Click any collapsed toggles we can find; usually there is only one relevant.
    const buttons = findShowMoreButtons(guideRoot);

    // Prefer ones that live inside a Subscriptions section if we can spot it
    const prioritized = buttons.sort((a, b) => {
      const aScore = scoreButton(a);
      const bScore = scoreButton(b);
      return bScore - aScore; // higher score first
    });

    for (const btn of prioritized) {
      try {
        btn.click();
        lastClickTs = Date.now();
        // Small break in case there are multiple collapses
        break;
      } catch (_) {}
    }
  }

  // Heuristic: reward buttons near text that says "Subscriptions" (any language-safe-ish hint)
  function scoreButton(el) {
    let score = 0;

    // aria-expanded false is a good sign
    if (el.getAttribute('aria-expanded') === 'false') score += 3;

    // Is it in the guide?
    if (el.closest('#guide, ytd-guide-renderer, ytd-mini-guide-renderer')) score += 2;

    // Nearby text “Subscriptions”
    const nearbyText = (el.closest('ytd-guide-section-renderer') || el.parentElement)?.textContent?.toLowerCase() || '';
    if (/\bsubscription/.test(nearbyText)) score += 3;

    // Label hints
    const label = (el.getAttribute('aria-label') || el.getAttribute('title') || el.textContent || '').toLowerCase();
    if (label.includes('show more')) score += 2;
    if (label === 'more' || label.includes('more')) score += 1;

    return score;
  }

  // Observe changes in the guide so we can re-expand when YouTube re-renders
  const observeGuide = debounce(() => {
    const guideRoot = getGuideRoot();
    if (!guideRoot) return;

    ensureExpanded();

    // MutationObserver to catch SPA updates or reflows of the guide section
    const mo = new MutationObserver(debounce(() => {
      ensureExpanded();
    }, 200));

    mo.observe(guideRoot, { childList: true, subtree: true });
  }, 250);

  // Hook into YouTube’s SPA navigation events if present
  window.addEventListener('yt-navigate-finish', () => {
    observeGuide();
    // Run a few times after navigation just in case
    setTimeout(ensureExpanded, 200);
    setTimeout(ensureExpanded, 600);
    setTimeout(ensureExpanded, 1200);
  }, { passive: true });

  // Initial attempts after page ready
  const kickOff = () => {
    observeGuide();
    // Try a few times early in case the guide loads late
    let tries = 0;
    const t = setInterval(() => {
      ensureExpanded();
      tries++;
      if (tries >= 10) clearInterval(t);
    }, 400);
  };

  if (document.readyState === 'complete' || document.readyState === 'interactive') {
    kickOff();
  } else {
    window.addEventListener('DOMContentLoaded', kickOff, { once: true });
  }
})();