Greasy Fork

Greasy Fork is available in English.

YouTube Cleaner - 隐藏 Shorts、广告与杂讯

隐藏 Shorts、广告、低观看数视频与杂讯,并绕过反广告拦截。20+ 过滤规则,完全可自定义。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube Cleaner - Block Shorts, Ads & Clutter
// @name:zh-TW   YouTube Cleaner - 隱藏 Shorts、廣告與雜訊
// @name:zh-CN   YouTube Cleaner - 隐藏 Shorts、广告与杂讯
// @name:ja      YouTube Cleaner - Shorts・広告・雑音をブロック
// @namespace    http://tampermonkey.net/
// @version      1.6.1
// @description  Hide YouTube Shorts, ads, low-view videos, clutter & bypass anti-adblock. 20+ filter rules, fully customizable.
// @description:zh-TW  隱藏 Shorts、廣告、低觀看數影片與雜訊,並繞過反廣告攔截。20+ 過濾規則,完全可自訂。
// @description:zh-CN  隐藏 Shorts、广告、低观看数视频与杂讯,并绕过反广告拦截。20+ 过滤规则,完全可自定义。
// @description:ja     Shorts、広告、低視聴数動画、雑音を非表示。アンチ広告ブロック回避対応。20以上のフィルタールール。
// @author       Benny & AI Collaborators
// @match        https://www.youtube.com/*
// @exclude      https://www.youtube.com/embed/*
// @grant        GM_info
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @run-at       document-start
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// ==/UserScript==

(function () {
    'use strict';

    /**
     * 🏛️ Architecture Overview (v2.0)
     * 0. SELECTORS: Centralized selector management for easy maintenance.
     * 1. ConfigManager: Unified state management for settings.
     * 2. Utils: Stateless helper functions (parsing, debouncing).
     * 3. Logger: centralized logging wrapper.
     * 4. FilterStats: Statistics tracking for filtered content.
     * 5. StyleManager: Handles CSS injection for high-performance static filtering.
     * 6. AdBlockGuard: Specialized module for anti-adblock popup removal.
     * 7. VideoFilter: The core engine for Dynamic Filtering (View counts, etc).
     * 8. CustomRuleManager: Extensible rule system for easy adding of new text-based filters.
     * 9. InteractionEnhancer: Open in new tab functionality.
     * 10. UIManager: Handles the Tampermonkey menu interface.
     * 11. App: Application entry point and orchestrator.
     */

    // --- 0. Centralized Selectors (Easy maintenance when YouTube updates) ---
    const SELECTORS = {
        // 頂層容器 (用於過濾)
        VIDEO_CONTAINERS: [
            'ytd-rich-item-renderer',
            'ytd-video-renderer',
            'ytd-compact-video-renderer',  // 播放頁側邊欄
            'ytd-grid-video-renderer',
            'yt-lockup-view-model',
            'ytd-compact-radio-renderer',   // 播放頁自動播放清單
            'ytd-playlist-panel-video-renderer'  // 播放清單面板
        ],
        SECTION_CONTAINERS: [
            'ytd-rich-section-renderer',
            'ytd-rich-shelf-renderer',
            'ytd-reel-shelf-renderer',
            'grid-shelf-view-model',
            'ytd-watch-next-secondary-results-renderer'  // 播放頁推薦區塊
        ],

        // Metadata 選擇器 (新舊版相容)
        METADATA: {
            // 觀看數/時間
            TEXT: '.inline-metadata-item, #metadata-line span, .yt-content-metadata-view-model__metadata-text, yt-content-metadata-view-model .yt-core-attributed-string',
            // 標題連結 (用於 aria-label 提取)
            TITLE_LINKS: [
                'a#video-title-link[aria-label]',
                'a#thumbnail[aria-label]',
                'a.yt-lockup-metadata-view-model__title[aria-label]',
                'a[href*="/watch?"][aria-label]'
            ],
            // 時長
            DURATION: 'ytd-thumbnail-overlay-time-status-renderer, span.ytd-thumbnail-overlay-time-status-renderer, badge-shape .yt-badge-shape__text, yt-thumbnail-badge-view-model .yt-badge-shape__text',
            // 頻道名稱
            CHANNEL: 'ytd-channel-name, .ytd-channel-name, a[href^="/@"]',
            // 標題文字
            TITLE: '#video-title, #title, .yt-lockup-metadata-view-model__title, h3'
        },

        // 會員/廣告標記
        BADGES: {
            MEMBERS: '.badge-style-type-members-only, [aria-label*="會員專屬"], [aria-label*="Members only"]',
            AD: '[aria-label*="廣告"], [aria-label*="Sponsor"], ad-badge-view-model, feed-ad-metadata-view-model',
            SHORTS: 'a[href*="/shorts/"]',
            MIX: 'a[aria-label*="合輯"], a[aria-label*="Mix"]'
        },

        // 互動排除
        INTERACTION_EXCLUDE: 'button, yt-icon-button, #menu, ytd-menu-renderer, ytd-toggle-button-renderer, yt-chip-cloud-chip-renderer, .yt-spec-button-shape-next, .yt-core-attributed-string__link, #subscribe-button, .ytp-progress-bar, .ytp-chrome-bottom',

        // 可點擊容器
        CLICKABLE: [
            'ytd-rich-item-renderer', 'ytd-video-renderer', 'ytd-compact-video-renderer',
            'yt-lockup-view-model', 'ytd-playlist-renderer', 'ytd-compact-playlist-renderer',
            'ytd-video-owner-renderer', 'ytd-grid-video-renderer'
        ],

        // 內嵌預覽
        PREVIEW_PLAYER: 'ytd-video-preview',

        // 連結候選
        LINK_CANDIDATES: [
            'a#thumbnail[href*="/watch?"]', 'a#thumbnail[href*="/shorts/"]', 'a#thumbnail[href*="/playlist?"]',
            'a#video-title-link', 'a#video-title', 'a.yt-simple-endpoint#video-title', 'a.yt-lockup-view-model-wiz__title'
        ],

        // 生成組合選擇器
        get allContainers() {
            return [...this.VIDEO_CONTAINERS, ...this.SECTION_CONTAINERS].join(', ');
        },
        get videoContainersStr() {
            return this.VIDEO_CONTAINERS.join(', ');
        }
    };

    // --- 0.1 Filter Statistics ---
    const FilterStats = {
        counts: {},
        session: { total: 0, byRule: {} },

        record(reason) {
            this.counts[reason] = (this.counts[reason] || 0) + 1;
            this.session.total++;
            this.session.byRule[reason] = (this.session.byRule[reason] || 0) + 1;
        },

        getSummary() {
            return `已過濾 ${this.session.total} 個項目\n` +
                Object.entries(this.session.byRule)
                    .sort((a, b) => b[1] - a[1])
                    .map(([k, v]) => `  ${k}: ${v}`)
                    .join('\n');
        },

        reset() {
            this.session = { total: 0, byRule: {} };
        }
    };

    // --- 0.3 Internationalization (i18n) ---
    const I18N = {
        _lang: null,

        // 語言字典
        strings: {
            'zh-TW': {
                title: 'YouTube 淨化大師',
                menu_rules: '📂 設定過濾規則',
                menu_low_view: '低觀看數過濾 (含直播)',
                menu_threshold: '🔢 設定閾值',
                menu_advanced: '🚫 進階過濾',
                menu_new_tab: '強制新分頁',
                menu_debug: 'Debug',
                menu_reset: '🔄 恢復預設',
                menu_stats: '📊 過濾統計',
                menu_export: '💾 匯出/匯入設定',
                menu_lang: '🌐 語言',
                menu_input: '輸入選項:',
                stats_title: '【 過濾統計 】',
                stats_empty: '尚未過濾任何內容',
                stats_filtered: '已過濾 {0} 個項目',
                export_title: '【 設定管理 】',
                export_export: '📤 匯出設定',
                export_import: '📥 匯入設定',
                export_success: '✅ 設定已複製到剪貼簿!',
                export_copy: '請複製以下設定 (Ctrl+C):',
                import_prompt: '請貼上設定 JSON:',
                import_success: '✅ 設定已成功匯入!',
                import_fail: '❌ 匯入失敗: ',
                rules_title: '【 過濾規則 】',
                rules_back: '(0 返回)',
                threshold_prompt: '閾值:',
                reset_confirm: '重設?',
                lang_title: '【 選擇語言 】',
                back: '返回',
                adv_keyword_filter: '關鍵字過濾',
                adv_keyword_list: '✏️ 關鍵字清單',
                adv_channel_filter: '頻道過濾',
                adv_channel_list: '✏️ 頻道清單',
                adv_duration_filter: '長度過濾',
                adv_duration_set: '⏱️ 設定長度',
                adv_min: '最短(分):',
                adv_max: '最長(分):',
                adv_add: '新增',
                adv_remove: '刪除',
                adv_clear: '清空'
            },
            'zh-CN': {
                title: 'YouTube 净化大师',
                menu_rules: '📂 设置过滤规则',
                menu_low_view: '低观看数过滤 (含直播)',
                menu_threshold: '🔢 设置阈值',
                menu_advanced: '🚫 高级过滤',
                menu_new_tab: '强制新标签页',
                menu_debug: 'Debug',
                menu_reset: '🔄 恢复默认',
                menu_stats: '📊 过滤统计',
                menu_export: '💾 导出/导入设置',
                menu_lang: '🌐 语言',
                menu_input: '输入选项:',
                stats_title: '【 过滤统计 】',
                stats_empty: '尚未过滤任何内容',
                stats_filtered: '已过滤 {0} 个项目',
                export_title: '【 设置管理 】',
                export_export: '📤 导出设置',
                export_import: '📥 导入设置',
                export_success: '✅ 设置已复制到剪贴板!',
                export_copy: '请复制以下设置 (Ctrl+C):',
                import_prompt: '请粘贴设置 JSON:',
                import_success: '✅ 设置已成功导入!',
                import_fail: '❌ 导入失败: ',
                rules_title: '【 过滤规则 】',
                rules_back: '(0 返回)',
                threshold_prompt: '阈值:',
                reset_confirm: '重置?',
                lang_title: '【 选择语言 】',
                back: '返回',
                adv_keyword_filter: '关键字过滤',
                adv_keyword_list: '✏️ 关键字列表',
                adv_channel_filter: '频道过滤',
                adv_channel_list: '✏️ 频道列表',
                adv_duration_filter: '时长过滤',
                adv_duration_set: '⏱️ 设置时长',
                adv_min: '最短(分):',
                adv_max: '最长(分):',
                adv_add: '新增',
                adv_remove: '删除',
                adv_clear: '清空'
            },
            'en': {
                title: 'YouTube Cleaner',
                menu_rules: '📂 Filter Rules',
                menu_low_view: 'Low View Filter (incl. Live)',
                menu_threshold: '🔢 Set Threshold',
                menu_advanced: '🚫 Advanced Filters',
                menu_new_tab: 'Force New Tab',
                menu_debug: 'Debug',
                menu_reset: '🔄 Reset to Default',
                menu_stats: '📊 Filter Stats',
                menu_export: '💾 Export/Import Settings',
                menu_lang: '🌐 Language',
                menu_input: 'Enter option:',
                stats_title: '【 Filter Statistics 】',
                stats_empty: 'No content filtered yet',
                stats_filtered: 'Filtered {0} items',
                export_title: '【 Settings Management 】',
                export_export: '📤 Export Settings',
                export_import: '📥 Import Settings',
                export_success: '✅ Settings copied to clipboard!',
                export_copy: 'Copy settings (Ctrl+C):',
                import_prompt: 'Paste settings JSON:',
                import_success: '✅ Settings imported successfully!',
                import_fail: '❌ Import failed: ',
                rules_title: '【 Filter Rules 】',
                rules_back: '(0 Back)',
                threshold_prompt: 'Threshold:',
                reset_confirm: 'Reset?',
                lang_title: '【 Select Language 】',
                back: 'Back',
                adv_keyword_filter: 'Keyword Filter',
                adv_keyword_list: '✏️ Keyword List',
                adv_channel_filter: 'Channel Filter',
                adv_channel_list: '✏️ Channel List',
                adv_duration_filter: 'Duration Filter',
                adv_duration_set: '⏱️ Set Duration',
                adv_min: 'Min (min):',
                adv_max: 'Max (min):',
                adv_add: 'Add',
                adv_remove: 'Remove',
                adv_clear: 'Clear'
            },
            'ja': {
                title: 'YouTube クリーナー',
                menu_rules: '📂 フィルタルール',
                menu_low_view: '低視聴数フィルター (ライブ含む)',
                menu_threshold: '🔢 閾値設定',
                menu_advanced: '🚫 詳細フィルター',
                menu_new_tab: '新しいタブで開く',
                menu_debug: 'デバッグ',
                menu_reset: '🔄 初期化',
                menu_stats: '📊 フィルター統計',
                menu_export: '💾 設定のエクスポート/インポート',
                menu_lang: '🌐 言語',
                menu_input: 'オプションを入力:',
                stats_title: '【 フィルター統計 】',
                stats_empty: 'まだフィルターされたコンテンツはありません',
                stats_filtered: '{0} 件をフィルターしました',
                export_title: '【 設定管理 】',
                export_export: '📤 設定をエクスポート',
                export_import: '📥 設定をインポート',
                export_success: '✅ 設定をクリップボードにコピーしました!',
                export_copy: '設定をコピー (Ctrl+C):',
                import_prompt: '設定JSONを貼り付け:',
                import_success: '✅ 設定をインポートしました!',
                import_fail: '❌ インポート失敗: ',
                rules_title: '【 フィルタールール 】',
                rules_back: '(0 戻る)',
                threshold_prompt: '閾値:',
                reset_confirm: 'リセットしますか?',
                lang_title: '【 言語選択 】',
                back: '戻る',
                adv_keyword_filter: 'キーワードフィルター',
                adv_keyword_list: '✏️ キーワードリスト',
                adv_channel_filter: 'チャンネルフィルター',
                adv_channel_list: '✏️ チャンネルリスト',
                adv_duration_filter: '長さフィルター',
                adv_duration_set: '⏱️ 長さ設定',
                adv_min: '最短(分):',
                adv_max: '最長(分):',
                adv_add: '追加',
                adv_remove: '削除',
                adv_clear: 'クリア'
            }
        },

        // 規則名稱翻譯
        ruleNames: {
            'zh-TW': {
                ad_block_popup: '廣告阻擋彈窗',
                ad_sponsor: '廣告/贊助',
                members_only: '會員專屬',
                shorts_item: 'Shorts 項目',
                mix_only: '合輯',
                premium_banner: 'Premium 橫幅',
                news_block: '新聞區塊',
                shorts_block: 'Shorts 區塊',
                posts_block: '社群貼文',
                playables_block: '可玩內容',
                fundraiser_block: '募款活動',
                shorts_grid_shelf: 'Shorts 網格',
                movies_shelf: '電影推薦',
                youtube_featured_shelf: 'YouTube 精選',
                popular_gaming_shelf: '熱門遊戲',
                more_from_game_shelf: '更多遊戲內容',
                trending_playlist: '熱門播放清單',
                inline_survey: '問卷調查',
                clarify_box: '資訊框',
                explore_topics: '探索主題',
                recommended_playlists: '推薦播放清單',
                members_early_access: '會員搶先看'
            },
            'zh-CN': {
                ad_block_popup: '广告拦截弹窗',
                ad_sponsor: '广告/赞助',
                members_only: '会员专属',
                shorts_item: 'Shorts 项目',
                mix_only: '合辑',
                premium_banner: 'Premium 横幅',
                news_block: '新闻区块',
                shorts_block: 'Shorts 区块',
                posts_block: '社区帖子',
                playables_block: '可玩内容',
                fundraiser_block: '募款活动',
                shorts_grid_shelf: 'Shorts 网格',
                movies_shelf: '电影推荐',
                youtube_featured_shelf: 'YouTube 精选',
                popular_gaming_shelf: '热门游戏',
                more_from_game_shelf: '更多游戏内容',
                trending_playlist: '热门播放列表',
                inline_survey: '问卷调查',
                clarify_box: '信息框',
                explore_topics: '探索主题',
                recommended_playlists: '推荐播放列表',
                members_early_access: '会员抢先看'
            },
            'en': {
                ad_block_popup: 'Ad-block Popup',
                ad_sponsor: 'Ads / Sponsors',
                members_only: 'Members Only',
                shorts_item: 'Shorts Items',
                mix_only: 'Mix Playlists',
                premium_banner: 'Premium Banner',
                news_block: 'News Section',
                shorts_block: 'Shorts Section',
                posts_block: 'Community Posts',
                playables_block: 'Playables',
                fundraiser_block: 'Fundraiser',
                shorts_grid_shelf: 'Shorts Grid',
                movies_shelf: 'Movies Shelf',
                youtube_featured_shelf: 'YouTube Featured',
                popular_gaming_shelf: 'Popular Gaming',
                more_from_game_shelf: 'More from Games',
                trending_playlist: 'Trending Playlist',
                inline_survey: 'Surveys',
                clarify_box: 'Clarify Box',
                explore_topics: 'Explore Topics',
                recommended_playlists: 'Recommended Playlists',
                members_early_access: 'Members Early Access'
            },
            'ja': {
                ad_block_popup: '広告ブロックポップアップ',
                ad_sponsor: '広告/スポンサー',
                members_only: 'メンバー限定',
                shorts_item: 'Shorts アイテム',
                mix_only: 'ミックス',
                premium_banner: 'Premium バナー',
                news_block: 'ニュースセクション',
                shorts_block: 'Shorts セクション',
                posts_block: 'コミュニティ投稿',
                playables_block: 'プレイアブル',
                fundraiser_block: '募金活動',
                shorts_grid_shelf: 'Shorts グリッド',
                movies_shelf: '映画のおすすめ',
                youtube_featured_shelf: 'YouTube おすすめ',
                popular_gaming_shelf: '人気ゲーム',
                more_from_game_shelf: 'ゲーム関連',
                trending_playlist: '急上昇プレイリスト',
                inline_survey: 'アンケート',
                clarify_box: '情報ボックス',
                explore_topics: 'トピックを探す',
                recommended_playlists: 'おすすめプレイリスト',
                members_early_access: 'メンバー先行'
            }
        },

        // 取得規則顯示名稱
        getRuleName(ruleKey) {
            return this.ruleNames[this.lang]?.[ruleKey] || this.ruleNames['en'][ruleKey] || ruleKey;
        },

        // 自動偵測語言
        detectLanguage() {
            const ytLang = document.documentElement.lang || navigator.language || 'zh-TW';
            if (ytLang.startsWith('zh-CN') || ytLang.startsWith('zh-Hans')) return 'zh-CN';
            if (ytLang.startsWith('zh')) return 'zh-TW';
            if (ytLang.startsWith('ja')) return 'ja';
            return 'en';
        },

        get lang() {
            if (!this._lang) {
                this._lang = GM_getValue('ui_language', null) || this.detectLanguage();
            }
            return this._lang;
        },

        set lang(value) {
            this._lang = value;
            GM_setValue('ui_language', value);
        },

        // 取得翻譯字串
        t(key, ...args) {
            const str = this.strings[this.lang]?.[key] || this.strings['en'][key] || key;
            return str.replace(/\{(\d+)\}/g, (_, i) => args[i] ?? '');
        },

        // 語言清單
        get availableLanguages() {
            return {
                'zh-TW': '繁體中文',
                'zh-CN': '简体中文',
                'en': 'English',
                'ja': '日本語'
            };
        }
    };

    // --- 1. Core: Configuration Management ---
    class ConfigManager {
        constructor() {
            this.defaults = {
                LOW_VIEW_THRESHOLD: 1000,
                ENABLE_LOW_VIEW_FILTER: true,
                DEBUG_MODE: false,
                OPEN_IN_NEW_TAB: true,
                ENABLE_KEYWORD_FILTER: false,
                KEYWORD_BLACKLIST: [],
                ENABLE_CHANNEL_FILTER: false,
                CHANNEL_BLACKLIST: [],
                ENABLE_DURATION_FILTER: false,
                DURATION_MIN: 0,
                DURATION_MAX: 0,
                GRACE_PERIOD_HOURS: 4,
                // These connect to simple toggle switches
                RULE_ENABLES: {
                    ad_block_popup: true, ad_sponsor: true, members_only: true, shorts_item: true,
                    mix_only: true, premium_banner: true, news_block: true, shorts_block: true,
                    posts_block: true, playables_block: true, fundraiser_block: true,
                    shorts_grid_shelf: true, movies_shelf: true,
                    youtube_featured_shelf: true, popular_gaming_shelf: true,
                    more_from_game_shelf: true, trending_playlist: true,
                    inline_survey: true, clarify_box: true, explore_topics: true,
                    recommended_playlists: true, members_early_access: true
                }
            };
            this.state = this._load();
        }

        _load() {
            const get = (k, d) => GM_getValue(k, d);
            const snake = str => str.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`);

            const loaded = {};
            for (const key in this.defaults) {
                if (key === 'RULE_ENABLES') {
                    const saved = get('ruleEnables', {});
                    loaded[key] = { ...this.defaults.RULE_ENABLES, ...saved };
                } else {
                    loaded[key] = get(snake(key), this.defaults[key]);
                    if (Array.isArray(this.defaults[key]) && !Array.isArray(loaded[key])) {
                        loaded[key] = [...this.defaults[key]];
                    }
                }
            }
            return loaded;
        }

        get(key) { return this.state[key]; }

        set(key, value) {
            this.state[key] = value;
            const snake = str => str.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`);
            if (key === 'RULE_ENABLES') GM_setValue('ruleEnables', value);
            else GM_setValue(snake(key), value);
        }

        toggleRule(ruleId) {
            this.state.RULE_ENABLES[ruleId] = !this.state.RULE_ENABLES[ruleId];
            this.set('RULE_ENABLES', this.state.RULE_ENABLES);
        }
    }

    // --- 2. Core: Utilities (Enhanced i18n Support) ---
    const Utils = {
        debounce: (func, delay) => {
            let t;
            return (...args) => { clearTimeout(t); t = setTimeout(() => func(...args), delay); };
        },

        // 國際化數字解析 (支援多語言)
        parseNumeric: (text, type = 'any') => {
            if (!text) return null;
            const clean = text.replace(/,/g, '').toLowerCase().trim();

            // 排除時間字串
            if (type === 'view' && /(ago|前|hour|minute|day|week|month|year|秒|分|時|天|週|月|年|時間|전|日|ヶ月|年前)/.test(clean)) return null;

            // 支援各種語言的數字單位
            const match = clean.match(/([\d.]+)\s*([kmb千萬万億亿]|천|만|억|lakh|crore)?/i);
            if (!match) return null;

            let num = parseFloat(match[1]);
            const unit = match[2]?.toLowerCase();
            if (unit) {
                const unitMap = {
                    // 英文
                    'k': 1e3, 'm': 1e6, 'b': 1e9,
                    // 繁體中文
                    '千': 1e3, '萬': 1e4, '億': 1e8,
                    // 簡體中文
                    '万': 1e4, '亿': 1e8,
                    // 日文 (同中文)
                    // 韓文
                    '천': 1e3, '만': 1e4, '억': 1e8,
                    // 印度
                    'lakh': 1e5, 'crore': 1e7
                };
                num *= (unitMap[unit] || 1);
            }
            return Math.floor(num);
        },

        parseDuration: (text) => {
            if (!text) return null;
            const parts = text.trim().split(':').map(Number);
            if (parts.some(isNaN)) return null;
            return parts.length === 3
                ? parts[0] * 3600 + parts[1] * 60 + parts[2]
                : (parts.length === 2 ? parts[0] * 60 + parts[1] : null);
        },

        // 國際化時間解析 (支援多語言)
        parseTimeAgo: (text) => {
            if (!text) return null;
            const raw = text.toLowerCase();

            // 秒
            if (/second|秒|초|วินาที/.test(raw)) return 0;

            const match = raw.match(/(\d+)/);
            if (!match) return null;
            const val = parseInt(match[1], 10);

            // 分鐘
            if (/minute|分鐘|分钟|分|분|นาที/.test(raw)) return val;
            // 小時
            if (/hour|小時|小时|時間|시간|ชั่วโมง/.test(raw)) return val * 60;
            // 天
            if (/day|天|日|일|วัน/.test(raw)) return val * 1440;
            // 週
            if (/week|週|周|주|สัปดาห์/.test(raw)) return val * 10080;
            // 月
            if (/month|月|ヶ月|개월|เดือน/.test(raw)) return val * 43200;
            // 年
            if (/year|年|년|ปี/.test(raw)) return val * 525600;

            return null;
        },

        // 解析直播觀看數 (支援「正在觀看」「觀眾」等關鍵字)
        parseLiveViewers: (text) => {
            if (!text) return null;
            const liveKeywords = /(正在觀看|觀眾|watching|viewers)/i;
            if (!liveKeywords.test(text)) return null;
            return Utils.parseNumeric(text, 'any');
        },

        // 從 aria-label 提取觀看數資訊
        extractAriaTextForCounts: (container) => {
            const a1 = container.querySelector(':scope a#video-title-link[aria-label]');
            if (a1?.ariaLabel) return a1.ariaLabel;
            const a2 = container.querySelector(':scope a#thumbnail[aria-label]');
            if (a2?.ariaLabel) return a2.ariaLabel;
            return '';
        }
    };

    // --- 3. Core: Logger ---
    const Logger = {
        enabled: false,
        prefix: `[Purifier]`,
        info(msg, ...args) { if (this.enabled) console.log(`%c${this.prefix} ${msg}`, 'color:#3498db;font-weight:bold', ...args); },
        warn(msg, ...args) { if (this.enabled) console.warn(`${this.prefix} ${msg}`, ...args); }
    };

    // --- 4. Module: Custom Rule Manager (Extensibility) ---
    /**
     * Designed to make adding new simple text-based rules easy.
     * Add new entries to the `definitions` array here.
     */
    class CustomRuleManager {
        constructor(config) {
            this.config = config;
            // ★ ADD NEW RULES HERE ★
            // Format: { key: 'config_key_name', rules: [/Regex/i, 'String'], type: 'text' (default) }
            this.definitions = [
                // 從 v1.4.0 還原的文字匹配規則 (作為 CSS 的備援)
                { key: 'members_only', rules: [/頻道會員專屬|Members only/i] },
                { key: 'mix_only', rules: [/^(合輯|Mix)[\s\-–]/i] },

                // 區塊/Shelf 類規則
                { key: 'news_block', rules: [/新聞快報|Breaking News|ニュース/i] },
                { key: 'posts_block', rules: [/貼文|Posts|投稿|Publicaciones|最新 YouTube 貼文/i] },
                { key: 'playables_block', rules: [/Playables|遊戲角落/i] },
                { key: 'fundraiser_block', rules: [/Fundraiser|募款/i] },
                { key: 'popular_gaming_shelf', rules: [/熱門遊戲直播/i] },
                { key: 'explore_topics', rules: [/探索更多主題|Explore more topics/i] },
                { key: 'movies_shelf', rules: [/為你推薦的特選電影|featured movies|YouTube 精選/i] },
                { key: 'trending_playlist', rules: [/發燒影片|Trending/i] },
                { key: 'youtube_featured_shelf', rules: [/YouTube 精選/i] },
                { key: 'shorts_block', rules: [/^Shorts$/i] },
                { key: 'shorts_grid_shelf', rules: [/^Shorts$/i] },
                { key: 'more_from_game_shelf', rules: [/^更多此遊戲相關內容$/i] },
                { key: 'members_early_access', rules: [/會員優先|Members Early Access|Early access for members/i] }
            ];
        }

        check(element, textContent) {
            const enables = this.config.get('RULE_ENABLES');
            for (const def of this.definitions) {
                if (enables[def.key]) { // Only check if enabled in config
                    for (const rule of def.rules) {
                        if (rule instanceof RegExp) {
                            if (rule.test(textContent)) return def.key;
                        } else if (textContent.includes(rule)) {
                            return def.key;
                        }
                    }
                }
            }
            return null;
        }
    }

    // --- 5. Module: Style Manager (CSS) ---
    class StyleManager {
        constructor(config) { this.config = config; }

        apply() {
            const rules = [];
            const enables = this.config.get('RULE_ENABLES');

            // 5.1 Global Fixes
            rules.push('body, html { font-family: "YouTube Noto", Roboto, Arial, "PingFang SC", "Microsoft YaHei", sans-serif !important; }');

            // 5.2 Anti-Adblock (完整還原 v1.4.0)
            if (enables.ad_block_popup) {
                rules.push(`
                    tp-yt-paper-dialog:has(ytd-enforcement-message-view-model),
                    ytd-enforcement-message-view-model,
                    #immersive-translate-browser-popup,
                    tp-yt-iron-overlay-backdrop:has(~ tp-yt-paper-dialog ytd-enforcement-message-view-model),
                    tp-yt-iron-overlay-backdrop.opened,
                    yt-playability-error-supported-renderers:has(ytd-enforcement-message-view-model) { display: none !important; }

                    ytd-app:has(ytd-enforcement-message-view-model), body:has(ytd-enforcement-message-view-model), html:has(ytd-enforcement-message-view-model) {
                        overflow: auto !important; overflow-y: auto !important; position: static !important;
                        pointer-events: auto !important; height: auto !important; top: 0 !important;
                        margin-right: 0 !important; overscroll-behavior: auto !important;
                    }

                    ytd-app[aria-hidden="true"]:has(ytd-enforcement-message-view-model) {
                        aria-hidden: false !important; display: block !important;
                    }

                    ytd-app { --ytd-app-scroll-offset: 0 !important; }
                `);
            }

            // 5.3 Simple Selection (CSS)
            // ★ Add new Selector-based rules here
            const map = {
                ad_sponsor: [
                    'ytd-ad-slot-renderer',
                    'ytd-promoted-sparkles-text-search-renderer',
                    '#masthead-ad',
                    'ytd-rich-item-renderer:has(.ytd-ad-slot-renderer)',
                    'feed-ad-metadata-view-model',
                    'ad-badge-view-model'
                ],
                premium_banner: ['ytd-statement-banner-renderer', 'ytd-rich-section-renderer:has(ytd-statement-banner-renderer)'],
                clarify_box: ['ytd-info-panel-container-renderer'],
                inline_survey: ['ytd-rich-section-renderer:has(ytd-inline-survey-renderer)'],
                playables_block: ['ytd-rich-section-renderer:has(ytd-rich-shelf-renderer[is-playables])', 'ytd-game-card-renderer']
            };

            for (const [key, selectors] of Object.entries(map)) {
                if (enables[key]) rules.push(`${selectors.join(', ')} { display: none !important; }`);
            }

            // 5.4 Advanced :has() Rules
            // ★ Add new Container rules here
            const VIDEO_CONTAINERS = 'ytd-rich-item-renderer, ytd-video-renderer, ytd-compact-video-renderer, ytd-grid-video-renderer, yt-lockup-view-model';
            const hasRules = [
                { key: 'ad_sponsor', selector: '[aria-label*="廣告"], [aria-label*="Sponsor"], [aria-label="贊助商廣告"], ad-badge-view-model, feed-ad-metadata-view-model' },
                { key: 'members_only', selector: '[aria-label*="會員專屬"]' },
                { key: 'shorts_item', selector: 'a[href*="/shorts/"]' },
                { key: 'mix_only', selector: 'a[aria-label*="合輯"], a[aria-label*="Mix"]' }
            ];

            hasRules.forEach(({ key, selector }) => {
                if (enables[key]) {
                    const containers = VIDEO_CONTAINERS.split(',').map(s => s.trim());
                    containers.forEach(c => rules.push(`${c}:has(${selector}) { display: none !important; }`));
                }
            });

            // 5.5 首頁推薦播放清單 (不影響頻道頁面)
            if (enables.recommended_playlists) {
                rules.push(`
                    ytd-browse[page-subtype="home"] ytd-rich-item-renderer:has(a[href^="/playlist?list="]),
                    ytd-browse[page-subtype="home"] ytd-rich-item-renderer:has([content-id^="PL"]) { display: none !important; }
                `);
            }

            GM_addStyle(rules.join('\n'));
            Logger.info('Static CSS rules injected');
        }
    }

    // --- 6. Module: AdBlock Guard (Enhanced with Whitelist) ---
    class AdBlockGuard {
        constructor() {
            // 多語言關鍵字偵測
            this.keywords = [
                'Ad blockers', '廣告攔截器', '广告拦截器', '広告ブロッカー', '광고 차단기',
                'Video player will be blocked', '影片播放器將被封鎖', '视频播放器将被封锁',
                'Allow YouTube', '允許 YouTube', '允许 YouTube',
                'You have an ad blocker', '您使用了廣告攔截器',
                'YouTube 禁止使用廣告攔截器', "YouTube doesn't allow ad blockers"
            ];
            // 白名單選擇器 - 這些對話框絕不是廣告警告
            this.whitelistSelectors = [
                'ytd-sponsorships-offer-renderer',   // 會員加入視窗
                'ytd-about-channel-renderer',         // 頻道資訊視窗
                'ytd-report-form-modal-renderer',     // 檢舉視窗
                'ytd-multi-page-menu-renderer',       // 通用選單
                'ytd-playlist-add-to-option-renderer' // 加入播放清單視窗
            ];
            this.lastTrigger = 0;
        }

        start() {
            const beat = () => {
                this.checkAndClean();
                setTimeout(() => requestAnimationFrame(beat), 800);
            };
            beat();
        }

        isWhitelisted(dialog) {
            for (const sel of this.whitelistSelectors) {
                if (dialog.querySelector(sel)) {
                    Logger.info(`✅ Whitelist dialog detected: ${sel}`);
                    return true;
                }
            }
            return false;
        }

        isAdBlockPopup(dialog) {
            // ytd-enforcement-message-view-model 是廣告攔截專屬標籤,直接判定
            if (dialog.tagName === 'YTD-ENFORCEMENT-MESSAGE-VIEW-MODEL') {
                return true;
            }
            // 檢查是否包含廣告攔截專屬標籤
            if (dialog.querySelector('ytd-enforcement-message-view-model')) {
                return true;
            }
            // 深度關鍵字檢查
            if (dialog.innerText && this.keywords.some(k => dialog.innerText.includes(k))) {
                return true;
            }
            return false;
        }

        checkAndClean() {
            // 更積極的彈窗選擇器
            const popupSelectors = [
                'tp-yt-paper-dialog',
                'ytd-enforcement-message-view-model',
                'yt-playability-error-supported-renderers',
                'ytd-popup-container tp-yt-paper-dialog',
                '[role="dialog"]:has(ytd-enforcement-message-view-model)'
            ];

            const dialogs = document.querySelectorAll(popupSelectors.join(', '));
            let detected = false;

            for (const dialog of dialogs) {
                // ★ 白名單優先檢查 - 避免誤殺會員視窗等
                if (this.isWhitelisted(dialog)) continue;

                if (this.isAdBlockPopup(dialog)) {
                    // 嘗試點擊關閉按鈕
                    const dismissBtns = dialog.querySelectorAll('[aria-label="Close"], #dismiss-button, [aria-label="可能有風險"], .yt-spec-button-shape-next--call-to-action');
                    dismissBtns.forEach(btn => btn.click());

                    dialog.remove();
                    detected = true;
                    Logger.info(`🚫 Removed AdBlock Popup: ${dialog.tagName}`);
                }
            }

            if (detected) {
                // 移除背景遮罩 (包含所有可能的遮罩)
                document.querySelectorAll('tp-yt-iron-overlay-backdrop, .ytd-popup-container, [style*="z-index: 9999"]').forEach(b => {
                    if (b.classList.contains('opened') || b.style.display !== 'none') {
                        b.style.display = 'none';
                        b.remove();
                    }
                });
                this.unlockScroll();
                this.resumeVideo();
            }
        }

        unlockScroll() {
            const css = (el, props) => {
                if (!el) return;
                for (const [key, val] of Object.entries(props)) {
                    el.style.setProperty(key, val, 'important');
                }
            };

            const allowScrollProps = {
                'overflow-y': 'auto',
                'overflow-x': 'hidden',
                'position': 'static',
                'pointer-events': 'auto',
                'top': 'auto',
                'display': 'block'
            };

            css(document.body, allowScrollProps);
            css(document.documentElement, allowScrollProps);

            const ytdApp = document.querySelector('ytd-app');
            if (ytdApp) {
                css(ytdApp, allowScrollProps);
                ytdApp.removeAttribute('aria-hidden');
            }

            // 移除播放器模糊效果
            const watchPage = document.querySelector('ytd-watch-flexy');
            if (watchPage) {
                watchPage.style.removeProperty('filter');
            }
        }

        resumeVideo() {
            // 只有剛偵測到彈窗時才強制播放,避免過度積極
            if (Date.now() - this.lastTrigger > 3000) {
                this.lastTrigger = Date.now();
                const video = document.querySelector('video');
                if (video && video.paused && !video.ended) {
                    video.play().catch(() => { });
                }
            }
        }
    }

    // --- 7. Module: Video Filter (Lazy Evaluator) ---
    class LazyVideoData {
        constructor(element) {
            this.el = element;
            this._title = null;
            this._channel = null;
            this._viewCount = undefined;
            this._liveViewers = undefined;
            this._timeAgo = undefined;
            this._duration = undefined;
        }

        get title() {
            if (this._title === null) this._title = this.el.querySelector(SELECTORS.METADATA.TITLE)?.textContent?.trim() || '';
            return this._title;
        }
        get channel() {
            if (this._channel === null) this._channel = this.el.querySelector(SELECTORS.METADATA.CHANNEL)?.textContent?.trim() || '';
            return this._channel;
        }
        _parseMetadata() {
            if (this._viewCount !== undefined) return;

            // 使用集中管理的選擇器
            const texts = Array.from(this.el.querySelectorAll(SELECTORS.METADATA.TEXT));

            // 嘗試從 aria-label 提取
            let aria = '';
            for (const sel of SELECTORS.METADATA.TITLE_LINKS) {
                const el = this.el.querySelector(`:scope ${sel}`);
                if (el?.ariaLabel) { aria = el.ariaLabel; break; }
            }

            if (texts.length === 0 && aria) {
                this._viewCount = Utils.parseNumeric(aria, 'view');
                this._liveViewers = Utils.parseLiveViewers(aria);
                this._timeAgo = Utils.parseTimeAgo(aria);
                return;
            }

            this._viewCount = null;
            this._liveViewers = null;
            this._timeAgo = null;

            for (const t of texts) {
                const text = t.textContent;
                // 直播觀看數優先檢查
                if (this._liveViewers === null) this._liveViewers = Utils.parseLiveViewers(text);
                // 一般觀看數
                if (this._viewCount === null && /view|觀看|次/i.test(text)) this._viewCount = Utils.parseNumeric(text, 'view');
                // 時間
                if (this._timeAgo === null && /ago|前/i.test(text)) this._timeAgo = Utils.parseTimeAgo(text);
            }
        }
        get viewCount() { this._parseMetadata(); return this._viewCount; }
        get liveViewers() { this._parseMetadata(); return this._liveViewers; }
        get timeAgo() { this._parseMetadata(); return this._timeAgo; }
        get duration() {
            if (this._duration === undefined) {
                const el = this.el.querySelector(SELECTORS.METADATA.DURATION);
                this._duration = el ? Utils.parseDuration(el.textContent) : null;
            }
            return this._duration;
        }
        get isShorts() { return !!this.el.querySelector(SELECTORS.BADGES.SHORTS); }
        get isLive() { return this._liveViewers !== null; }
        get isMembers() {
            return this.el.querySelector(SELECTORS.BADGES.MEMBERS) ||
                this.el.innerText.includes('會員專屬') ||
                this.el.innerText.includes('Members only');
        }
    }

    class VideoFilter {
        constructor(config) {
            this.config = config;
            this.customRules = new CustomRuleManager(config);
        }
        // 使用 requestIdleCallback 分批處理以優化效能
        processPage() {
            const elements = Array.from(document.querySelectorAll(SELECTORS.allContainers));
            const unprocessed = elements.filter(el => !el.dataset.ypChecked);

            if (unprocessed.length === 0) return;

            // 如果瀏覽器支援 requestIdleCallback,使用分批處理
            if ('requestIdleCallback' in window) {
                this._processBatch(unprocessed, 0);
            } else {
                // Fallback: 直接處理
                for (const el of unprocessed) this.processElement(el);
            }
        }

        _processBatch(elements, startIndex, batchSize = 20) {
            requestIdleCallback((deadline) => {
                let i = startIndex;
                // 在空閒時間內處理盡可能多的元素
                while (i < elements.length && (deadline.timeRemaining() > 0 || deadline.didTimeout)) {
                    this.processElement(elements[i]);
                    i++;
                    // 每批最多處理 batchSize 個
                    if (i - startIndex >= batchSize) break;
                }
                // 如果還有未處理的元素,繼續排程
                if (i < elements.length) {
                    this._processBatch(elements, i, batchSize);
                }
            }, { timeout: 500 }); // 500ms 超時保證
        }

        processElement(element) {
            if (element.dataset.ypChecked) return;
            if (element.offsetParent === null) return;

            // 7.2 Custom Text Rules Check (Extensible)
            const textRule = this.customRules.check(element, element.innerText);
            if (textRule) return this._hide(element, textRule);

            // 7.3 Base Logic
            if (element.tagName.includes('VIDEO') || element.tagName.includes('LOCKUP') || element.tagName.includes('RICH-ITEM')) {
                const item = new LazyVideoData(element);

                // Advanced Filters
                // Advanced Filters
                if (this.config.get('ENABLE_KEYWORD_FILTER') && item.title) {
                    if (this.config.get('KEYWORD_BLACKLIST').some(k => item.title.toLowerCase().includes(k.toLowerCase()))) return this._hide(element, 'keyword_blacklist');
                }
                if (this.config.get('ENABLE_CHANNEL_FILTER') && item.channel) {
                    if (this.config.get('CHANNEL_BLACKLIST').some(k => item.channel.toLowerCase().includes(k.toLowerCase()))) return this._hide(element, 'channel_blacklist');
                }

                // 強化會員過濾 (JS補刀):若開啟成員過濾且偵測到是會員影片,直接隱藏
                if (this.config.get('RULE_ENABLES').members_only && item.isMembers) {
                    return this._hide(element, 'members_only_js');
                }

                if (this.config.get('ENABLE_LOW_VIEW_FILTER') && !item.isShorts) {
                    const th = this.config.get('LOW_VIEW_THRESHOLD');
                    const grace = this.config.get('GRACE_PERIOD_HOURS') * 60;

                    // 直播觀看數過濾 (不受豁免期限制)
                    if (item.isLive && item.liveViewers !== null && item.liveViewers < th) {
                        return this._hide(element, 'low_viewer_live');
                    }

                    // 一般影片觀看數過濾 (受豁免期限制)
                    if (!item.isLive && item.viewCount !== null && item.timeAgo !== null && item.timeAgo > grace && item.viewCount < th) {
                        return this._hide(element, 'low_view');
                    }
                }

                if (this.config.get('ENABLE_DURATION_FILTER') && !item.isShorts && item.duration !== null) {
                    const min = this.config.get('DURATION_MIN');
                    const max = this.config.get('DURATION_MAX');
                    if ((min > 0 && item.duration < min) || (max > 0 && item.duration > max)) return this._hide(element, 'duration_filter');
                }
            }
            element.dataset.ypChecked = 'true';
        }

        _hide(element, reason) {
            element.style.display = 'none';
            element.dataset.ypHidden = reason;
            FilterStats.record(reason);  // 記錄統計
            Logger.info(`Hidden [${reason}]`, element);
        }

        reset() {
            document.querySelectorAll('[data-yp-hidden]').forEach(el => {
                el.style.display = '';
                delete el.dataset.ypHidden;
                delete el.dataset.ypChecked;
            });
            FilterStats.reset();  // 重設統計
        }
    }

    // --- 8. Module: Interaction Enhancer (使用集中選擇器) ---
    class InteractionEnhancer {
        constructor(config) {
            this.config = config;
        }

        findPrimaryLink(container) {
            if (!container) return null;
            for (const sel of SELECTORS.LINK_CANDIDATES) {
                const a = container.querySelector(sel);
                if (a?.href) return a;
            }
            return container.querySelector('a[href*="/watch?"], a[href*="/shorts/"], a[href*="/playlist?"]');
        }

        init() {
            document.addEventListener('click', (e) => {
                if (!this.config.get('OPEN_IN_NEW_TAB')) return;
                if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;

                // 使用集中管理的排除清單
                if (e.target.closest(SELECTORS.INTERACTION_EXCLUDE)) return;

                let targetLink = null;
                const previewPlayer = e.target.closest(SELECTORS.PREVIEW_PLAYER);

                if (previewPlayer) {
                    targetLink = this.findPrimaryLink(previewPlayer) || this.findPrimaryLink(previewPlayer.closest(SELECTORS.CLICKABLE.join(',')));
                } else {
                    const container = e.target.closest(SELECTORS.CLICKABLE.join(', '));
                    if (!container) return;

                    // 頻道連結處理
                    const channelLink = e.target.closest('a#avatar-link, .ytd-channel-name a, a[href^="/@"], a[href^="/channel/"]');
                    targetLink = channelLink?.href ? channelLink : this.findPrimaryLink(container);
                }

                if (!targetLink) return;

                try {
                    const hostname = new URL(targetLink.href, location.origin).hostname;
                    const isValidTarget = targetLink.href && /(^|\.)youtube\.com$/.test(hostname);
                    if (isValidTarget) {
                        e.preventDefault();
                        e.stopImmediatePropagation();
                        window.open(targetLink.href, '_blank');
                    }
                } catch (err) { }
            }, { capture: true });
        }
    }

    // --- 9. Module: UI Manager (Enhanced with i18n) ---
    class UIManager {
        constructor(config, onRefresh) { this.config = config; this.onRefresh = onRefresh; }

        t(key, ...args) { return I18N.t(key, ...args); }

        showMainMenu() {
            const i = (k) => this.config.get(k) ? '✅' : '❌';
            const statsInfo = FilterStats.session.total > 0 ? ` (${FilterStats.session.total})` : '';
            const langName = I18N.availableLanguages[I18N.lang];
            const choice = prompt(
                `【 ${this.t('title')} v1.6.0 】\n\n` +
                `1. ${this.t('menu_rules')}\n` +
                `2. ${i('ENABLE_LOW_VIEW_FILTER')} ${this.t('menu_low_view')}\n` +
                `3. ${this.t('menu_threshold')} (${this.config.get('LOW_VIEW_THRESHOLD')})\n` +
                `4. ${this.t('menu_advanced')}\n` +
                `5. ${i('OPEN_IN_NEW_TAB')} ${this.t('menu_new_tab')}\n` +
                `6. ${i('DEBUG_MODE')} ${this.t('menu_debug')}\n` +
                `7. ${this.t('menu_reset')}\n` +
                `8. ${this.t('menu_stats')}${statsInfo}\n` +
                `9. ${this.t('menu_export')}\n` +
                `10. ${this.t('menu_lang')} [${langName}]\n\n` +
                this.t('menu_input')
            );
            if (choice) this.handleMenu(choice);
        }
        handleMenu(c) {
            switch (c.trim()) {
                case '1': this.showRuleMenu(); break;
                case '2': this.toggle('ENABLE_LOW_VIEW_FILTER'); break;
                case '3': const v = prompt(this.t('threshold_prompt')); if (v) this.update('LOW_VIEW_THRESHOLD', Number(v)); break;
                case '4': this.showAdvancedMenu(); break;
                case '5': this.toggle('OPEN_IN_NEW_TAB'); break;
                case '6': this.toggle('DEBUG_MODE'); break;
                case '7': if (confirm(this.t('reset_confirm'))) { Object.keys(this.config.defaults).forEach(k => this.config.set(k, this.config.defaults[k])); this.update('', null); } break;
                case '8': this.showStats(); break;
                case '9': this.showExportImportMenu(); break;
                case '10': this.showLanguageMenu(); break;
            }
        }
        showStats() {
            const summary = FilterStats.getSummary();
            alert(`${this.t('stats_title')}\n\n${summary || this.t('stats_empty')}`);
            this.showMainMenu();
        }
        showLanguageMenu() {
            const langs = I18N.availableLanguages;
            const keys = Object.keys(langs);
            const current = I18N.lang;
            const menu = keys.map((k, i) => `${i + 1}. ${k === current ? '✅' : '⬜'} ${langs[k]}`).join('\n');
            const c = prompt(`${this.t('lang_title')}\n\n${menu}\n\n0. ${this.t('back')}`);
            if (c && c !== '0') {
                const idx = parseInt(c) - 1;
                if (keys[idx]) {
                    I18N.lang = keys[idx];
                    alert(`✅ ${langs[keys[idx]]}`);
                }
            }
            this.showMainMenu();
        }
        showExportImportMenu() {
            const c = prompt(`${this.t('export_title')}\n\n1. ${this.t('export_export')}\n2. ${this.t('export_import')}\n0. ${this.t('back')}`);
            if (c === '1') this.exportSettings();
            else if (c === '2') this.importSettings();
            else if (c === '0') this.showMainMenu();
        }
        exportSettings() {
            const exportData = {
                version: '1.6.0',
                timestamp: new Date().toISOString(),
                settings: this.config.state,
                language: I18N.lang
            };
            const json = JSON.stringify(exportData, null, 2);

            navigator.clipboard.writeText(json).then(() => {
                alert(this.t('export_success'));
            }).catch(() => {
                prompt(this.t('export_copy'), json);
            });
            this.showExportImportMenu();
        }
        importSettings() {
            const json = prompt(this.t('import_prompt'));
            if (!json) { this.showExportImportMenu(); return; }

            try {
                const data = JSON.parse(json);
                if (!data.settings) throw new Error('Invalid format');

                for (const key in data.settings) {
                    if (key in this.config.defaults) {
                        this.config.set(key, data.settings[key]);
                    }
                }
                if (data.language) I18N.lang = data.language;
                alert(this.t('import_success'));
                this.onRefresh();
            } catch (e) {
                alert(this.t('import_fail') + e.message);
            }
            this.showExportImportMenu();
        }
        showRuleMenu() {
            const r = this.config.get('RULE_ENABLES'); const k = Object.keys(r);
            const c = prompt(`${this.t('rules_title')} ${this.t('rules_back')}\n` + k.map((key, i) => `${i + 1}. [${r[key] ? '✅' : '❌'}] ${I18N.getRuleName(key)}`).join('\n'));
            if (c && c !== '0') { this.config.toggleRule(k[parseInt(c) - 1]); this.onRefresh(); this.showRuleMenu(); } else if (c === '0') this.showMainMenu();
        }
        showAdvancedMenu() {
            const i = (k) => this.config.get(k) ? '✅' : '❌';
            const c = prompt(
                `1. ${i('ENABLE_KEYWORD_FILTER')} ${this.t('adv_keyword_filter')}\n` +
                `2. ${this.t('adv_keyword_list')}\n` +
                `3. ${i('ENABLE_CHANNEL_FILTER')} ${this.t('adv_channel_filter')}\n` +
                `4. ${this.t('adv_channel_list')}\n` +
                `5. ${i('ENABLE_DURATION_FILTER')} ${this.t('adv_duration_filter')}\n` +
                `6. ${this.t('adv_duration_set')}\n` +
                `0. ${this.t('back')}`
            );
            if (c === '1' || c === '3' || c === '5') this.toggle(c === '1' ? 'ENABLE_KEYWORD_FILTER' : c === '3' ? 'ENABLE_CHANNEL_FILTER' : 'ENABLE_DURATION_FILTER', true);
            else if (c === '2') this.manage('KEYWORD_BLACKLIST', this.t('adv_keyword_filter'));
            else if (c === '4') this.manage('CHANNEL_BLACKLIST', this.t('adv_channel_filter'));
            else if (c === '6') {
                const min = prompt(this.t('adv_min')); const max = prompt(this.t('adv_max'));
                if (min) this.config.set('DURATION_MIN', min * 60);
                if (max) this.config.set('DURATION_MAX', max * 60);
                this.onRefresh(); this.showAdvancedMenu();
            } else if (c === '0') this.showMainMenu();
        }
        manage(k, n) {
            const l = this.config.get(k);
            const c = prompt(`[${l.join(', ')}]\n1.${this.t('adv_add')} 2.${this.t('adv_remove')} 3.${this.t('adv_clear')} 0.${this.t('back')}`);
            if (c === '1') { const v = prompt(`${this.t('adv_add')}:`); if (v) this.config.set(k, [...l, ...v.split(',')]); }
            if (c === '2') { const v = prompt(`${this.t('adv_remove')}:`); if (v) this.config.set(k, l.filter(i => i !== v)); }
            if (c === '3') this.config.set(k, []);
            this.onRefresh(); this.showAdvancedMenu();
        }
        toggle(k, adv) { this.config.set(k, !this.config.get(k)); this.onRefresh(); adv ? this.showAdvancedMenu() : this.showMainMenu(); }
        update(k, v) { if (k) this.config.set(k, v); this.onRefresh(); this.showMainMenu(); }
    }

    // --- 10. App Entry ---
    class App {
        constructor() {
            this.config = new ConfigManager();
            this.styleManager = new StyleManager(this.config);
            this.adGuard = new AdBlockGuard();
            this.filter = new VideoFilter(this.config);
            this.enhancer = new InteractionEnhancer(this.config);
            this.ui = new UIManager(this.config, () => this.refresh());
        }

        // **ANTI-ADBLOCK PATCH**: 透過 YouTube 自身的配置對象來阻止偵測
        patchYouTubeConfig() {
            try {
                const config = window.yt?.config_ || window.ytcfg?.data_;
                if (config?.openPopupConfig?.supportedPopups?.adBlockMessageViewModel) {
                    config.openPopupConfig.supportedPopups.adBlockMessageViewModel = false;
                }
                if (config?.EXPERIMENT_FLAGS) {
                    config.EXPERIMENT_FLAGS.ad_blocker_notifications_disabled = true;
                    config.EXPERIMENT_FLAGS.web_enable_adblock_detection_block_playback = false;
                }
            } catch (e) {
                // 忽略錯誤
            }
        }

        init() {
            Logger.enabled = this.config.get('DEBUG_MODE');

            // 先嘗試 patch YouTube 配置
            this.patchYouTubeConfig();

            this.styleManager.apply();
            this.adGuard.start();
            this.enhancer.init();
            GM_registerMenuCommand('⚙️ 淨化大師設定', () => this.ui.showMainMenu());

            const obs = new MutationObserver(Utils.debounce(() => this.filter.processPage(), 100));
            obs.observe(document.body, { childList: true, subtree: true });

            window.addEventListener('yt-navigate-finish', () => {
                this.patchYouTubeConfig(); // 每次導航後重新 patch
                this.filter.processPage();
                this.adGuard.checkAndClean();
            });

            this.filter.processPage();
            Logger.info(`🚀 YouTube 淨化大師 v1.6.0 啟動`);
        }

        refresh() {
            Logger.enabled = this.config.get('DEBUG_MODE');
            this.filter.reset();
            this.styleManager.apply();
            this.filter.processPage();
        }
    }

    // 防止腳本重複初始化
    if (window.ytPurifierInitialized) return;
    window.ytPurifierInitialized = true;

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => new App().init());
    else new App().init();

})();