B站推荐过滤器

Bilibili首页推荐过滤器:智能屏蔽广告、分类、直播和自定义关键词,支持自适应持续屏蔽、拖拽控制面板、暗黑模式切换,以及修复屏蔽后页面留白问题。优化UI交互,提升浏览体验。

// ==UserScript==
// @name         B站推荐过滤器
// @namespace    http://tampermonkey.net/
// @homepageURL  https://github.com/StarsWhere/Bilibili-Video-Filter
// @version      7.0.0
// @description  Bilibili首页推荐过滤器:智能屏蔽广告、分类、直播和自定义关键词,支持自适应持续屏蔽、拖拽控制面板、暗黑模式切换,以及修复屏蔽后页面留白问题。优化UI交互,提升浏览体验。
// @author       StarsWhere
// @match        *://www.bilibili.com/*
// @exclude      *://www.bilibili.com/video/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';
    // ===== 工具函数 =====
    const debounce = (func, wait) => {
        let timeout;
        return (...args) => {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    };
    const throttle = (func, limit) => {
        let inThrottle;
        return (...args) => {
            if (!inThrottle) {
                func.apply(this, args);
                inThrottle = true;
                setTimeout(() => inThrottle = false, limit);
            }
        };
    };
    const safeExecute = (func, errorMessage) => {
        try {
            return func();
        } catch (error) {
            console.warn(`[B站过滤插件] ${errorMessage}:`, error);
            return 0;
        }
    };
    // ===== 选择器常量 =====
    const SELECTORS = {
        CARD_CONTAINERS: '.feed-card, .bili-video-card, .bili-feed-card',
        LIVE_CARDS: '.bili-live-card, .floor-single-card',
        AD_INDICATORS: '.bili-ad, [ad-id], .ad-report, [data-report*="ad"], .bili-video-card__stats--text:is(:has-text("广告"), :has-text("推广"))',
        VIDEO_TITLES: '.bili-video-card__info--tit, .bili-video-card__info--title',
        VIDEO_AUTHORS: '.bili-video-card__info--author, .bili-video-card__info--owner',
        CATEGORY_TITLES: '.floor-title, .bili-grid-floor-header__title',
        LIVE_INDICATORS: '.bili-video-card__stats--item[title*="直播"], .live-tag, .bili-live-card__info--text, .recommend-card__live-status',
        CARD_SELECTORS: ['.feed-card', '.bili-video-card', '.bili-live-card', '.bili-feed-card', '.floor-single-card']
    };
    // ===== 配置管理器 =====
    class ConfigManager {
        constructor() {
            this.config = {};
            this.loadConfig();
        }
        loadConfig() {
            this.config = {
                video: {
                    enabled: GM_getValue('video.enabled', true),
                    blacklist: GM_getValue('video.blacklist', [])
                },
                category: {
                    enabled: GM_getValue('category.enabled', true),
                    blacklist: GM_getValue('category.blacklist', ['番剧', '直播', '国创', '综艺', '课堂', '电影', '电视剧', '纪录片', '漫画'])
                },
                ad: GM_getValue('ad', true),
                live: GM_getValue('live', true),
                continuousBlock: GM_getValue('continuousBlock', false),
                floatBtnPosition: GM_getValue('floatBtnPosition', { x: 30, y: 100 }),
                darkMode: GM_getValue('darkMode', false)
            };
        }
        get(path) {
            return path.split('.').reduce((acc, key) => acc?.[key], this.config);
        }
        setValue(key, value) {
            GM_setValue(key, value);
            const keys = key.split('.');
            let current = this.config;
            for (let i = 0; i < keys.length - 1; i++) {
                if (!current[keys[i]]) current[keys[i]] = {};
                current = current[keys[i]];
            }
            current[keys[keys.length - 1]] = value;
        }
        reset() {
            const keysToDelete = [
                'video.enabled', 'video.blacklist',
                'category.enabled', 'category.blacklist',
                'ad', 'live', 'continuousBlock', 'floatBtnPosition', 'darkMode',
                'videoEnabled', 'videoBlacklist', 'categoryEnabled', 'categoryBlacklist',
                'adEnabled', 'liveEnabled'
            ];
            keysToDelete.forEach(key => GM_setValue(key, undefined));
        }
    }
    const configManager = new ConfigManager();

    // ===== 自适应屏蔽器 =====
    class AdaptiveBlocker {
        constructor() {
            this.baseInterval = 2000;
            this.maxInterval = 8000;
            this.minInterval = 500;
            this.currentInterval = this.baseInterval;
            this.lastBlockCount = 0;
            this.intervalId = null;
            this.isRunning = false;
        }
        adjustInterval(currentBlockCount) {
            if (currentBlockCount > this.lastBlockCount) {
                this.currentInterval = Math.max(this.minInterval, this.currentInterval * 0.7);
            } else {
                this.currentInterval = Math.min(this.maxInterval, this.currentInterval * 1.3);
            }
            this.lastBlockCount = currentBlockCount;
        }
        start() {
            if (this.isRunning) return;
            this.isRunning = true;
            const execute = () => {
                if (!this.isRunning) return;
                const blockedCount = runAllBlockers();
                this.adjustInterval(blockedCount);
                this.intervalId = setTimeout(execute, this.currentInterval);
            };
            execute();
        }
        stop() {
            this.isRunning = false;
            clearTimeout(this.intervalId);
            this.intervalId = null;
        }
    }
    const adaptiveBlocker = new AdaptiveBlocker();
    // ===== 状态指示器 =====
    function showBlockingStatus(count = 0) {
        let indicator = document.querySelector('.block-status-indicator');
        if (!indicator) {
            const indicatorContainer = document.createElement('div');
            indicatorContainer.innerHTML = `<style>.block-status-indicator{position:fixed;top:20px;right:20px;background:rgba(0,161,214,0.9);color:#fff;padding:8px 12px;border-radius:6px;font-size:12px;z-index:9998;opacity:0;transition:opacity .3s;pointer-events:none}.block-status-indicator.show{opacity:1}</style><div class="block-status-indicator"></div>`;
            document.head.appendChild(indicatorContainer.querySelector('style'));
            indicator = indicatorContainer.querySelector('.block-status-indicator');
            document.body.appendChild(indicator);
        }
        if (count > 0) indicator.textContent = `🛡️ 已屏蔽 ${count} 项`;
        indicator.classList.add('show');
        setTimeout(() => indicator.classList.remove('show'), 1500);
    }
    // ===== 核心屏蔽功能 =====
    function removeCardElement(element) {
        if (!element || element.dataset.blocked) return false;
        const card = element.closest(SELECTORS.CARD_SELECTORS.join(', '));
        if (card && !card.dataset.blocked) {
            card.dataset.blocked = 'true';
            card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
            card.style.opacity = '0';
            card.style.transform = 'scale(0.95)';
            setTimeout(() => card.remove(), 300);
            return true;
        }
        return false;
    }
    function blockAds() {
        if (!configManager.get('ad')) return 0;
        let blockedCount = 0;
        document.querySelectorAll(SELECTORS.AD_INDICATORS).forEach(indicator => {
            if (removeCardElement(indicator)) blockedCount++;
        });
        const recommendItem = document.querySelector(".recommended-swipe");
        if (recommendItem) {
            recommendItem.remove();
            blockedCount++;
        }
        return blockedCount;
    }
    function blockCategories() {
        if (!configManager.get('category.enabled')) return 0;
        let blockedCount = 0;
        const blacklist = configManager.get('category.blacklist') || [];
        document.querySelectorAll('.floor-single-card, .bili-grid-floor-header').forEach(card => {
            if (card.dataset.blocked) return;
            const categoryElement = card.querySelector(SELECTORS.CATEGORY_TITLES);
            if (categoryElement && blacklist.some(keyword => categoryElement.textContent.trim().includes(keyword))) {
                const floorContainer = card.closest('.bili-grid-floor') || card;
                if (floorContainer && removeCardElement(floorContainer)) blockedCount++;
            }
        });
        return blockedCount;
    }
    function blockVideos() {
        if (!configManager.get('video.enabled')) return 0;
        const blacklist = configManager.get('video.blacklist') || [];
        if (blacklist.length === 0) return 0;
        let blockedCount = 0;
        document.querySelectorAll(SELECTORS.CARD_CONTAINERS).forEach(card => {
            if (card.dataset.blocked) return;
            const title = card.querySelector(SELECTORS.VIDEO_TITLES)?.textContent.trim() || '';
            const author = card.querySelector(SELECTORS.VIDEO_AUTHORS)?.textContent.trim() || '';
            if (blacklist.some(keyword => title.includes(keyword) || author.includes(keyword))) {
                if (removeCardElement(card)) blockedCount++;
            }
        });
        return blockedCount;
    }
    function blockLive() {
        if (!configManager.get('live')) return 0;
        let blockedCount = 0;
        document.querySelectorAll(`${SELECTORS.LIVE_CARDS}, ${SELECTORS.CARD_CONTAINERS}`).forEach(card => {
            if (card.dataset.blocked) return;
            const isLive = card.querySelector(SELECTORS.LIVE_INDICATORS) ||
                card.textContent.includes('正在直播') ||
                card.querySelector('.floor-title')?.textContent.includes('直播');
            if (isLive && removeCardElement(card)) blockedCount++;
        });
        return blockedCount;
    }
    const runAllBlockers = () => {
        const totalBlocked = [blockAds, blockCategories, blockVideos, blockLive]
            .reduce((sum, func) => sum + safeExecute(func, `${func.name}执行失败`), 0);
        if (totalBlocked > 0) showBlockingStatus(totalBlocked);
        return totalBlocked;
    };
    const debouncedRunAllBlockers = debounce(runAllBlockers, 300);

    // ===== UI 组件 =====
    function injectGlobalStyles() {
        const style = document.createElement('style');
        style.textContent = `
            :root {
                --panel-bg: rgba(255, 255, 255, 0.95);
                --panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.1);
                --text-primary: #18191c;
                --text-secondary: #5f6368;
                --text-tertiary: #888c92;
                --border-color: #e3e5e7;
                --hover-bg: #f1f2f3;
                --active-bg: #e7e8e9;
                --accent-blue: #00a1d6;
                --accent-blue-hover: #00b5e5;
                --accent-red: #fd4c5d;
                --accent-green: #52c41a;
                --accent-gray: #d9d9d9;
                --input-bg: #f1f2f3;
                --input-border: #e3e5e7;
                --tag-bg: #f1f2f3;
                --tag-text: #5f6368;
                --tag-hover-bg: #e7e8e9;
                --info-bg: #e6f7ff;
                --info-text: #0958d9;
            }
            body[data-theme="dark"] {
                --panel-bg: rgba(37, 37, 37, 0.95);
                --panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.2);
                --text-primary: #e7e9ea;
                --text-secondary: #b0b3b8;
                --text-tertiary: #8a8d91;
                --border-color: #4d4d4d;
                --hover-bg: #4a4a4a;
                --active-bg: #5a5a5a;
                --input-bg: #3a3b3c;
                --input-border: #4d4d4d;
                --tag-bg: #3a3b3c;
                --tag-text: #e4e6eb;
                --tag-hover-bg: #5a5a5a;
                --info-bg: #263e5e;
                --info-text: #69b1ff;
            }
            .filter-panel {
                position: fixed; left: 50%; top: 50%;
                transform: translate(-50%, -50%);
                background: var(--panel-bg);
                color: var(--text-primary);
                border-radius: 16px;
                box-shadow: var(--panel-shadow);
                padding: 24px;
                z-index: 10000;
                backdrop-filter: blur(12px);
                -webkit-backdrop-filter: blur(12px);
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                transition: opacity 0.3s, transform 0.3s;
                border: 1px solid var(--border-color);
            }
            .filter-panel.main-panel { width: 400px; }
            .filter-panel.sub-panel { width: 480px; max-height: 85vh; display: flex; flex-direction: column;}
            .panel-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 15px; border-bottom: 1px solid var(--border-color); margin-bottom: 20px; gap: 15px; }
            .panel-header h3 { margin: 0; font-size: 18px; display: flex; align-items: center; gap: 8px; }
            .panel-header .header-buttons { display: flex; align-items: center; gap: 8px; }
            .header-btn { cursor: pointer; font-size: 20px; color: var(--text-secondary); width: 32px; height: 32px; border-radius: 50%; transition: background .2s, color .2s; border: none; background: none; display: flex; align-items: center; justify-content: center; }
            .header-btn:hover { background: var(--hover-bg); color: var(--text-primary); }
            .switch-item { display: flex; justify-content: space-between; align-items: center; margin: 16px 0; padding: 10px; border-radius: 8px; transition: background-color 0.2s; }
            .switch-item:hover { background-color: var(--hover-bg); }
            .switch-item > div:first-child { display: flex; align-items: center; gap: 8px; }
            .switch-item span { font-size: 14px; }
            .manage-btn { color: var(--accent-blue); cursor: pointer; margin-left: 10px; padding: 6px 10px; border-radius: 6px; transition: background .2s; border: none; background: none; font-size: 13px; font-weight: 500; }
            .manage-btn:hover { background: var(--accent-blue); color: #fff; }
            .switch { position: relative; display: inline-block; width: 40px; height: 20px; }
            .switch input { opacity: 0; width: 0; height: 0; }
            .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--accent-gray); transition: .4s; border-radius: 20px; }
            .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; }
            .switch input:checked + .slider { background-color: var(--accent-blue); }
            .switch input:checked + .slider:before { transform: translateX(20px); }
            .continuous-block-section { background: var(--hover-bg); padding: 12px; border-radius: 8px; margin: 20px 0; }
            .status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-left: 8px; transition: background-color 0.4s; }
            .status-indicator.status-active { background: var(--accent-green); }
            .status-indicator.status-inactive { background: var(--text-tertiary); }
            .action-buttons { display: flex; gap: 12px; margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border-color); }
            .action-btn { flex: 1; padding: 10px 14px; border: 1px solid var(--accent-blue); background: transparent; color: var(--accent-blue); border-radius: 8px; cursor: pointer; transition: all .2s; font-size: 14px; font-weight: 500; display: flex; align-items: center; justify-content: center; gap: 6px;}
            .action-btn:hover { background: var(--accent-blue); color: #fff; }
            .action-btn.primary { background: var(--accent-blue); color: #fff; }
            .action-btn.primary:hover { background: var(--accent-blue-hover); border-color: var(--accent-blue-hover); }
            .input-group { display: flex; gap: 10px; margin-bottom: 20px; }
            .input-field { flex: 1; padding: 12px; border: 1px solid var(--input-border); border-radius: 8px; font-size: 14px; background: var(--input-bg); color: var(--text-primary); }
            .input-field:focus { border-color: var(--accent-blue); outline: none; box-shadow: 0 0 0 2px rgba(0, 161, 214, 0.2); }
            .add-btn { padding: 12px 24px; background: var(--accent-blue); color: #fff; border: none; border-radius: 8px; cursor: pointer; transition: background .2s; font-weight: 500; }
            .add-btn:hover { background: var(--accent-blue-hover); }
            .item-list { overflow-y: auto; padding: 5px; margin: -5px; display: flex; flex-wrap: wrap; gap: 10px; align-content: flex-start; }
            .item-tag { display: flex; align-items: center; padding: 6px 12px; background: var(--tag-bg); color: var(--tag-text); border-radius: 16px; transition: background .2s; font-size: 13px; }
            .item-tag span { word-break: keep-all; white-space: nowrap; }
            .item-tag .delete-btn { color: var(--text-tertiary); background: none; border: none; cursor: pointer; padding: 4px; margin-left: 6px; font-size: 16px; line-height: 1; border-radius: 50%; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; }
            .item-tag .delete-btn:hover { background: var(--active-bg); color: var(--accent-red); }
            .stats-bar { padding: 12px; border-radius: 8px; margin-bottom: 20px; background: var(--info-bg); color: var(--info-text); font-size: 14px; }
            .master-float-btn { position: fixed; background: linear-gradient(135deg, #00a1d6, #0085b3); color: #fff; padding: 12px; border-radius: 50%; z-index: 9999; cursor: grab; box-shadow: 0 4px 12px rgba(0,0,0,0.3); transition: transform .2s, box-shadow .2s; user-select: none; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; font-size: 20px; -webkit-user-select: none; }
            .master-float-btn:hover { transform: scale(1.1); box-shadow: 0 6px 16px rgba(0,0,0,0.4); }
            .master-float-btn:active { cursor: grabbing; }
            .master-float-btn.dragging { opacity: .8; z-index: 10000; cursor: grabbing; transform: scale(1.05); }
        `;
        document.head.appendChild(style);
    }
    const ICONS = {
        shield: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>`,
        close: `&times;`,
        sun: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`,
        moon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>`,
        settings: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-4.44a2 2 0 0 0-2 2v.77a2 2 0 0 1-1.11 1.79l-1.33.88a2 2 0 0 0-.89 2.6l.33.6a2 2 0 0 1 0 2.12l-.33.6a2 2 0 0 0 .89 2.6l1.33.88a2 2 0 0 1 1.11 1.79V20a2 2 0 0 0 2 2h4.44a2 2 0 0 0 2-2v-.77a2 2 0 0 1 1.11-1.79l1.33-.88a2 2 0 0 0 .89-2.6l-.33-.6a2 2 0 0 1 0-2.12l.33-.6a2 2 0 0 0-.89-2.6l-1.33-.88a2 2 0 0 1-1.11-1.79V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></svg>`,
        list: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>`,
        zap: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>`,
        trash: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>`
    };

    function createMainPanel() {
        const panel = document.createElement('div');
        panel.className = 'filter-panel main-panel';
        panel.style.display = 'none';
        const isDarkMode = configManager.get('darkMode');
        panel.innerHTML = `
            <div class="panel-header">
                <h3>${ICONS.shield} 智能屏蔽控制中心 v7.0.0</h3>
                <div class="header-buttons">
                    <button class="header-btn theme-toggle-btn" title="切换主题">${isDarkMode ? ICONS.sun : ICONS.moon}</button>
                    <button class="header-btn close-btn" title="关闭">${ICONS.close}</button>
                </div>
            </div>
            <div class="continuous-block-section">
                <div class="switch-item">
                    <div>${ICONS.settings}<span>自适应持续屏蔽 <span class="status-indicator ${configManager.get('continuousBlock') ? 'status-active' : 'status-inactive'}"></span></span></div>
                    <label class="switch"><input type="checkbox" id="continuous-block" ${configManager.get('continuousBlock') ? 'checked' : ''}><span class="slider"></span></label>
                </div>
                <div style="font-size:12px;color:var(--text-tertiary);margin-top:5px;padding-left:10px;">智能调节屏蔽间隔,优化性能</div>
            </div>
            <div class="switch-item"><div>${ICONS.list}<span>视频关键词屏蔽</span></div><div><label class="switch"><input type="checkbox" id="video-enabled" ${configManager.get('video.enabled') ? 'checked' : ''}><span class="slider"></span></label><button class="manage-btn" data-type="video">管理</button></div></div>
            <div class="switch-item"><div>${ICONS.list}<span>分类屏蔽</span></div><div><label class="switch"><input type="checkbox" id="category-enabled" ${configManager.get('category.enabled') ? 'checked' : ''}><span class="slider"></span></label><button class="manage-btn" data-type="category">管理</button></div></div>
            <div class="switch-item"><div>${ICONS.list}<span>广告屏蔽</span></div><label class="switch"><input type="checkbox" id="ad-enabled" ${configManager.get('ad') ? 'checked' : ''}><span class="slider"></span></label></div>
            <div class="switch-item"><div>${ICONS.list}<span>直播推荐屏蔽</span></div><label class="switch"><input type="checkbox" id="live-enabled" ${configManager.get('live') ? 'checked' : ''}><span class="slider"></span></label></div>
            <div class="action-buttons">
                <button class="action-btn primary" id="run-once">${ICONS.zap}立即执行</button>
                <button class="action-btn" id="reset-config">${ICONS.trash}重置配置</button>
            </div>
        `;
        // Event listeners
        panel.querySelector('.theme-toggle-btn').addEventListener('click', e => {
            const body = document.body;
            const currentTheme = body.dataset.theme === 'dark';
            const newTheme = !currentTheme;
            body.dataset.theme = newTheme ? 'dark' : 'light';
            configManager.setValue('darkMode', newTheme);
            e.currentTarget.innerHTML = newTheme ? ICONS.sun : ICONS.moon;
        });
        panel.querySelector('#continuous-block').addEventListener('change', e => {
            configManager.setValue('continuousBlock', e.target.checked);
            panel.querySelector('.status-indicator').className = `status-indicator ${e.target.checked ? 'status-active' : 'status-inactive'}`;
            e.target.checked ? adaptiveBlocker.start() : adaptiveBlocker.stop();
        });
        panel.querySelector('#video-enabled').addEventListener('change', e => {
            configManager.setValue('video.enabled', e.target.checked);
            runAllBlockers();
        });
        panel.querySelector('#category-enabled').addEventListener('change', e => {
            configManager.setValue('category.enabled', e.target.checked);
            runAllBlockers();
        });
        panel.querySelector('#ad-enabled').addEventListener('change', e => {
            configManager.setValue('ad', e.target.checked);
            runAllBlockers();
        });
        panel.querySelector('#live-enabled').addEventListener('change', e => {
            configManager.setValue('live', e.target.checked);
            runAllBlockers();
        });
        panel.querySelectorAll('.manage-btn').forEach(btn => {
            btn.addEventListener('click', e => {
                e.stopPropagation();
                showManagementPanel(btn.dataset.type);
            });
        });
        panel.querySelector('#run-once').addEventListener('click', runAllBlockers);
        panel.querySelector('#reset-config').addEventListener('click', () => {
            if (confirm('确定要重置所有配置吗?此操作不可撤销,页面将刷新。')) {
                configManager.reset();
                location.reload();
            }
        });
        panel.querySelector('.close-btn').addEventListener('click', () => panel.style.display = 'none');
        return panel;
    }

    function createSubPanel(type) {
        const isVideo = type === 'video';
        const panel = document.createElement('div');
        panel.className = 'filter-panel sub-panel';
        panel.id = `${type}-management-panel`;

        const listClass = `${type}-item-list`;
        const inputClass = `${type}-management-input`;
        const countClass = `${type}-management-count`;

        panel.innerHTML = `
            <div class="panel-header">
                <h3>${isVideo ? '📝 视频关键词管理' : '🏷️ 分类屏蔽管理'}</h3>
                <div class="header-buttons">
                    <button class="header-btn close-btn" title="关闭">${ICONS.close}</button>
                </div>
            </div>
            <div class="stats-bar">当前屏蔽${isVideo ? '关键词' : '分类'}:<span class="${countClass}">${(configManager.get(`${type}.blacklist`) || []).length}</span> 个</div>
            <div class="input-group">
                <input type="text" class="${inputClass} input-field" placeholder="输入要屏蔽的${isVideo ? '关键词(含UP主)' : '分类名称'},回车添加">
                <button class="add-btn">添加</button>
            </div>
            <div class="item-list ${listClass}"></div>
        `;

        document.body.appendChild(panel);

        const list = panel.querySelector(`.${listClass}`);
        const countSpan = panel.querySelector(`.${countClass}`);
        const input = panel.querySelector(`.${inputClass}`);

        const updateList = () => {
            const blacklist = configManager.get(`${type}.blacklist`) || [];
            if (countSpan) countSpan.textContent = blacklist.length;
            list.innerHTML = blacklist.map(item =>
                `<div class="item-tag">
                    <span>${item}</span>
                    <button class="delete-btn" data-item="${item}" title="删除">${ICONS.close}</button>
                </div>`
            ).join('');

            list.querySelectorAll('.delete-btn').forEach(btn => {
                btn.addEventListener('click', () => {
                    const itemToRemove = btn.dataset.item;
                    let currentList = configManager.get(`${type}.blacklist`) || [];
                    const newList = currentList.filter(i => i !== itemToRemove);
                    configManager.setValue(`${type}.blacklist`, newList);
                    updateList();
                    runAllBlockers();
                });
            });
        };

        const addItem = () => {
            if (!input) return;
            const item = input.value.trim();
            const currentList = configManager.get(`${type}.blacklist`) || [];
            if (item && !currentList.includes(item)) {
                const newList = [...currentList, item];
                configManager.setValue(`${type}.blacklist`, newList);
                input.value = '';
                updateList();
                runAllBlockers();
            }
            input.focus();
        };

        panel.querySelector('.add-btn')?.addEventListener('click', addItem);
        input?.addEventListener('keypress', e => {
            if (e.key === 'Enter') addItem();
        });
        panel.querySelector('.close-btn')?.addEventListener('click', () => panel.remove());
        updateList();
    }

    const showManagementPanel = type => {
        const existingPanel = document.getElementById(`${type}-management-panel`);
        if (existingPanel) {
            existingPanel.remove();
        }
        createSubPanel(type);
    };

    function createDraggableFloatBtn(mainPanelElement) {
        const btn = document.createElement('div');
        btn.className = 'master-float-btn';
        btn.title = '拖动移动位置,点击打开控制面板';
        btn.innerHTML = '🛡️';
        const pos = configManager.get('floatBtnPosition');
        btn.style.left = `${pos.x}px`;
        btn.style.bottom = `${pos.y}px`;

        let isDragging = false, hasMoved = false;
        let startX, startY, initialX, initialY;

        const handleStart = e => {
            e.preventDefault();
            isDragging = true;
            hasMoved = false;
            btn.classList.add('dragging');
            const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
            const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;
            startX = clientX;
            startY = clientY;
            const rect = btn.getBoundingClientRect();
            initialX = rect.left;
            initialY = window.innerHeight - rect.bottom;
            document.addEventListener(e.type.includes('touch') ? 'touchmove' : 'mousemove', handleMove, { passive: false });
            document.addEventListener(e.type.includes('touch') ? 'touchend' : 'mouseup', handleEnd);
        };

        const handleMove = e => {
            if (!isDragging) return;
            e.preventDefault();
            const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
            const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;
            const deltaX = clientX - startX;
            const deltaY = startY - clientY;
            if (!hasMoved && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) hasMoved = true;

            if (hasMoved) {
                let newX = initialX + deltaX, newY = initialY + deltaY;
                newX = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, newX));
                newY = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, newY));
                btn.style.left = `${newX}px`;
                btn.style.bottom = `${newY}px`;
            }
        };

        const handleEnd = () => {
            if (!isDragging) return;
            isDragging = false;
            btn.classList.remove('dragging');
            document.removeEventListener('mousemove', handleMove);
            document.removeEventListener('mouseup', handleEnd);
            document.removeEventListener('touchmove', handleMove);
            document.removeEventListener('touchend', handleEnd);

            if (hasMoved) {
                const rect = btn.getBoundingClientRect();
                configManager.setValue('floatBtnPosition', { x: rect.left, y: window.innerHeight - rect.bottom });
            } else {
                mainPanelElement.style.display = mainPanelElement.style.display === 'none' ? 'block' : 'none';
            }
        };

        btn.addEventListener('mousedown', handleStart);
        btn.addEventListener('touchstart', handleStart, { passive: false });

        window.addEventListener('resize', throttle(() => {
            const rect = btn.getBoundingClientRect();
            const x = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, rect.left));
            const y = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, window.innerHeight - rect.bottom));
            btn.style.left = `${x}px`;
            btn.style.bottom = `${y}px`;
        }, 100));
        return btn;
    }
    // ===== 初始化 =====
    function init() {
        injectGlobalStyles();
        if (configManager.get('darkMode')) {
            document.body.dataset.theme = 'dark';
        }

        const mainPanelElement = createMainPanel();
        const floatBtnElement = createDraggableFloatBtn(mainPanelElement);
        document.body.appendChild(mainPanelElement);
        document.body.appendChild(floatBtnElement);

        document.addEventListener('click', e => {
            const isClickInsidePanel = e.target.closest('.filter-panel');
            const isClickOnFloatBtn = floatBtnElement.contains(e.target);

            if (!isClickInsidePanel && !isClickOnFloatBtn) {
                mainPanelElement.style.display = 'none';
                const subPanels = document.querySelectorAll('.sub-panel');
                subPanels.forEach(p => p.remove());
            }
        });

        const observer = new MutationObserver(throttle(() => {
            if (!configManager.get('continuousBlock')) debouncedRunAllBlockers();
        }, 500));
        observer.observe(document.body, { childList: true, subtree: true });

        if (document.readyState === 'complete') {
            runAllBlockers();
        } else {
            window.addEventListener('load', runAllBlockers);
        }

        if (configManager.get('continuousBlock')) {
            adaptiveBlocker.start();
        }

        window.addEventListener('beforeunload', () => {
            adaptiveBlocker.stop();
            observer.disconnect();
        });
        console.log('[B站过滤插件] v7.0.0 初始化完成');
    }

    init();
})();