Content Control

Block or filter content on Zhihu, Xiaohongshu, Bilibili, and Weibo based on keywords.

// ==UserScript==
// @name         Content Control
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Block or filter content on Zhihu, Xiaohongshu, Bilibili, and Weibo based on keywords.
// @author       Your Name
// @match        https://www.zhihu.com/*
// @match        https://www.xiaohongshu.com/*
// @match        https://www.bilibili.com/*
// @match        https://www.bilibili.com/?*
// @match        https://www.bilibili.com/v/*
// @match        https://search.bilibili.com/*
// @match        https://weibo.com/*
// @match        https://www.weibo.com/*
// @match        https://s.weibo.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Constants and Storage ---
    const BLOCK_STORAGE_KEY = 'keyword_blocker_words';
    const SHOW_STORAGE_KEY = 'showlist_keywords';
    const MODE_STORAGE_KEY = 'content_control_mode'; // 'block' or 'show'
    const DISABLED_SITES_KEY = 'content_control_disabled_sites';

    const DEFAULT_BLOCK_KEYWORDS = ['男','女'];
    const DEFAULT_SHOW_KEYWORDS = ['AI'];

    // --- Utility Functions ---
    function loadFromStorage(key, defaultValue) {
        try {
            const saved = localStorage.getItem(key);
            return saved ? JSON.parse(saved) : defaultValue;
        } catch (e) {
            console.error(`Failed to load from ${key}:`, e);
            return defaultValue;
        }
    }

    function saveToStorage(key, value) {
        localStorage.setItem(key, JSON.stringify(value));
    }

    // --- State Management ---
    let blockKeywords = loadFromStorage(BLOCK_STORAGE_KEY, [...DEFAULT_BLOCK_KEYWORDS]);
    let showKeywords = loadFromStorage(SHOW_STORAGE_KEY, [...DEFAULT_SHOW_KEYWORDS]);
    let currentMode = loadFromStorage(MODE_STORAGE_KEY, 'block');
    let disabledSites = loadFromStorage(DISABLED_SITES_KEY, []);

    // --- Site Configuration ---
    function getCurrentSite() {
        const hostname = window.location.hostname;
        if (hostname.includes('zhihu.com')) return 'zhihu';
        if (hostname.includes('xiaohongshu.com')) return 'xiaohongshu';
        if (hostname.includes('bilibili.com')) return 'bilibili';
        if (hostname.includes('weibo.com')) return 'weibo';
        return 'unknown';
    }

    const siteConfigs = {
        zhihu: {
            containerSelector: '.ContentItem',
            cardSelector: '.Card',
            titleSelector: '.ContentItem-title a',
        },
        xiaohongshu: {
            containerSelector: 'section.note-item',
            cardSelector: 'section.note-item',
            titleSelector: 'a.title, .title',
        },
        bilibili: {
            containerSelector: '.bili-feed-card, .bili-video-card',
            cardSelector: '.bili-feed-card, .bili-video-card',
            titleSelector: '.bili-video-card__info--tit, .bili-video-card__info--tit a, .bili-video-card__wrap .bili-video-card__info--tit',
        },
        weibo: {
            containerSelector: '.wbpro-scroller-item',
            cardSelector: '.wbpro-scroller-item',
            titleSelector: '.wbpro-feed-content .detail_wbtext_4CRf9',
        }
    };

    // --- UI ---
    function createManagementUI() {
        const style = document.createElement('style');
        style.textContent = `
            #content-control-toggle {
                position: fixed; left: 20px; top: 50%; transform: translateY(-50%); z-index: 10000;
                background: #1890ff; color: white; border: none; border-radius: 6px; padding: 12px 8px;
                cursor: pointer; font-size: 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.15);
                writing-mode: vertical-lr; text-orientation: mixed; transition: all 0.3s ease;
            }
            #content-control-toggle:hover {
                background: #40a9ff;
                transform: translateY(-50%) scale(1.05);
            }
            #content-control-panel {
                position: fixed; left: -350px; top: 50%; transform: translateY(-50%); z-index: 9999;
                width: 320px; max-height: 70vh; background: white; border: 1px solid #d9d9d9;
                border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.15); transition: left 0.3s ease;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            }
            #content-control-panel.show {
                left: 20px;
            }
            .cc-header {
                padding: 16px; border-bottom: 1px solid #f0f0f0; background: #fafafa;
                border-radius: 8px 8px 0 0;
            }
            .cc-header h3 {
                margin: 0; font-size: 16px; font-weight: 500; color: #262626;
            }
            .cc-mode-switch {
                margin-top: 10px;
            }
            .cc-mode-switch label {
                margin-right: 15px; font-size: 14px; color: #595959;
            }
            .cc-input-group {
                padding: 16px; display: flex; gap: 8px;
            }
            #cc-keyword-input {
                flex: 1; padding: 8px 12px; border: 1px solid #d9d9d9; border-radius: 4px;
                font-size: 14px; outline: none;
            }
            #cc-keyword-input:focus {
                border-color: #1890ff; box-shadow: 0 0 0 2px rgba(24,144,255,0.2);
            }
            #cc-add-keyword {
                padding: 8px 16px; background: #1890ff; color: white; border: none;
                border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.3s ease;
            }
            #cc-add-keyword:hover {
                background: #40a9ff;
            }
            #cc-keyword-list {
                list-style: none; margin: 0; padding: 0; max-height: calc(70vh - 200px);
                overflow-y: auto;
            }
            #cc-keyword-list li {
                display: flex; justify-content: space-between; align-items: center;
                padding: 12px 16px; border-bottom: 1px solid #f0f0f0;
            }
            #cc-keyword-list li:hover {
                background: #f5f5f5;
            }
            .cc-delete-keyword {
                padding: 4px 8px; background: #ff4d4f; color: white; border: none;
                border-radius: 3px; cursor: pointer; font-size: 12px;
            }
            .cc-delete-keyword:hover {
                background: #ff7875;
            }
            .cc-footer {
                padding: 12px 16px; background: #f9f9f9; border-top: 1px solid #f0f0f0;
                font-size: 12px; color: #666; border-radius: 0 0 8px 8px;
            }
        `;
        document.head.appendChild(style);

        const toggleBtn = document.createElement('button');
        toggleBtn.id = 'content-control-toggle';
        toggleBtn.textContent = '内容控制';
        document.body.appendChild(toggleBtn);

        const panel = document.createElement('div');
        panel.id = 'content-control-panel';
        panel.innerHTML = `
            <div class="cc-header">
                <h3>内容控制</h3>
                <div class="cc-mode-switch">
                    <label><input type="radio" name="mode" value="filter"> 筛选</label>
                    <label><input type="radio" name="mode" value="block"> 屏蔽</label>
                </div>
            </div>
            <div class="cc-input-group">
                <input type="text" id="cc-keyword-input" placeholder="输入关键词...">
                <button id="cc-add-keyword">添加</button>
            </div>
            <ul id="cc-keyword-list"></ul>
            <div class="cc-footer">
                <label><input type="checkbox" id="cc-site-disable"> 在当前网站禁用</label>
            </div>
        `;
        document.body.appendChild(panel);

        updatePanelUI();
        addPanelEventListeners();

        toggleBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            panel.classList.toggle('show');
            toggleBtn.style.display = panel.classList.contains('show') ? 'none' : 'block';
        });
        
        document.addEventListener('click', (e) => {
            if (!panel.contains(e.target) && !toggleBtn.contains(e.target)) {
                panel.classList.remove('show');
                toggleBtn.style.display = 'block';
            }
        });
    }

    function updatePanelUI() {
        const isBlockMode = currentMode === 'block';
        const keywords = isBlockMode ? blockKeywords : showKeywords;
        const listElement = document.querySelector('#cc-keyword-list');
        if (listElement) {
            listElement.innerHTML = keywords.map((kw, i) =>
                `<li data-index="${i}">${kw} <button class="cc-delete-keyword">删除</button></li>`).join('');
        }
        const blockRadio = document.querySelector('input[name="mode"][value="block"]');
        const filterRadio = document.querySelector('input[name="mode"][value="filter"]');
        if (blockRadio) blockRadio.checked = isBlockMode;
        if (filterRadio) filterRadio.checked = !isBlockMode;
        
        const inputElement = document.querySelector('#cc-keyword-input');
        if (inputElement) {
            inputElement.placeholder = isBlockMode ? '输入屏蔽词...' : '输入筛选词...';
        }
        
        const disableCheckbox = document.querySelector('#cc-site-disable');
        if (disableCheckbox) {
            disableCheckbox.checked = disabledSites.includes(getCurrentSite());
        }
    }

    function addPanelEventListeners() {
        const modeRadios = document.querySelectorAll('input[name="mode"]');
        modeRadios.forEach(radio => {
            radio.addEventListener('change', (e) => {
                currentMode = e.target.value;
                saveToStorage(MODE_STORAGE_KEY, currentMode);
                updatePanelUI();
                processAllItems();
            });
        });

        const addKeywordBtn = document.getElementById('cc-add-keyword');
        if (addKeywordBtn) {
            addKeywordBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                const input = document.getElementById('cc-keyword-input');
                if (input && input.value.trim()) {
                    const newKeywords = input.value.split(/[,,/]/).map(k => k.trim()).filter(Boolean);
                    if (newKeywords.length > 0) {
                        const keywords = currentMode === 'block' ? blockKeywords : showKeywords;
                        const storageKey = currentMode === 'block' ? BLOCK_STORAGE_KEY : SHOW_STORAGE_KEY;
                        keywords.unshift(...newKeywords);
                        saveToStorage(storageKey, keywords);
                        input.value = '';
                        updatePanelUI();
                        processAllItems();
                    }
                }
            });
        }

        const keywordList = document.getElementById('cc-keyword-list');
        if (keywordList) {
            keywordList.addEventListener('click', (e) => {
                e.stopPropagation();
                if (e.target.classList.contains('cc-delete-keyword')) {
                    const index = parseInt(e.target.parentElement.dataset.index, 10);
                    const keywords = currentMode === 'block' ? blockKeywords : showKeywords;
                    const storageKey = currentMode === 'block' ? BLOCK_STORAGE_KEY : SHOW_STORAGE_KEY;
                    keywords.splice(index, 1);
                    saveToStorage(storageKey, keywords);
                    updatePanelUI();
                    processAllItems();
                }
            });
        }

        const siteDisableCheckbox = document.getElementById('cc-site-disable');
        if (siteDisableCheckbox) {
            siteDisableCheckbox.addEventListener('change', (e) => {
                const site = getCurrentSite();
                if (e.target.checked) {
                    if (!disabledSites.includes(site)) disabledSites.push(site);
                } else {
                    disabledSites = disabledSites.filter(s => s !== site);
                }
                saveToStorage(DISABLED_SITES_KEY, disabledSites);
                processAllItems();
            });
        }
        
        // 添加回车键支持
        const keywordInput = document.getElementById('cc-keyword-input');
        if (keywordInput) {
            keywordInput.addEventListener('keypress', (e) => {
                if (e.key === 'Enter') {
                    e.stopPropagation();
                    document.getElementById('cc-add-keyword').click();
                }
            });
        }
    }

    // --- Content Processing ---
    function processItem(item, config) {
        const titleElement = item.querySelector(config.titleSelector);
        const content = titleElement ? titleElement.innerText : item.innerText;
        const card = item.closest(config.cardSelector);
        if (!card) return;

        if (currentMode === 'block') {
            const shouldBlock = blockKeywords.some(keyword => content.toLowerCase().includes(keyword.toLowerCase()));
            card.style.display = shouldBlock ? 'none' : '';
        } else {
            const shouldShow = showKeywords.some(keyword => content.toLowerCase().includes(keyword.toLowerCase()));
            card.style.display = shouldShow ? '' : 'none';
        }
    }

    function processAllItems() {
        const site = getCurrentSite();
        if (site === 'unknown' || disabledSites.includes(site)) return;
        const config = siteConfigs[site];
        document.querySelectorAll(config.containerSelector).forEach(item => processItem(item, config));
    }

    // --- Initialization ---
    function init() {
        const site = getCurrentSite();
        if (site === 'unknown') return;

        // 确保DOM加载完成后再初始化UI
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                createManagementUI();
                processAllItems();
            });
        } else {
            createManagementUI();
            processAllItems();
        }

        const observer = new MutationObserver(mutations => {
            if (disabledSites.includes(site)) return;
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === 1) {
                        const config = siteConfigs[site];
                        if (node.matches && node.matches(config.containerSelector)) {
                            processItem(node, config);
                        }
                        if (node.querySelectorAll) {
                            node.querySelectorAll(config.containerSelector).forEach(item => processItem(item, config));
                        }
                    }
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    init();

})();