您需要先安装一个扩展,例如 篡改猴、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 ✅ // @description 安定動作でYouTubeのコメントと返信、「他の返信を表示」も自動展開!現行UIに完全対応。 // @description:en Reliably auto-expands YouTube comments, replies, and "Show more replies". Fully updated for current UI. // @description:ja 安定動作でYouTubeのコメントと返信、「他の返信を表示」も自動展開!現行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. Полностью адаптирован к новому интерфейсу. // @version 5.7.1 // @namespace https://github.com/koyasi777/youtube-auto-comment-expander // @author koyasi777 // @match *://www.youtube.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @run-at document-end // @license MIT // @homepageURL https://github.com/koyasi777/youtube-auto-comment-expander // @supportURL https://github.com/koyasi777/youtube-auto-comment-expander/issues // ==/UserScript== (function() { 'use strict'; class ConfigManager { constructor() { this.defaults = { scriptEnabled: true, debugMode: false, initialDelay: 2500, clickInterval: 130, expandComments: true, expandReplies: true, expandNestedReplies: true, expandLongComments: true, }; this.config = {}; this.load(); } load() { for (const key in this.defaults) this.config[key] = GM_getValue(key, this.defaults[key]); } get(key) { return this.config[key]; } set(key, value) { this.config[key] = value; GM_setValue(key, value); } reset() { for (const key in this.defaults) this.set(key, this.defaults[key]); } registerMenu() { GM_registerMenuCommand('⚙️ コメント自動展開 設定', () => this.showSettingsPrompt()); GM_registerMenuCommand('🗑️ 設定をリセット', () => { if (confirm('本当に全ての設定をリセットしますか?')) { this.reset(); alert('設定がリセットされました。ページをリロードして反映させてください。'); } }); } showSettingsPrompt() { const newSettings = {}; for (const key in this.defaults) { const currentValue = this.get(key), type = typeof this.defaults[key]; let newValue = prompt(`${key} (${type}) [デフォルト: ${this.defaults[key]}]\n現在の値: ${currentValue}`, currentValue); if (newValue === null) return; if (type === 'boolean') newSettings[key] = newValue.toLowerCase() === 'true'; else if (type === 'number') { newSettings[key] = parseInt(newValue, 10); if (isNaN(newSettings[key])) newSettings[key] = this.defaults[key]; } else newSettings[key] = newValue; } for (const key in newSettings) this.set(key, newSettings[key]); alert('設定が更新されました。ページをリロードして反映させてください。'); } } class YouTubeCommentExpander { constructor(config) { this.config = config; this.mainObserver = null; this.actionObserver = null; this.readMoreObserver = null; this.rules = [ { name: 'ExpandComments', selector: 'ytd-comments > #sections > #contents > ytd-continuation-item-renderer, ytd-engagement-panel-section-list-renderer > #contents > ytd-continuation-item-renderer', condition: () => this.config.get('expandComments') }, { name: 'ExpandReplies', selector: '#more-replies', condition: () => this.config.get('expandReplies') }, { name: 'ExpandNestedReplies', selector: 'ytd-comment-replies-renderer ytd-continuation-item-renderer button', condition: () => this.config.get('expandNestedReplies') }, ]; } log(level, ...args) { if (!this.config.get('debugMode')) return; console.log(`[YTCE:${level.toUpperCase()}]`, ...args); } setupObservers() { this.actionObserver = new IntersectionObserver(async (entries, observer) => { for (const entry of entries) { if (entry.isIntersecting && this.config.get('scriptEnabled')) { const target = entry.target; observer.unobserve(target); this.log('debug', 'Action target in view, clicking.', target); await new Promise(resolve => setTimeout(resolve, this.config.get('clickInterval'))); const clickable = target.querySelector('button, yt-button-shape') || target; clickable.click(); } } }, { rootMargin: '0px 0px 500px 0px' }); if (this.config.get('expandLongComments')) { this.readMoreObserver = new IntersectionObserver(async (entries, observer) => { for (const entry of entries) { if (entry.isIntersecting && this.config.get('scriptEnabled')) { const button = entry.target; observer.unobserve(button); this.log('debug', 'ReadMore button in view, clicking.', button); await new Promise(resolve => setTimeout(resolve, this.config.get('clickInterval'))); button.click(); await new Promise(resolve => setTimeout(resolve, 200)); const commentViewModel = button.closest('ytd-comment-view-model, ytd-comment-renderer'); if (commentViewModel) { const lessButton = commentViewModel.querySelector('.less-button, tp-yt-paper-button#less'); if (lessButton) lessButton.style.display = 'none'; } } } }, { threshold: 0.1 }); } } observeNewNodes(node) { if (!(node instanceof Element)) return; for (const rule of this.rules) { if (rule.condition()) { if (node.matches(rule.selector)) this.actionObserver.observe(node); node.querySelectorAll(rule.selector).forEach(el => this.actionObserver.observe(el)); } } if (this.readMoreObserver) { const readMoreSelector = '#content-text[collapsed], .more-button.ytd-comment-view-model, tp-yt-paper-button#more:not([aria-expanded="true"])'; if (node.matches(readMoreSelector)) this.readMoreObserver.observe(node); node.querySelectorAll(readMoreSelector).forEach(btn => this.readMoreObserver.observe(btn)); } } start(commentsContainer) { if (!this.config.get('scriptEnabled')) { this.log('info', 'Script is disabled by toggle, not starting.'); return false; } if (!commentsContainer) { this.log('error', 'start() called without a valid container.'); return false; } this.stop(); this.log('info', 'Comment container found. Starting observers.', commentsContainer); this.setupObservers(); this.observeNewNodes(commentsContainer); this.mainObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) this.observeNewNodes(node); } }); this.mainObserver.observe(commentsContainer, { childList: true, subtree: true }); this.log('info', 'All observers started.'); return true; } stop() { if (this.mainObserver) { this.mainObserver.disconnect(); this.mainObserver = null; } if (this.actionObserver) { this.actionObserver.disconnect(); this.actionObserver = null; } if (this.readMoreObserver) { this.readMoreObserver.disconnect(); this.readMoreObserver = null; } this.log('info', 'All observers stopped and state reset.'); } } class UIManager { constructor(configManager, expander) { this.configManager = configManager; this.expander = expander; this.toggleContainerId = 'ytce-toggle-container'; this.toggle = null; this.uiObserver = null; this.icons = { on: `<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false"><path d="M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z"></path></svg>`, off: `<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"></path></svg>`, }; this.tooltips = { on: { ja: 'コメント自動展開: ON', en: 'Auto-expand comments: ON' }, off: { ja: 'コメント自動展開: OFF', en: 'Auto-expand comments: OFF' } }; this.injectStyles(); } injectStyles() { GM_addStyle(` #${this.toggleContainerId} { position: relative; display: flex; align-items: center; margin-left: 16px; border: 1px solid var(--yt-spec-mono-10, #ccc); border-radius: 16px; padding: 2px 8px; height: 30px; cursor: pointer; background-color: var(--yt-spec-badge-chip-background, #f2f2f2); box-shadow: 0 1px 2px rgba(0,0,0,0.05); transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out; -webkit-tap-highlight-color: transparent; } #${this.toggleContainerId}:hover { background-color: var(--yt-spec-mono-15, #e0e0e0); } #${this.toggleContainerId}.ytce-active { background-color: var(--yt-spec-brand-button-background, #1c62b9); border-color: var(--yt-spec-brand-button-background, #1c62b9); } .ytce-toggle-icon { width: 15px; height: 15px; margin-right: 6px; display: flex; align-items: center; pointer-events: none; } .ytce-toggle-icon svg { width: 15px; height: 15px; fill: var(--yt-spec-icon-inactive, #606060); transition: fill 0.2s ease-in-out; } #${this.toggleContainerId}.ytce-active .ytce-toggle-icon svg { fill: #fff; } .ytce-toggle-switch { position: relative; display: inline-block; width: 28px; height: 14px; pointer-events: none; } .ytce-toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #aaa; transition: .3s; border-radius: 14px; } .ytce-toggle-slider:before { position: absolute; content: ""; height: 10px; width: 10px; left: 2px; bottom: 2px; background-color: white; transition: .3s; border-radius: 50%; } input:checked + .ytce-toggle-slider { background-color: var(--yt-spec-call-to-action, #065fd4); } #${this.toggleContainerId}.ytce-active input:checked + .ytce-toggle-slider { background-color: rgba(255,255,255,0.4); } input:checked + .ytce-toggle-slider:before { transform: translateX(14px); } #${this.toggleContainerId} .ytce-toggle-switch input { opacity: 0 !important; width: 0 !important; height: 0 !important; position: absolute !important; z-index: -1 !important; pointer-events: none !important; } `); } createToggleElement() { if (document.getElementById(this.toggleContainerId)) return null; const container = document.createElement('div'); container.id = this.toggleContainerId; const tooltip = document.createElement('tp-yt-paper-tooltip'); tooltip.setAttribute('role', 'tooltip'); const tooltipText = document.createElement('div'); tooltipText.id = 'tooltip'; tooltipText.className = 'style-scope tp-yt-paper-tooltip'; tooltip.appendChild(tooltipText); const iconDiv = document.createElement('div'); const switchLabel = document.createElement('label'); switchLabel.className = 'ytce-toggle-switch'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; const slider = document.createElement('span'); slider.className = 'ytce-toggle-slider'; switchLabel.append(checkbox, slider); container.append(iconDiv, switchLabel, tooltip); this.toggle = { container, checkbox, iconDiv, tooltipText }; container.addEventListener('click', (e) => { e.stopPropagation(); checkbox.checked = !checkbox.checked; this.onToggleChange(); }); const initialState = this.configManager.get('scriptEnabled'); checkbox.checked = initialState; this.updateToggleVisuals(initialState); return container; } onToggleChange() { const isEnabled = this.toggle.checkbox.checked; this.configManager.set('scriptEnabled', isEnabled); this.updateToggleVisuals(isEnabled); this.expander.log('info', `Script ${isEnabled ? 'enabled' : 'disabled'} by toggle.`); if (isEnabled) { const commentsContainer = getCurrentCommentsContainer(); if (commentsContainer) this.expander.start(commentsContainer); } else { this.expander.stop(); } } updateToggleVisuals(isEnabled) { if (!this.toggle) return; this.toggle.iconDiv.innerHTML = this.icons[isEnabled ? 'on' : 'off']; this.toggle.iconDiv.className = `ytce-toggle-icon ${isEnabled ? 'on' : 'off'}`; this.toggle.container.classList.toggle('ytce-active', isEnabled); const lang = document.documentElement.lang.startsWith('ja') ? 'ja' : 'en'; this.toggle.tooltipText.textContent = this.tooltips[isEnabled ? 'on' : 'off'][lang]; } observeCommentsHeader(containerSelector, sortMenuSelector, sortMenuLabelSelector, insertMode) { waitForElement(containerSelector, (container) => { this.stop(); const updateUI = () => this.updateCommentsHeaderUI(sortMenuSelector, sortMenuLabelSelector, insertMode); this.uiObserver = new MutationObserver(updateUI); this.uiObserver.observe(container, { childList: true, subtree: true }); updateUI(); this.expander.log('info', `UI Observer started for "${containerSelector}".`); }); } updateCommentsHeaderUI(sortMenuSelector, sortMenuLabelSelector, insertMode) { const sortMenu = document.querySelector(sortMenuSelector); if (!sortMenu) return; if (!document.getElementById(this.toggleContainerId)) { const toggleElement = this.createToggleElement(); if (toggleElement) { if (insertMode === 'append') { sortMenu.parentElement.appendChild(toggleElement); } else if (insertMode === 'after') { sortMenu.insertAdjacentElement('afterend', toggleElement); } this.expander.log('debug', 'Toggle UI injected.'); } } const label = document.querySelector(sortMenuLabelSelector); if (label && label.style.display !== 'none') { label.style.display = 'none'; this.expander.log('debug', 'Sort menu label hidden.'); } } initForWatchPage() { this.observeCommentsHeader( 'ytd-comments#comments', '#comments #sort-menu', '#comments #sort-menu #icon-label', 'append' ); } initForShortsPage() { this.observeCommentsHeader( 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-comments-section"]', 'ytd-engagement-panel-title-header-renderer #menu', 'ytd-engagement-panel-title-header-renderer #menu #icon-label', 'after' ); } stop() { if (this.uiObserver) { this.uiObserver.disconnect(); this.uiObserver = null; this.expander.log('info', 'UI Observer stopped.'); } } } const configManager = new ConfigManager(); let expander = null; let uiManager = null; let currentPath = ''; function waitForElement(selector, callback, timeout = 15000) { let timeoutId = null; const observer = new MutationObserver((mutations, obs) => { const element = document.querySelector(selector); if (element) { if (timeoutId) clearTimeout(timeoutId); obs.disconnect(); callback(element); } }); timeoutId = setTimeout(() => { observer.disconnect(); expander?.log('warn', `waitForElement timed out for selector: ${selector}`); }, timeout); observer.observe(document.body, { childList: true, subtree: true }); } function getCurrentCommentsContainer() { if (location.pathname.startsWith('/watch')) { return document.querySelector('ytd-comments#comments'); } else if (location.pathname.startsWith('/shorts/')) { return document.querySelector('ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-comments-section"]'); } return null; } function initializeScript() { const path = location.pathname + location.search; if (currentPath === path && expander) return; currentPath = path; if (expander) expander.stop(); if (uiManager) uiManager.stop(); expander = new YouTubeCommentExpander(configManager); uiManager = new UIManager(configManager, expander); setTimeout(() => { if (location.pathname.startsWith('/shorts/')) { expander.log('info', 'Shorts page detected. Initializing...'); const commentsButtonSelector = '#comments-button button, #comments-button a'; waitForElement(commentsButtonSelector, (button) => { button.click(); const commentsContainerSelector = 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-comments-section"]'; waitForElement(commentsContainerSelector, (container) => { uiManager.initForShortsPage(); if (configManager.get('scriptEnabled')) expander.start(container); }); }); } else if (location.pathname.startsWith('/watch')) { expander.log('info', 'Watch page detected. Initializing...'); uiManager.initForWatchPage(); const commentsContainerSelector = 'ytd-comments#comments'; waitForElement(commentsContainerSelector, (container) => { if (configManager.get('scriptEnabled')) expander.start(container); }); } else { expander.log('info', 'Not a watch/shorts page. Script is idle.'); } }, configManager.get('initialDelay')); } configManager.registerMenu(); window.addEventListener('yt-navigate-finish', initializeScript, true); initializeScript(); })();