Greasy Fork

Greasy Fork is available in English.

YouTube 评论自动展开、翻译与排序 ✅

自动展开评论、回复和“显示更多回复”,自动点击翻译按钮,并可选择自动按“最新”排序。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        YouTube コメントと返信を自動展開・翻訳・並び替え ✅
// @name:en     YouTube Auto Expand, Translate & Sort Comments ✅
// @name:ja     YouTube コメントと返信を自動展開・翻訳・並び替え ✅
// @name:zh-CN  YouTube 评论自动展开、翻译与排序 ✅
// @name:zh-TW  YouTube 評論自動展開、翻譯與排序 ✅
// @name:ko     YouTube 댓글 자동 확장, 번역 및 정렬 ✅
// @name:fr     Déploiement, traduction et tri automatique des commentaires YouTube ✅
// @name:es     Expansión, traducción y ordenación automática de comentarios de YouTube ✅
// @name:de     Automatische Erweiterung, Übersetzung und Sortierung von YouTube-Kommentaren ✅
// @name:pt-BR  Expansão, tradução e ordenação automática de comentários do YouTube ✅
// @name:ru     Авторазворачивание, перевод и сортировка комментариев YouTube ✅
// @description YouTubeのコメント・返信・他の返信を自動展開し、翻訳ボタンも自動化。並び替え(新しい順)の自動選択も可能です。
// @description:en Automatically expands comments, replies, and "Show more replies". Also auto-translates comments and sorts by "Newest first".
// @description:ja YouTubeのコメント・返信・他の返信を自動展開し、翻訳ボタンも自動化。並び替え(新しい順)の自動選択も可能です。
// @description:zh-CN 自动展开评论、回复和“显示更多回复”,自动点击翻译按钮,并可选择自动按“最新”排序。
// @description:zh-TW 自動展開評論、回覆和「顯示更多回覆」,自動點擊翻譯按鈕,並可選擇自動按「最新」排序。
// @description:ko YouTube 댓글, 답글 및 "답글 더보기"를 자동 확장하고 번역 버튼을 클릭하며, "최신순" 자동 정렬 기능을 제공합니다.
// @description:fr Déploie automatiquement les commentaires et les réponses. Traduit automatiquement et trie par "Les plus récents".
// @description:es Expande automáticamente comentarios y respuestas. Traduce automáticamente y ordena por "Más recientes".
// @description:de Erweitert automatisch Kommentare und Antworten. Übersetzt automatisch und sortiert nach "Neueste zuerst".
// @description:pt-BR Expande automaticamente comentários e respostas. Traduz automaticamente e ordena por "Mais recentes".
// @description:ru Автоматически разворачивает комментарии и ответы. Выполняет автоперевод и сортирует по "Сначала новые".
// @version     6.2.0
// @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_unregisterMenuCommand
// @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';

    /**
     * Internationalization (i18n) Manager
     */
    const I18n = {
        languages: [
            { code: 'auto', label: 'Auto' },
            { code: 'ja', label: '日本語' },
            { code: 'en', label: 'English' },
            { code: 'zh-CN', label: '简体中文' },
            { code: 'zh-TW', label: '繁體中文' },
            { code: 'ko', label: '한국어' },
            { code: 'fr', label: 'Français' },
            { code: 'es', label: 'Español' },
            { code: 'de', label: 'Deutsch' },
            { code: 'pt-BR', label: 'Português (BR)' },
            { code: 'ru', label: 'Русский' }
        ],
        messages: {
            'ja': {
                settingsCommand: '⚙️ 設定 (コンソール)',
                resetCommand: '🗑️ 設定をリセット',
                resetConfirm: '本当に全ての設定をリセットしますか?',
                resetComplete: '設定がリセットされました。ページをリロードして反映させてください。',
                updateComplete: '設定が更新されました。ページをリロードして反映させてください。',
                modalTitle: 'Auto Expand 設定',
                lblLanguage: '言語:',
                tooltipOpenSettings: '詳細設定を開く',
                tooltipToggle: '自動展開を有効/無効にする',
                optLongComments: '長いコメントを展開 ("続きを読む")',
                optReplies: '返信を自動展開',
                optNestedReplies: '「他の返信を表示」も展開',
                optAutoTranslate: 'コメントを自動翻訳',
                optHideOriginal: '└ "原文を見る" を非表示',
                optSortByNewest: '自動で「新しい順」に並び替え'
            },
            'en': {
                settingsCommand: '⚙️ Settings (Console)',
                resetCommand: '🗑️ Reset Settings',
                resetConfirm: 'Are you sure you want to reset all settings?',
                resetComplete: 'Settings reset. Please reload the page.',
                updateComplete: 'Settings updated. Please reload the page.',
                modalTitle: 'Auto Expand Settings',
                lblLanguage: 'Language:',
                tooltipOpenSettings: 'Open Detailed Settings',
                tooltipToggle: 'Toggle Auto Expand On/Off',
                optLongComments: 'Expand long comments ("Read more")',
                optReplies: 'Auto expand replies',
                optNestedReplies: 'Expand "Show more replies"',
                optAutoTranslate: 'Auto translate comments',
                optHideOriginal: '└ Hide "Show original"',
                optSortByNewest: 'Auto sort by "Newest first"'
            },
            'zh-CN': {
                settingsCommand: '⚙️ 设置 (控制台)',
                resetCommand: '🗑️ 重置设置',
                resetConfirm: '确定要重置所有设置吗?',
                resetComplete: '设置已重置。请刷新页面。',
                updateComplete: '设置已更新。请刷新页面。',
                modalTitle: '自动展开设置',
                lblLanguage: '语言:',
                tooltipOpenSettings: '打开详细设置',
                tooltipToggle: '开启/关闭自动展开',
                optLongComments: '展开长评论 ("阅读更多")',
                optReplies: '自动展开回复',
                optNestedReplies: '展开 "显示更多回复"',
                optAutoTranslate: '自动翻译评论',
                optHideOriginal: '└ 隐藏 "查看原文"',
                optSortByNewest: '自动按 "最新" 排序'
            },
            'zh-TW': {
                settingsCommand: '⚙️ 設定 (控制台)',
                resetCommand: '🗑️ 重置設定',
                resetConfirm: '確定要重置所有設定嗎?',
                resetComplete: '設定已重置。請重新整理頁面。',
                updateComplete: '設定已更新。請重新整理頁面。',
                modalTitle: '自動展開設定',
                lblLanguage: '語言:',
                tooltipOpenSettings: '開啟詳細設定',
                tooltipToggle: '開啟/關閉自動展開',
                optLongComments: '展開長留言 ("顯示更多")',
                optReplies: '自動展開回覆',
                optNestedReplies: '展開 "顯示更多回覆"',
                optAutoTranslate: '自動翻譯留言',
                optHideOriginal: '└ 隱藏 "查看原文"',
                optSortByNewest: '自動按 "最新" 排序'
            },
            'ko': {
                settingsCommand: '⚙️ 설정 (콘솔)',
                resetCommand: '🗑️ 설정 초기화',
                resetConfirm: '모든 설정을 초기화하시겠습니까?',
                resetComplete: '설정이 초기화되었습니다. 페이지를 새로 고침하세요.',
                updateComplete: '설정이 업데이트되었습니다. 페이지를 새로 고침하세요.',
                modalTitle: '자동 확장 설정',
                lblLanguage: '언어:',
                tooltipOpenSettings: '상세 설정 열기',
                tooltipToggle: '자동 확장 켜기/끄기',
                optLongComments: '긴 댓글 확장 ("자세히 보기")',
                optReplies: '답글 자동 확장',
                optNestedReplies: '"답글 더보기" 확장',
                optAutoTranslate: '댓글 자동 번역',
                optHideOriginal: '└ "원본 보기" 숨기기',
                optSortByNewest: '자동으로 "최신순" 정렬'
            },
            'fr': {
                settingsCommand: '⚙️ Paramètres (Console)',
                resetCommand: '🗑️ Réinitialiser',
                resetConfirm: 'Voulez-vous vraiment réinitialiser tous les paramètres ?',
                resetComplete: 'Paramètres réinitialisés. Veuillez recharger la page.',
                updateComplete: 'Paramètres mis à jour. Veuillez recharger la page.',
                modalTitle: 'Paramètres Auto Expand',
                lblLanguage: 'Langue:',
                tooltipOpenSettings: 'Ouvrir les paramètres détaillés',
                tooltipToggle: 'Activer/Désactiver l\'extension auto',
                optLongComments: 'Développer les longs commentaires',
                optReplies: 'Développer les réponses',
                optNestedReplies: 'Développer "Afficher d\'autres réponses"',
                optAutoTranslate: 'Traduire automatiquement',
                optHideOriginal: '└ Masquer "Voir l\'original"',
                optSortByNewest: 'Trier auto par "Les plus récents"'
            },
            'es': {
                settingsCommand: '⚙️ Configuración (Consola)',
                resetCommand: '🗑️ Restablecer',
                resetConfirm: '¿Estás seguro de que deseas restablecer toda la configuración?',
                resetComplete: 'Configuración restablecida. Por favor, recarga la página.',
                updateComplete: 'Configuración actualizada. Por favor, recarga la página.',
                modalTitle: 'Configuración de Expansión',
                lblLanguage: 'Idioma:',
                tooltipOpenSettings: 'Abrir configuración detallada',
                tooltipToggle: 'Activar/Desactivar expansión automática',
                optLongComments: 'Expandir comentarios largos ("Leer más")',
                optReplies: 'Expandir respuestas automáticamente',
                optNestedReplies: 'Expandir "Mostrar más respuestas"',
                optAutoTranslate: 'Traducir comentarios automáticamente',
                optHideOriginal: '└ Ocultar "Ver original"',
                optSortByNewest: 'Ordenar auto por "Más recientes"'
            },
            'de': {
                settingsCommand: '⚙️ Einstellungen (Konsole)',
                resetCommand: '🗑️ Zurücksetzen',
                resetConfirm: 'Möchten Sie wirklich alle Einstellungen zurücksetzen?',
                resetComplete: 'Einstellungen zurückgesetzt. Bitte laden Sie die Seite neu.',
                updateComplete: 'Einstellungen aktualisiert. Bitte laden Sie die Seite neu.',
                modalTitle: 'Auto Expand Einstellungen',
                lblLanguage: 'Sprache:',
                tooltipOpenSettings: 'Detaillierte Einstellungen öffnen',
                tooltipToggle: 'Autom. Erweitern Ein/Aus',
                optLongComments: 'Lange Kommentare erweitern ("Mehr anzeigen")',
                optReplies: 'Antworten automatisch erweitern',
                optNestedReplies: '"Weitere Antworten" erweitern',
                optAutoTranslate: 'Kommentare automatisch übersetzen',
                optHideOriginal: '└ "Original ansehen" ausblenden',
                optSortByNewest: 'Autom. nach "Neueste zuerst" sortieren'
            },
            'pt-BR': {
                settingsCommand: '⚙️ Configurações (Console)',
                resetCommand: '🗑️ Redefinir',
                resetConfirm: 'Tem certeza que deseja redefinir todas as configurações?',
                resetComplete: 'Configurações redefinidas. Por favor, recarregue a página.',
                updateComplete: 'Configurações atualizadas. Por favor, recarregue a página.',
                modalTitle: 'Configurações de Expansão',
                lblLanguage: 'Idioma:',
                tooltipOpenSettings: 'Abrir configurações detalhadas',
                tooltipToggle: 'Ativar/Desactivar expansão automática',
                optLongComments: 'Expandir comentários longos ("Ler mais")',
                optReplies: 'Expandir respostas automaticamente',
                optNestedReplies: 'Expandir "Mostrar mais respostas"',
                optAutoTranslate: 'Traduzir comentários automaticamente',
                optHideOriginal: '└ Ocultar "Ver original"',
                optSortByNewest: 'Ordenar auto por "Mais recentes"'
            },
            'ru': {
                settingsCommand: '⚙️ Настройки (Консоль)',
                resetCommand: '🗑️ Сбросить настройки',
                resetConfirm: 'Вы уверены, что хотите сбросить все настройки?',
                resetComplete: 'Настройки сброшены. Пожалуйста, перезагрузите страницу.',
                updateComplete: 'Настройки обновлены. Пожалуйста, перезагрузите страницу.',
                modalTitle: 'Настройки авторазворачивания',
                lblLanguage: 'Язык:',
                tooltipOpenSettings: 'Открыть подробные настройки',
                tooltipToggle: 'Вкл/Выкл авторазворачивание',
                optLongComments: 'Разворачивать длинные комментарии ("Читать дальше")',
                optReplies: 'Автоматически разворачивать ответы',
                optNestedReplies: 'Разворачивать "Показать другие ответы"',
                optAutoTranslate: 'Автоперевод комментариев',
                optHideOriginal: '└ Скрыть "Показать оригинал"',
                optSortByNewest: 'Автосортировка "Сначала новые"'
            }
        },
        getCurrentLangCode: function() {
            if (typeof configManager !== 'undefined') {
                const userLang = configManager.get('userLanguage');
                if (userLang && userLang !== 'auto') return userLang;
            }
            return navigator.language || navigator.userLanguage || 'en';
        },
        t: function(key) {
            const code = this.getCurrentLangCode();
            let dict = this.messages[code] || this.messages[code.slice(0, 2)] || this.messages['en'];
            return dict[key] || key;
        }
    };

    class ConfigManager {
        constructor() {
            this.defaults = {
                scriptEnabled: true,
                userLanguage: 'auto',
                debugMode: false,
                initialDelay: 2500,
                clickInterval: 130,
                expandLongComments: true,
                expandReplies: true,
                expandNestedReplies: true,
                autoTranslate: true,
                hideOriginalButton: false,
                sortByNewest: false
            };
            this.config = {};
            this.menuIds = [];
            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() {
            if (typeof GM_unregisterMenuCommand === 'function') {
                this.menuIds.forEach(id => GM_unregisterMenuCommand(id));
                this.menuIds = [];
            }
            this.menuIds.push(GM_registerMenuCommand(I18n.t('settingsCommand'), () => this.showSettingsPrompt()));
            this.menuIds.push(GM_registerMenuCommand(I18n.t('resetCommand'), () => {
                if (confirm(I18n.t('resetConfirm'))) {
                    this.reset();
                    alert(I18n.t('resetComplete'));
                    location.reload();
                }
            }));
        }

        showSettingsPrompt() {
            const newSettings = {};
            for (const key in this.defaults) {
                const currentValue = this.get(key), type = typeof this.defaults[key];
                let newValue = prompt(`${key} (${type}) [Default: ${this.defaults[key]}]\nCurrent: ${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]);
            this.registerMenu();
            if (uiManager) uiManager.updateAllText();
            alert(I18n.t('updateComplete'));
        }
    }

    class YouTubeCommentExpander {
        constructor(config) {
            this.config = config;
            this.mainObserver = null;
            this.actionObserver = null;
            this.readMoreObserver = null;
            this.sortRetryTimer = null;
            this.rules = [
                { name: 'ExpandReplies', selector: '#more-replies, #more-replies-sub-thread', condition: () => this.config.get('expandReplies') },
                { name: 'ExpandNestedReplies', selector: 'ytd-comment-replies-renderer ytd-continuation-item-renderer', condition: () => this.config.get('expandNestedReplies') },
                { name: 'AutoTranslate', selector: 'ytd-comment-view-model .translate-button[state="untoggled"]', condition: () => this.config.get('autoTranslate') }
            ];
        }

        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, tp-yt-paper-button') || target.querySelector('yt-button-shape') || target;
                        clickable.click();
                    }
                }
            }, { rootMargin: '0px 0px 500px 0px' });

            this.readMoreObserver = new IntersectionObserver(async (entries, observer) => {
                for (const entry of entries) {
                    if (entry.isIntersecting && this.config.get('scriptEnabled') && this.config.get('expandLongComments')) {
                        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 = 'ytd-expander tp-yt-paper-button#more, .more-button.ytd-comment-view-model';
                if (node.matches(readMoreSelector)) this.readMoreObserver.observe(node);
                node.querySelectorAll(readMoreSelector).forEach(btn => this.readMoreObserver.observe(btn));
            }
        }

        processExistingNodes(container) {
            this.log('info', 'Settings changed. Re-processing existing nodes...');
            this.observeNewNodes(container);
        }

        // =========================================================================
        //  Enhanced Sort Logic with Retry Mechanism
        // =========================================================================
        initiateSortSequence(container) {
            if (!this.config.get('scriptEnabled') || !this.config.get('sortByNewest')) return;
            // Only applicable on Watch page standard comments
            if (!location.pathname.startsWith('/watch')) return;

            this.log('info', 'Sort sequence initiated. Polling for sort menu...');
            if (this.sortRetryTimer) clearTimeout(this.sortRetryTimer);

            // Retry for up to 20 attempts (approx 10 seconds)
            this.performSortAttempt(container, 0, 20);
        }

        async performSortAttempt(container, attempt, maxAttempts) {
            if (attempt >= maxAttempts) {
                this.log('warn', 'Sort menu never appeared or remained invalid. Giving up.');
                return;
            }

            const sortMenu = container.querySelector('#sort-menu');
            // If sort menu is not found or not visible yet
            if (!sortMenu || sortMenu.offsetParent === null) {
                this.sortRetryTimer = setTimeout(() => this.performSortAttempt(container, attempt + 1, maxAttempts), 500);
                return;
            }

            const trigger = sortMenu.querySelector('yt-sort-filter-sub-menu-renderer tp-yt-dropdown-menu #trigger, #trigger');
            if (!trigger) {
                this.sortRetryTimer = setTimeout(() => this.performSortAttempt(container, attempt + 1, maxAttempts), 500);
                return;
            }

            // Trigger found. Now checking state.
            this.log('debug', `Sort menu found on attempt ${attempt + 1}. Checking state...`);

            // Step 1: Open menu to populate items (essential for YouTube's lazy polymer)
            trigger.click();

            // Step 2: Short wait for DOM to hydrate items
            await new Promise(r => setTimeout(r, 100));

            const menuList = sortMenu.querySelector('tp-yt-paper-listbox#menu');
            if (!menuList) {
                // Should fail gracefully and close if something is wrong
                trigger.click(); // close attempts
                this.sortRetryTimer = setTimeout(() => this.performSortAttempt(container, attempt + 1, maxAttempts), 500);
                return;
            }

            const items = menuList.querySelectorAll('a.yt-simple-endpoint');
            if (items.length >= 2) {
                const newestItem = items[1]; // Usually 2nd option
                const isSelected = newestItem.classList.contains('iron-selected') || newestItem.querySelector('tp-yt-paper-item.iron-selected');

                if (isSelected) {
                    this.log('info', 'Already sorted by Newest. Closing menu.');
                    // Close the menu by clicking the trigger again or background
                    // Clicking trigger again is safer
                    trigger.click();
                } else {
                    this.log('info', 'Sorting by Newest...');
                    newestItem.click(); // This triggers reload.
                    // No further retries needed as page/comments will reload
                }
            } else {
                 // Items not ready? Close and retry.
                 trigger.click();
                 this.sortRetryTimer = setTimeout(() => this.performSortAttempt(container, attempt + 1, maxAttempts), 500);
            }
        }
        // =========================================================================

        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);

            // Initiate Sort with Retry Logic
            this.initiateSortSequence(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; }
            if (this.sortRetryTimer) { clearTimeout(this.sortRetryTimer); this.sortRetryTimer = 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.modalId = 'ytce-settings-modal';
            this.toggle = null;
            this.modalElements = {};
            this.uiObserver = null;
            this.pendingWait = null; // 待機プロセス管理用
            this.staticIcon = `<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18zM18 14H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"></path></svg>`;
            this.injectStyles();
        }

        injectStyles() {
            GM_addStyle(`
                /* Toolbar Toggle Container */
                #${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: default;
                    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;
                }
                /* Shorts specific styles */
                ytd-engagement-panel-title-header-renderer #${this.toggleContainerId} {
                    margin-left: 8px;
                    transform: scale(0.9);
                }
                #${this.toggleContainerId}:hover {
                    background-color: var(--yt-spec-mono-15, #e0e0e0);
                }
                #${this.toggleContainerId}.ytce-active {
                    background-color: var(--yt-spec-badge-chip-background, #f2f2f2);
                    border-color: var(--yt-spec-brand-button-background, #1c62b9);
                }
                .ytce-toggle-icon {
                    width: 20px; height: 20px; margin-right: 8px;
                    display: flex; align-items: center; cursor: pointer;
                    border-radius: 50%; padding: 2px;
                }
                .ytce-toggle-icon:hover {
                    background-color: rgba(0,0,0,0.1);
                }
                .ytce-toggle-icon svg {
                    width: 18px; height: 18px;
                    fill: var(--yt-spec-icon-inactive, #606060);
                    transition: fill 0.2s ease-in-out;
                }
                #${this.toggleContainerId}.ytce-active .ytce-toggle-icon svg {
                    fill: var(--yt-spec-brand-button-background, #065fd4);
                }
                .ytce-toggle-switch {
                    position: relative; display: inline-block; width: 28px; height: 14px; cursor: pointer;
                }
                .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);
                }
                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;
                }

                /* Modal Styles */
                .ytce-modal-overlay {
                    position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                    background-color: rgba(0, 0, 0, 0.5);
                    z-index: 99999;
                    display: none;
                    justify-content: center; align-items: center;
                    backdrop-filter: blur(2px);
                }
                .ytce-modal-overlay.visible { display: flex; }

                .ytce-modal {
                    background: var(--yt-spec-base-background, #fff);
                    color: var(--yt-spec-text-primary, #0f0f0f);
                    width: 450px; max-width: 90%;
                    border-radius: 12px;
                    box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.2);
                    display: flex; flex-direction: column;
                    border: 1px solid var(--yt-spec-10-percent-layer, #e5e5e5);
                    animation: ytceFadeIn 0.2s ease-out;
                }
                @keyframes ytceFadeIn {
                    from { opacity: 0; transform: scale(0.95); }
                    to { opacity: 1; transform: scale(1); }
                }

                .ytce-modal-header {
                    display: flex; justify-content: space-between; align-items: center;
                    padding: 16px 24px;
                    border-bottom: 1px solid var(--yt-spec-10-percent-layer, #e5e5e5);
                }
                .ytce-header-left {
                    display: flex; align-items: center; gap: 12px;
                }
                .ytce-modal-title {
                    font-size: 1.8rem; font-weight: 500;
                    color: var(--yt-spec-text-primary, #0f0f0f);
                    margin-right: 12px;
                }

                /* Language Label */
                .ytce-lang-label {
                    font-size: 1.3rem;
                    color: var(--yt-spec-text-secondary, #606060);
                    margin-right: 4px;
                }

                /* Language Select Fixed */
                .ytce-lang-select {
                    padding: 4px 8px; border-radius: 4px;
                    border: 1px solid var(--yt-spec-10-percent-layer, #ccc);
                    background-color: var(--yt-spec-menu-background, #fff);
                    color: var(--yt-spec-text-primary, #0f0f0f);
                    font-size: 1.2rem; cursor: pointer;
                    outline: none;
                }
                .ytce-lang-select:focus {
                    border-color: var(--yt-spec-call-to-action, #065fd4);
                }
                .ytce-lang-select option {
                    background-color: var(--yt-spec-menu-background, #fff);
                    color: var(--yt-spec-text-primary, #0f0f0f);
                }

                .ytce-close-btn {
                    background: none; border: none; cursor: pointer;
                    width: 36px; height: 36px; border-radius: 50%;
                    display: flex; align-items: center; justify-content: center;
                    transition: background-color 0.2s;
                }
                .ytce-close-btn:hover {
                    background-color: var(--yt-spec-10-percent-layer, #f2f2f2);
                }
                .ytce-close-btn svg {
                    width: 24px; height: 24px;
                    fill: var(--yt-spec-icon-active-other, #606060);
                }

                .ytce-modal-content {
                    padding: 8px 0;
                    overflow-y: auto; max-height: 70vh;
                }

                .ytce-menu-item {
                    display: flex; align-items: center; justify-content: space-between;
                    padding: 12px 24px; cursor: pointer; user-select: none;
                    transition: background-color 0.1s;
                }
                .ytce-menu-item:hover {
                    background-color: var(--yt-spec-10-percent-layer, #f2f2f2);
                }
                .ytce-menu-label { flex: 1; margin-right: 12px; font-size: 1.4rem; }
                .ytce-menu-checkbox {
                    width: 20px; height: 20px; accent-color: var(--yt-spec-call-to-action, #065fd4);
                    cursor: pointer;
                }

                body.ytce-hide-original ytd-tri-state-button-view-model.translate-button[state="toggled"] {
                    display: none !important;
                }
            `);
        }

        createToggleElement() {
            const existingToggle = document.getElementById(this.toggleContainerId);
            if (existingToggle) {
                existingToggle.remove();
            }
            const container = document.createElement('div');
            container.id = this.toggleContainerId;

            const iconDiv = document.createElement('div');
            iconDiv.className = 'ytce-toggle-icon';
            iconDiv.innerHTML = this.staticIcon;
            iconDiv.title = I18n.t('tooltipOpenSettings');
            iconDiv.onclick = (e) => {
                e.stopPropagation();
                this.openModal();
            };
            this.modalElements.iconDiv = iconDiv;

            const switchLabel = document.createElement('label');
            switchLabel.className = 'ytce-toggle-switch';
            switchLabel.title = I18n.t('tooltipToggle');
            this.modalElements.switchLabel = switchLabel;

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            const slider = document.createElement('span');
            slider.className = 'ytce-toggle-slider';
            switchLabel.append(checkbox, slider);
            checkbox.addEventListener('change', (e) => {
                e.stopPropagation();
                e.target.blur();
                this.onMasterToggleChange(checkbox.checked);
            });

            container.append(iconDiv, switchLabel);
            this.toggle = { container, checkbox, iconDiv };

            const initialState = this.configManager.get('scriptEnabled');
            checkbox.checked = initialState;
            this.updateToggleVisuals(initialState);
            if (this.configManager.get('hideOriginalButton')) {
                document.body.classList.add('ytce-hide-original');
            }

            this.createModal();
            return container;
        }

        createModal() {
            const existingModal = document.getElementById(this.modalId);
            if (existingModal) {
                existingModal.remove();
            }

            const overlay = document.createElement('div');
            overlay.id = this.modalId;
            overlay.className = 'ytce-modal-overlay';
            overlay.onclick = (e) => {
                if (e.target === overlay) this.closeModal();
            };

            const modal = document.createElement('div');
            modal.className = 'ytce-modal';

            // ヘッダー
            const header = document.createElement('div');
            header.className = 'ytce-modal-header';

            const leftGroup = document.createElement('div');
            leftGroup.className = 'ytce-header-left';

            // タイトル
            const title = document.createElement('div');
            title.className = 'ytce-modal-title';
            this.modalElements.title = title;

            // 言語ラベル
            const langLabel = document.createElement('span');
            langLabel.className = 'ytce-lang-label';
            this.modalElements.langLabel = langLabel;

            // 言語選択
            const langSelect = document.createElement('select');
            langSelect.className = 'ytce-lang-select';
            I18n.languages.forEach(lang => {
                const option = document.createElement('option');
                option.value = lang.code;
                option.textContent = lang.label;
                langSelect.appendChild(option);
            });
            langSelect.value = this.configManager.get('userLanguage');

            langSelect.onchange = (e) => {
                this.configManager.set('userLanguage', e.target.value);
                this.updateAllText();
                this.configManager.registerMenu();
                this.expander.log('info', `Language changed to: ${e.target.value}`);
            };

            leftGroup.append(title, langLabel, langSelect);

            const closeBtn = document.createElement('button');
            closeBtn.className = 'ytce-close-btn';
            closeBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></svg>';
            closeBtn.onclick = () => this.closeModal();

            header.append(leftGroup, closeBtn);

            // コンテンツ
            const content = document.createElement('div');
            content.className = 'ytce-modal-content';
            this.modalElements.content = content;

            modal.append(header, content);
            overlay.append(modal);
            document.body.appendChild(overlay);

            this.toggle.modalOverlay = overlay;

            this.renderModalContent();
            this.updateAllText();
        }

        renderModalContent() {
            if (!this.modalElements.content) return;
            this.modalElements.content.innerHTML = '';

            const menuItems = [
                { key: 'expandLongComments', label: I18n.t('optLongComments') },
                { key: 'expandReplies', label: I18n.t('optReplies') },
                { key: 'expandNestedReplies', label: I18n.t('optNestedReplies') },
                { key: 'autoTranslate', label: I18n.t('optAutoTranslate') },
                { key: 'hideOriginalButton', label: I18n.t('optHideOriginal'), indent: true },
                { key: 'sortByNewest', label: I18n.t('optSortByNewest') }
            ];

            menuItems.forEach(item => {
                const row = document.createElement('div');
                row.className = 'ytce-menu-item';
                if (item.indent) {
                    row.style.paddingLeft = '48px';
                    row.style.color = 'var(--yt-spec-text-secondary, #606060)';
                }

                const label = document.createElement('span');
                label.className = 'ytce-menu-label';
                label.textContent = item.label;

                const chk = document.createElement('input');
                chk.type = 'checkbox';
                chk.className = 'ytce-menu-checkbox';
                chk.checked = this.configManager.get(item.key);

                chk.onchange = (e) => {
                    const checked = e.target.checked;
                    this.configManager.set(item.key, checked);
                    if (item.key === 'hideOriginalButton') {
                        document.body.classList.toggle('ytce-hide-original', checked);
                    } else if (checked) {
                        const container = getCurrentCommentsContainer();
                        if (container) {
                            if (item.key === 'sortByNewest') {
                                this.expander.initiateSortSequence(container);
                            } else {
                                this.expander.processExistingNodes(container);
                            }
                        }
                    }
                };

                row.onclick = (e) => {
                    if (e.target !== chk) {
                        chk.checked = !chk.checked;
                        chk.dispatchEvent(new Event('change'));
                    }
                };

                row.append(label, chk);
                this.modalElements.content.append(row);
            });
        }

        updateAllText() {
            if (this.modalElements.title) this.modalElements.title.textContent = I18n.t('modalTitle');
            if (this.modalElements.langLabel) this.modalElements.langLabel.textContent = I18n.t('lblLanguage'); // ラベル更新
            if (this.modalElements.iconDiv) this.modalElements.iconDiv.title = I18n.t('tooltipOpenSettings');
            if (this.modalElements.switchLabel) this.modalElements.switchLabel.title = I18n.t('tooltipToggle');
            this.renderModalContent();
        }

        openModal() {
            if (this.toggle && this.toggle.modalOverlay) {
                this.toggle.modalOverlay.classList.add('visible');
            }
        }

        closeModal() {
            if (this.toggle && this.toggle.modalOverlay) {
                this.toggle.modalOverlay.classList.remove('visible');
            }
        }

        onMasterToggleChange(isEnabled) {
            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.container.classList.toggle('ytce-active', isEnabled);
        }

        observeCommentsHeader(containerSelector, sortMenuSelector, sortMenuLabelSelector, insertMode) {
            // 前の待機処理をキャンセル(重複防止)
            if (this.pendingWait) {
                this.pendingWait.abort();
            }

            // コンテナが出現するのを待機
            this.pendingWait = waitForElement(containerSelector, (container) => {
                this.pendingWait = null;

                // コンテナが見つかったので、ここでExpanderも起動する (Logicの一元化)
                if (this.configManager.get('scriptEnabled')) {
                    this.expander.start(container);
                }

                this.stopUIObserver(); // 既存のUI監視があれば停止

                // UIの注入処理
                const updateUI = () => this.updateCommentsHeaderUI(container, 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(container, sortMenuSelector, sortMenuLabelSelector, insertMode) {
            // Find the Sort Menu WITHIN the specific container
            const sortMenu = container.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.');
                }
            }

            // Hide the label if needed (for Watch page mainly)
            if (sortMenuLabelSelector) {
                const label = container.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',
                '#sort-menu',
                '#sort-menu #icon-label',
                'append'
            );
        }

        initForShortsPage() {
            // Shorts uses engagement panel
            this.observeCommentsHeader(
                'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-comments-section"]',
                'ytd-engagement-panel-title-header-renderer #menu', // Insert relative to the menu container
                null, // No label to hide in shorts typically or selector is different
                'after' // Insert after the #menu div (between Sort and Close button)
            );
        }

        stopUIObserver() {
            if (this.uiObserver) {
                this.uiObserver.disconnect();
                this.uiObserver = null;
                this.expander.log('info', 'UI Observer stopped.');
            }
        }

        stop() {
            // 待機中の検索を中止 (waitForElementのキャンセル)
            if (this.pendingWait) {
                this.pendingWait.abort();
                this.pendingWait = null;
            }

            this.stopUIObserver();

            const modal = document.getElementById(this.modalId);
            if (modal) modal.remove();

            const toggle = document.getElementById(this.toggleContainerId);
            if (toggle) toggle.remove();
        }
    }

    const configManager = new ConfigManager();
    let expander = null;
    let uiManager = null;
    let currentPath = '';
    let initTimer = null; // Timer ID for debounce/cleanup

    function waitForElement(selector, callback, timeout = 15000) {
        let timeoutId = null;
        let observer = null;
        let isAborted = false;

        const abort = () => {
            if (isAborted) return;
            isAborted = true;
            if (timeoutId) clearTimeout(timeoutId);
            if (observer) {
                observer.disconnect();
                observer = null;
            }
        };

        // 即時チェック
        const element = document.querySelector(selector);
        if (element) {
            callback(element);
            return { abort: () => {} };
        }

        observer = new MutationObserver((mutations) => {
            if (isAborted) return;
            const el = document.querySelector(selector);
            if (el) {
                abort();
                callback(el);
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });

        timeoutId = setTimeout(() => {
            if (!isAborted) {
                if (typeof expander !== 'undefined' && expander) {
                    expander.log('warn', `waitForElement timed out for selector: ${selector}`);
                }
                abort();
            }
        }, timeout);

        return { abort };
    }

    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 (initTimer) {
            clearTimeout(initTimer);
            initTimer = null;
        }

        // 既存インスタンスの完全破棄
        if (expander) {
            expander.stop();
            expander = null;
        }
        if (uiManager) {
            uiManager.stop(); // ここで waitForElement も abort される
            uiManager = null;
        }

        expander = new YouTubeCommentExpander(configManager);
        uiManager = new UIManager(configManager, expander);

        configManager.registerMenu();

        initTimer = setTimeout(() => {
            initTimer = null;

            if (location.pathname.startsWith('/shorts/')) {
                expander.log('info', 'Shorts page detected. Initializing...');
                // UIManagerに処理を委譲 (内部で waitForElement -> expander.start を実行)
                uiManager.initForShortsPage();
            } else if (location.pathname.startsWith('/watch')) {
                expander.log('info', 'Watch page detected. Initializing...');
                uiManager.initForWatchPage();
            } else {
                expander.log('info', 'Not a watch/shorts page. Script is idle.');
            }
        }, configManager.get('initialDelay'));
    }

    window.addEventListener('yt-navigate-finish', initializeScript, true);
    initializeScript();

})();