Filter Reddit

Hide posts and comments containing specified keywords on Reddit

目前为 2024-12-01 提交的版本。查看 最新版本

// ==UserScript==
// @name         Filter Reddit
// @namespace    https://gf.qytechs.cn/en/users/567951-stuart-saddler
// @version      1.6
// @description  Hide posts and comments containing specified keywords on Reddit
// @author       Stuart Saddler
// @license      MIT
// @icon         https://static.vecteezy.com/system/resources/previews/023/986/983/original/reddit-logo-reddit-logo-transparent-reddit-icon-transparent-free-free-png.png
// @supportURL   https://gf.qytechs.cn/en/users/567951-stuart-saddler
// @match        *://www.reddit.com/*
// @match        *://old.reddit.com/*
// @run-at       document-end
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    // Cache selectors for better performance
    const POST_SELECTORS = 'article, div[data-testid="post-container"], shreddit-post';
    const CONTENT_SELECTORS = 'h1, h2, h3, p, a[href], [role="heading"]';

    // Initialize state variables
    let filteredCount = 0;
    let menuCommand = null;
    let processedPosts = new WeakSet();
    let keywordsArray = [];
    const processQueue = new Set();

    // Cross-compatibility wrapper for GM functions
    const GM = {
        async getValue(name, defaultValue) {
            return typeof GM_getValue !== 'undefined'
                ? GM_getValue(name, defaultValue)
                : await GM.getValue(name, defaultValue);
        },
        async setValue(name, value) {
            if (typeof GM_setValue !== 'undefined') {
                GM_setValue(name, value);
            } else {
                await GM.setValue(name, value);
            }
        }
    };

    // Styles for the filter dialog
    const CSS = `
        .content-filtered {
            display: none !important;
            height: 0 !important;
            overflow: hidden !important;
        }
        .reddit-filter-dialog {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border-radius: 8px;
            z-index: 1000000;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            min-width: 300px;
            max-width: 500px;
        }
        .reddit-filter-dialog textarea {
            width: 100%;
            min-height: 200px;
            margin: 10px 0;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-family: monospace;
        }
        .reddit-filter-dialog button {
            padding: 8px 16px;
            margin: 0 5px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .reddit-filter-dialog .save-btn {
            background-color: #0079d3;
            color: white;
        }
        .reddit-filter-dialog .cancel-btn {
            background-color: #f2f2f2;
        }
        .reddit-filter-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,0.5);
            z-index: 999999;
        }
    `;

    // Add CSS to page
    if (typeof GM_addStyle !== 'undefined') {
        GM_addStyle(CSS);
    } else {
        const style = document.createElement('style');
        style.textContent = CSS;
        document.head.appendChild(style);
    }

    // Utility function to debounce processing
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // Schedule processing during idle time
    function scheduleProcessing() {
        if ('requestIdleCallback' in window) {
            requestIdleCallback((deadline) => {
                while (deadline.timeRemaining() > 0 && processQueue.size > 0) {
                    const post = processQueue.values().next().value;
                    processPost(post);
                    processQueue.delete(post);
                }
                if (processQueue.size > 0) {
                    scheduleProcessing();
                }
            });
        } else {
            processQueue.forEach(post => {
                processPost(post);
                processQueue.delete(post);
            });
        }
    }

    // Preload next page content
    async function preloadPosts() {
        const nextPageLink = document.querySelector('a[data-click-id="next"]');
        if (nextPageLink) {
            try {
                const nextUrl = nextPageLink.href;
                const response = await fetch(nextUrl);
                const html = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(html, 'text/html');
                return doc.querySelectorAll(POST_SELECTORS);
            } catch (error) {
                console.error('Preloading failed:', error);
                return null;
            }
        }
        return null;
    }

    function getKeywords() {
        return GM_getValue('filterKeywords', []);
    }

    // Configuration dialog
    async function showConfig() {
        const overlay = document.createElement('div');
        overlay.className = 'reddit-filter-overlay';

        const dialog = document.createElement('div');
        dialog.className = 'reddit-filter-dialog';
        dialog.innerHTML = `
            <h2 style="margin-top: 0;">Reddit Filter Keywords</h2>
            <p>Enter keywords one per line:</p>
            <textarea spellcheck="false">${keywordsArray.join('\n')}</textarea>
            <div style="text-align: right;">
                <button class="cancel-btn">Cancel</button>
                <button class="save-btn">Save</button>
            </div>
        `;

        document.body.appendChild(overlay);
        document.body.appendChild(dialog);

        const closeDialog = () => {
            dialog.remove();
            overlay.remove();
        };

        dialog.querySelector('.save-btn').addEventListener('click', async () => {
            const newKeywords = dialog.querySelector('textarea').value
                .split('\n')
                .map(k => k.trim())
                .filter(k => k.length > 0);
            await GM.setValue('filterKeywords', newKeywords);
            closeDialog();
            location.reload();
        });

        dialog.querySelector('.cancel-btn').addEventListener('click', closeDialog);
        overlay.addEventListener('click', closeDialog);
    }

    function updateCounter() {
        if (menuCommand) {
            GM_unregisterMenuCommand(menuCommand);
        }
        menuCommand = GM_registerMenuCommand(
            `Configure Filter Keywords (${filteredCount} blocked)`,
            showConfig
        );
    }

    // Process individual posts
    const processPost = debounce((post) => {
        if (!post || processedPosts.has(post)) return;
        processedPosts.add(post);

        const postContent = [
            post.textContent,
            ...Array.from(post.querySelectorAll(CONTENT_SELECTORS))
                .map(el => el.textContent)
        ].join(' ').toLowerCase();

        if (keywordsArray.some(keyword => postContent.includes(keyword.toLowerCase()))) {
            post.classList.add('content-filtered');
            const parentArticle = post.closest(POST_SELECTORS);
            if (parentArticle) {
                parentArticle.classList.add('content-filtered');
            }
            filteredCount++;
            updateCounter();
        }
    }, 100);

    // Initialize the script
    async function init() {
        keywordsArray = await GM.getValue('filterKeywords', []);
        updateCounter();

        // Start preloading next page
        preloadPosts();

        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.matches(POST_SELECTORS)) {
                            processQueue.add(node);
                        }
                        node.querySelectorAll(POST_SELECTORS).forEach(post => {
                            processQueue.add(post);
                        });
                        scheduleProcessing();
                    }
                }
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        // Process initial posts
        document.querySelectorAll(POST_SELECTORS).forEach(post => {
            processQueue.add(post);
        });
        scheduleProcessing();
    }

    // Memory cleanup
    setInterval(() => {
        for (const post of processedPosts) {
            if (!document.contains(post)) {
                processedPosts.delete(post);
            }
        }
    }, 60000);

    // Start the script
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址