Content Control

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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();

})();