Content Control

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

})();