您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
稳定展开YouTube评论和回复,包括“显示更多回复”。兼容新界面。
// ==UserScript== // @name YouTube コメントと返信を自動展開 ✅ // @name:en YouTube Auto Expand Comments and Replies ✅ // @name:ja YouTube コメントと返信を自動展開 ✅ // @name:zh-CN 自动展开 YouTube 评论与回复 ✅ // @name:zh-TW 自動展開 YouTube 評論與回覆 ✅ // @name:ko YouTube 댓글 및 답글 자동 확장 ✅ // @name:fr Déploiement automatique des commentaires YouTube ✅ // @name:es Expansión automática de comentarios de YouTube ✅ // @name:de YouTube-Kommentare automatisch erweitern ✅ // @name:pt-BR Expandir automaticamente os comentários do YouTube ✅ // @name:ru Авторазворачивание комментариев на YouTube ✅ // @version 3.5.0 // @description 安定動作でYouTubeのコメントと返信、「他の返信を表示」も自動展開!現行UIに完全対応。 // @description:en Reliably auto-expands YouTube comments, replies, and "Show more replies". Fully updated for current UI. // @description:zh-CN 稳定展开YouTube评论和回复,包括“显示更多回复”。兼容新界面。 // @description:zh-TW 穩定展開YouTube評論和回覆,包括「顯示更多回覆」。支援最新介面。 // @description:ko YouTube의 댓글과 답글을 안정적으로 자동 확장. 최신 UI에 대응. // @description:fr Déploie automatiquement les commentaires et réponses YouTube. Compatible avec la nouvelle interface. // @description:es Expande automáticamente los comentarios y respuestas en YouTube. Totalmente actualizado para la nueva interfaz. // @description:de Erweiterung von YouTube-Kommentaren und Antworten – automatisch und zuverlässig. Für aktuelle Oberfläche optimiert. // @description:pt-BR Expande automaticamente comentários e respostas no YouTube. Compatível com a nova UI. // @description:ru Автоматически разворачивает комментарии и ответы на YouTube. Полностью адаптирован к новому интерфейсу. // @namespace https://github.com/koyasi777/youtube-auto-expand-comments // @author koyasi777 // @match https://www.youtube.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @grant none // @run-at document-end // @license MIT // @homepageURL https://github.com/koyasi777/youtube-auto-expand-comments // @supportURL https://github.com/koyasi777/youtube-auto-expand-comments/issues // ==/UserScript== (function () { 'use strict'; const CONFIG = { INITIAL_DELAY: 1500, CLICK_INTERVAL: 500, EXPANDED_CLASS: 'yt-auto-expanded', DEBUG: false }; const SELECTORS = { COMMENTS: 'ytd-comments#comments', COMMENT_THREAD: 'ytd-comment-thread-renderer', MORE_COMMENTS: 'ytd-continuation-item-renderer #button:not([disabled])', SHOW_REPLIES: '#more-replies > yt-button-shape > button:not([disabled])', HIDDEN_REPLIES: 'ytd-comment-replies-renderer ytd-button-renderer#more-replies button:not([disabled])', CONTINUATION_REPLIES: 'ytd-comment-replies-renderer ytd-continuation-item-renderer ytd-button-renderer button:not([disabled])', READ_MORE: 'tp-yt-paper-button#more[aria-expanded="false"]:not([aria-disabled="true"])' }; class YouTubeCommentExpander { constructor() { this.observer = null; this.io = null; this.ioTargets = []; this.ioControlInterval = null; this.expandedComments = new Set(); this.autoClickPaused = false; this.resumeTimer = null; } log(...args) { if (CONFIG.DEBUG) console.log('[YTCExpander]', ...args); } isNotificationOpen() { const notificationBtn = document.querySelector('ytd-notification-topbar-button-renderer button[aria-expanded="true"]'); return !!notificationBtn; } isAutoClickPaused() { return this.autoClickPaused; } getCommentId(thread) { const timestamp = thread.querySelector('#header-author time')?.getAttribute('datetime') || ''; const id = thread.getAttribute('data-thread-id') || ''; return `${id}-${timestamp}`; } isCommentExpanded(thread) { return this.expandedComments.has(this.getCommentId(thread)); } markAsExpanded(thread) { thread.classList.add(CONFIG.EXPANDED_CLASS); this.expandedComments.add(this.getCommentId(thread)); } async clickElements(selector) { if (this.isAutoClickPaused()) { this.log('Auto-click paused, skipping:', selector); return; } const elements = Array.from(document.querySelectorAll(selector)); for (const el of elements) { const thread = el.closest(SELECTORS.COMMENT_THREAD); if (thread && this.isCommentExpanded(thread)) continue; const inComment = thread || el.closest(SELECTORS.COMMENTS); if (!inComment) continue; el.scrollIntoView({ behavior: 'auto', block: 'center' }); await new Promise(r => setTimeout(r, 100)); if (el.disabled || el.getAttribute('aria-expanded') === 'true') continue; try { el.click(); if (thread) this.markAsExpanded(thread); await new Promise(r => setTimeout(r, CONFIG.CLICK_INTERVAL)); } catch (e) { this.log('Click error:', e); } } } async processVisibleElements() { if (this.isAutoClickPaused()) { this.log('Auto-click paused, skipping processVisibleElements'); return; } await this.clickElements(SELECTORS.MORE_COMMENTS); await this.clickElements(SELECTORS.SHOW_REPLIES); await this.clickElements(SELECTORS.HIDDEN_REPLIES); await this.clickElements(SELECTORS.CONTINUATION_REPLIES); await this.clickElements(SELECTORS.READ_MORE); } setupGlobalClickPause() { document.addEventListener('click', (e) => { const sortMenu = e.target.closest('tp-yt-paper-menu-button'); if (sortMenu) { this.autoClickPaused = true; this.log('Auto-click PAUSED due to click on sort menu'); clearTimeout(this.resumeTimer); this.resumeTimer = setTimeout(() => { this.autoClickPaused = false; this.log('Auto-click RESUMED after sort menu interaction'); }, 2000); } }, true); } setupMutationObserver() { const container = document.querySelector(SELECTORS.COMMENTS); if (!container) return false; this.observer = new MutationObserver(() => { if (this.isAutoClickPaused()) { this.log('Auto-click paused, skipping mutation-triggered expansion'); return; } this.processVisibleElements(); }); this.observer.observe(container, { childList: true, subtree: true }); return true; } setupReadMoreIntersectionObserver() { const observer = new IntersectionObserver(entries => { for (const entry of entries) { if (entry.isIntersecting) { const el = entry.target; if (!el || el.getAttribute('aria-expanded') === 'true') continue; try { el.click(); setTimeout(() => { const less = el.parentElement?.parentElement?.querySelector('tp-yt-paper-button#less'); if (less) less.style.display = 'none'; }, 500); } catch (e) { this.log('ReadMore IO click error', e); } } } }); const observeAllReadMores = () => { const buttons = document.querySelectorAll(SELECTORS.READ_MORE); buttons.forEach(btn => observer.observe(btn)); }; observeAllReadMores(); const container = document.querySelector(SELECTORS.COMMENTS); if (container) { new MutationObserver(observeAllReadMores).observe(container, { childList: true, subtree: true }); } } setupIntersectionObserver() { this.io = new IntersectionObserver(entries => { if (this.isNotificationOpen() || this.isAutoClickPaused()) return; for (const entry of entries) { const inComment = entry.target.closest(SELECTORS.COMMENT_THREAD) || entry.target.closest(SELECTORS.COMMENTS); if (entry.isIntersecting && inComment) { try { entry.target.click(); } catch (e) { this.log('IO click error', e); } } } }); const register = () => { const buttons = document.querySelectorAll( `${SELECTORS.MORE_COMMENTS},${SELECTORS.SHOW_REPLIES},${SELECTORS.HIDDEN_REPLIES},${SELECTORS.CONTINUATION_REPLIES}` ); this.ioTargets = Array.from(buttons).filter(btn => btn.closest(SELECTORS.COMMENT_THREAD) || btn.closest(SELECTORS.COMMENTS) ); this.ioTargets.forEach(btn => this.io.observe(btn)); }; register(); const container = document.querySelector(SELECTORS.COMMENTS); if (container) { new MutationObserver(register).observe(container, { childList: true, subtree: true }); } this.ioControlInterval = setInterval(() => { if (this.isNotificationOpen() || this.isAutoClickPaused()) { this.io.disconnect(); } else { this.ioTargets.forEach(el => { try { this.io.observe(el); } catch {} }); } }, 500); } async init() { if (!location.pathname.startsWith('/watch')) return; for (let i = 0; i < 10 && !document.querySelector(SELECTORS.COMMENTS); i++) { await new Promise(r => setTimeout(r, CONFIG.INITIAL_DELAY)); } if (!document.querySelector(SELECTORS.COMMENTS)) return; this.setupGlobalClickPause(); this.setupMutationObserver(); this.setupReadMoreIntersectionObserver(); this.setupIntersectionObserver(); setTimeout(() => { if (!this.autoClickPaused) { this.processVisibleElements(); this.log('Initial processVisibleElements() executed'); } else { this.log('Initial expansion skipped due to paused state'); } }, 3000); } cleanup() { if (this.observer) this.observer.disconnect(); if (this.io) this.io.disconnect(); clearInterval(this.ioControlInterval); this.expandedComments.clear(); } } const expander = new YouTubeCommentExpander(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(() => expander.init(), CONFIG.INITIAL_DELAY)); } else { setTimeout(() => expander.init(), CONFIG.INITIAL_DELAY); } let lastUrl = location.href; new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; expander.cleanup(); setTimeout(() => expander.init(), CONFIG.INITIAL_DELAY); } }).observe(document.body, { childList: true, subtree: true }); })();