您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Автоматически раскрывает ветки комментариев на YouTube, включая "Показать ещё ответы" и "Читать дальше".
// ==UserScript== // @name YouTube - Развернуть комментарии // @namespace http://tampermonkey.net/ // @version 1.2025.519.0 // @description Автоматически раскрывает ветки комментариев на YouTube, включая "Показать ещё ответы" и "Читать дальше". // @author dlw@mcprv (адаптировано для Tampermonkey) // @match *://*.youtube.com/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; // --- НАСТРОЙКИ ПО УМОЛЧАНИЮ --- const DEFAULTS = { auto_expand: true, expand_show_more_replies: true, expand_read_more: true, hide_read_less_button: false, wait_time: 200, view_replies_selector: "#more-replies", show_more_replies_selector: ".style-scope.ytd-continuation-item-renderer button.yt-spec-button-shape-next--text", read_more_selector: "ytd-comments tp-yt-paper-button#more", read_less_selector: "ytd-comments tp-yt-paper-button#less", toggle_expand_shortcutkey: "", expand_replies_shortcutkey: "", expand_show_more_replies_shortcutkey: "", expand_read_more_shortcutkey: "", hide_replies_shortcutkey: "", remove_focus_shortcutkey: "", scroll_by_space: false }; let options = {}; // --- ФУНКЦИИ ДЛЯ РАБОТЫ С НАСТРОЙКАМИ (GM_setValue / GM_getValue) --- function loadOptions() { for (const key in DEFAULTS) { options[key] = GM_getValue(key, DEFAULTS[key]); } // Убедимся, что wait_time - это число options.wait_time = parseInt(options.wait_time, 10); } function saveOption(key, value) { options[key] = value; GM_setValue(key, value); } // --- РЕГИСТРАЦИЯ МЕНЮ ДЛЯ НАСТРОЕК В TAMPERMONKEY --- function registerMenuCommands() { GM_registerMenuCommand(`${options.auto_expand ? '✅' : '❌'} Автоматически раскрывать комментарии`, () => { saveOption('auto_expand', !options.auto_expand); location.reload(); // Перезагружаем страницу для применения }); GM_registerMenuCommand(`${options.expand_show_more_replies ? '✅' : '❌'} Также раскрывать "Показать ещё ответы"`, () => { saveOption('expand_show_more_replies', !options.expand_show_more_replies); initSelectors(); }); GM_registerMenuCommand(`${options.expand_read_more ? '✅' : '❌'} Также раскрывать "Читать дальше"`, () => { saveOption('expand_read_more', !options.expand_read_more); initSelectors(); }); GM_registerMenuCommand("--- Команды ---"); GM_registerMenuCommand("Раскрыть все ветки комментариев", () => expand_replies("expand_replies")); GM_registerMenuCommand("Раскрыть все \"Показать ещё ответы\"", () => expand_replies("expand_show_more_replies")); GM_registerMenuCommand("Раскрыть все \"Читать дальше\"", () => expand_replies("expand_read_more")); GM_registerMenuCommand("Скрыть все ветки комментариев", hide_replies); } // --- ОСНОВНАЯ ЛОГИКА СКРИПТА --- let observer = new MutationObserver(observe_func); let disconnected = false; let target_selector = ""; const node_spool = new Set(); let lock = false; function initSelectors() { target_selector = options.view_replies_selector; if (options.expand_show_more_replies) { target_selector += "," + options.show_more_replies_selector; } if (options.expand_read_more) { target_selector += "," + options.read_more_selector; } if (options.hide_read_less_button) { GM_addStyle(`${options.read_less_selector} { display: none !important; }`); } else { GM_addStyle(`${options.read_less_selector} { display: revert !important; }`); } } function init() { if (options.auto_expand) { observer.observe(document, { childList: true, subtree: true }); document.addEventListener("scroll", scroll_listener); window.addEventListener("resize", resize_listener); } else { disconnected = true; observer.disconnect(); document.removeEventListener("scroll", scroll_listener); window.removeEventListener("resize", resize_listener); } initSelectors(); document.removeEventListener("keydown", shortcut_keys); if (Object.values(options).some(val => typeof val === 'string' && val.includes('+')) || options.scroll_by_space) { document.addEventListener("keydown", shortcut_keys); } } function observe_func(mutations) { if (disconnected) return; for (const mutation of mutations) { if (!mutation.addedNodes.length) continue; for (const node of mutation.addedNodes) { if (node.nodeType !== 1) continue; // Проверяем, существует ли узел в DOM перед запросом if (document.body.contains(node)) { const elems = node.querySelectorAll(target_selector); for (const elem of elems) { node_spool.add(elem); } } } } // Дополнительная проверка для "Show more replies", так как они могут появляться динамически if (options.expand_show_more_replies) { const elems = document.querySelectorAll(options.show_more_replies_selector); for (const elem of elems) { node_spool.add(elem); } } } let timer; function scroll_listener() { clearTimeout(timer); timer = setTimeout(click, 300); } function resize_listener() { click(); } async function click() { if (lock) return; lock = true; for (const node of [...node_spool]) { if (!document.body.contains(node)) { node_spool.delete(node); continue; } const rect = node.getBoundingClientRect(); // Раскрываем комментарии, которые находятся в пределах 2-х высот окна просмотра if (rect.top <= window.innerHeight * 2) { node_spool.delete(node); if (rect.top !== 0 && rect.height !== 0) { // Проверяем, что элемент видим node.click(); await new Promise(resolve => setTimeout(resolve, options.wait_time)); } } } lock = false; } async function expand_replies(level) { let selectors = []; if (level === "expand_replies" || level === "expand_show_more_replies" || level === "expand_read_more") { selectors.push(options.view_replies_selector); } if (level === "expand_show_more_replies" || level === "expand_read_more") { selectors.push(options.show_more_replies_selector); } if (level === "expand_read_more") { selectors.push(options.read_more_selector); } for (const selector of selectors) { const elems = document.querySelectorAll(selector); for (const elem of elems) { elem.click(); await new Promise(resolve => setTimeout(resolve, options.wait_time)); } } } async function hide_replies() { const elems = document.querySelectorAll("#less-replies, " + options.read_less_selector); for (const elem of elems) { elem.click(); await new Promise(resolve => setTimeout(resolve, 50)); // Небольшая задержка } } function shortcut_keys(e) { const player = document.getElementById("movie_player"); if (e.repeat || (e.target != document.body && e.target != player && e.target.tagName !== 'INPUT')) { return; } let mod = []; if (e.ctrlKey) mod.push("Ctrl"); if (e.altKey) mod.push("Alt"); if (e.shiftKey) mod.push("Shift"); if (e.metaKey) mod.push("Meta"); let key = e.key; if (key.length === 1 && key !== " ") { key = key.toUpperCase(); } mod.push(key); const text = mod.join(" + "); if (text === options.remove_focus_shortcutkey) { if (document.activeElement === player) { document.activeElement.blur(); } else if (document.activeElement === document.body) { player.focus({ preventScroll: true }); } return; } if (e.target !== document.body) return; if (e.key === " " && options.scroll_by_space) { e.preventDefault(); window.scrollByPages(e.shiftKey ? -1 : 1); return; } switch (text) { case options.toggle_expand_shortcutkey: saveOption('auto_expand', !options.auto_expand); location.reload(); break; case options.expand_replies_shortcutkey: expand_replies("expand_replies"); break; case options.expand_show_more_replies_shortcutkey: expand_replies("expand_show_more_replies"); break; case options.expand_read_more_shortcutkey: expand_replies("expand_read_more"); break; case options.hide_replies_shortcutkey: hide_replies(); break; } } let lastUrl = location.href; new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; node_spool.clear(); // Очищаем пул при смене URL } }).observe(document, { subtree: true, childList: true }); // --- ЗАПУСК СКРИПТА --- // Ожидаем загрузки контента перед инициализацией if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { loadOptions(); registerMenuCommands(); init(); }); } else { loadOptions(); registerMenuCommands(); init(); } })();