Reddit Content Filter

Hide posts and comments containing specified keywords on Reddit

目前為 2024-11-10 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Reddit Content Filter
// @namespace    https://gf.qytechs.cn/en/users/567951-stuart-saddler
// @version      1.0
// @description  Hide posts and comments containing specified keywords on Reddit
// @license      MIT
// @match        *://www.reddit.com/*
// @run-at       document-start
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    let filteredCount = 0;
    let menuCommand = null;
    let processedPosts = new Set();
    let keywordsArray = [];

    // Add CSS immediately
    const style = document.createElement('style');
    style.textContent = `
        .content-filtered {
            display: none !important;
            height: 0 !important;
            min-height: 0 !important;
            margin: 0 !important;
            padding: 0 !important;
            border: none !important;
            opacity: 0 !important;
            position: absolute !important;
            pointer-events: none !important;
            clip: rect(0 0 0 0) !important;
            max-height: 0 !important;
            overflow: hidden !important;
        }

        /* Reddit Layout Fixes */
        :root {
            --post-width: 740px;
        }

        /* Main container */
        div[class*="ListingLayout"] {
            max-width: 100% !important;
        }

        /* Content wrapper */
        div[class*="ListingLayout-backgroundContainer"] {
            max-width: calc(var(--post-width) + 600px) !important;
            margin: 0 auto !important;
            display: grid !important;
            grid-template-columns: 0fr minmax(var(--post-width), 1fr) 0fr !important;
            gap: 24px !important;
            padding: 20px 24px !important;
        }

        /* Left sidebar */
        nav[role="navigation"] {
            position: sticky !important;
            top: 48px !important;
            height: calc(100vh - 48px) !important;
            display: block !important;
            visibility: visible !important;
            opacity: 1 !important;
            margin: 0 !important;
            padding: 0 !important;
            width: 270px !important;
        }

        /* Main feed */
        div[class*="Post"] {
            width: var(--post-width) !important;
            max-width: var(--post-width) !important;
            margin: 0 auto !important;
        }

        /* Right sidebar */
        div[data-testid="subreddit-sidebar"] {
            position: sticky !important;
            top: 48px !important;
            width: 312px !important;
            display: block !important;
            visibility: visible !important;
            opacity: 1 !important;
        }

        .filter-dialog {
            background: white !important;
            z-index: 1000000 !important;
            display: block !important;
            visibility: visible !important;
            opacity: 1 !important;
        }
    `;
    document.head.appendChild(style);

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

    function showConfig() {
        const dialog = document.createElement('dialog');
        dialog.className = 'filter-dialog';
        dialog.style.cssText = `
            padding: 20px;
            max-width: 500px;
            max-height: 80vh;
            background: white;
            border-radius: 8px;
            border: 1px solid #ccc;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        `;

        dialog.innerHTML = `
            <h2 style="margin-top: 0;">Reddit Filter Keywords</h2>
            <p>Enter keywords one per line:</p>
            <textarea style="width: 100%; height: 300px; margin-bottom: 10px; padding: 8px;">${keywordsArray.join('\n')}</textarea>
            <div style="display: flex; justify-content: flex-end; gap: 10px;">
                <button id="cancelBtn">Cancel</button>
                <button id="saveBtn">Save</button>
            </div>
        `;

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

        dialog.querySelector('#saveBtn').addEventListener('click', () => {
            const newKeywords = dialog.querySelector('textarea').value
                .split('\n')
                .map(k => k.trim())
                .filter(k => k.length > 0);
            GM_setValue('filterKeywords', newKeywords);
            dialog.remove();
            location.reload();
        });

        dialog.querySelector('#cancelBtn').addEventListener('click', () => {
            dialog.remove();
        });
    }

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

    function processPost(post) {
        if (!post || processedPosts.has(post)) return;
        processedPosts.add(post);

        const ariaLabel = post.getAttribute('aria-label');
        const postContent = [
            ariaLabel,
            post.textContent,
            ...Array.from(post.querySelectorAll('h1, h2, h3, p, a[data-click-id="body"], [slot="title"], [role="heading"]'))
                .map(el => el.textContent)
        ].filter(Boolean).join(' ').toLowerCase();

        if (postContent && keywordsArray.some(keyword =>
            postContent.includes(keyword.toLowerCase())
        )) {
            post.classList.add('content-filtered');

            let parent = post.closest('article');
            if (parent) {
                parent.classList.add('content-filtered');
            }

            filteredCount++;
            updateCounter();
        }
    }

    // Initialize
    keywordsArray = getKeywords();
    if (!keywordsArray || keywordsArray.length === 0) {
        keywordsArray = [];
    }

    // Process posts
    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (node.nodeType === 1) {
                    if (node.matches('article, shreddit-post')) {
                        processPost(node);
                    }
                    const posts = node.querySelectorAll('article, shreddit-post');
                    posts.forEach(processPost);
                }
            }
        }
    });

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

    // Initial processing
    document.querySelectorAll('article, shreddit-post').forEach(processPost);

    // Register menu command with counter
    updateCounter();

})();

QingJ © 2025

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