Reddit Advanced Content Filter

Automatically hides posts and comments on Reddit based on keywords or subreddits you specify. Case-insensitive filtering supports plural forms. Perfect for curating your feed.

当前为 2024-12-06 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Reddit Advanced Content Filter
// @namespace    https://greasyfork.org/en/users/567951-stuart-saddler
// @version      2.0
// @description  Automatically hides posts and comments on Reddit based on keywords or subreddits you specify. Case-insensitive filtering supports plural forms. Perfect for curating your feed.
// @author       Stuart Saddler
// @license      MY
// @icon         https://clipart-library.com/images_k/smoke-clipart-transparent/smoke-clipart-transparent-6.png
// @supportURL   https://greasyfork.org/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==

(async function () {
    'use strict';

    let filteredCount = 0;
    let menuCommand = null;
    let processedPosts = new WeakSet();
    let blocklistArray = [];

    const CSS = `
        .content-filtered {
            display: none !important;
            height: 0 !important;
            overflow: hidden !important;
        }
        /* Updated CSS to match Bluesky's configuration dialog styles */
        .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: 350px;
            font-family: Arial, sans-serif;
            color: #333;
        }
        .reddit-filter-dialog h2 {
            margin-top: 0;
            color: #0079d3;
            font-size: 1.5em;
            font-weight: bold;
        }
        .reddit-filter-dialog p {
            font-size: 0.9em;
            margin-bottom: 10px;
            color: #555;
        }
        .reddit-filter-dialog textarea {
            width: calc(100% - 16px); /* Ensures consistent padding */
            height: 150px; /* Adjusted height to match Bluesky's */
            padding: 8px;
            margin: 10px 0;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-family: monospace;
            background: #f9f9f9;
            color: #000;
            resize: vertical;
        }
        .reddit-filter-dialog .button-container {
            display: flex;
            justify-content: flex-end;
            gap: 10px;
            margin-top: 10px;
        }
        .reddit-filter-dialog button {
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 8px 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 1em;
            text-align: center;
        }
        .reddit-filter-dialog .save-btn {
            background-color: #0079d3;
            color: white;
        }
        .reddit-filter-dialog .cancel-btn {
            background-color: #f2f2f2;
            color: #333;
        }
        .reddit-filter-dialog button:hover {
            opacity: 0.9;
        }
        .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
    if (typeof GM_addStyle !== 'undefined') {
        GM_addStyle(CSS);
    } else {
        const style = document.createElement('style');
        style.textContent = CSS;
        document.head.appendChild(style);
    }

    // Function to create and display a modal dialog for configuration
    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>Reddit Filter: Blocklist</h2>
            <p>Enter keywords or subreddit names one per line. Filtering is case-insensitive.</p>
            <p><em>Keywords can match common plural forms (e.g., "apple" blocks "apples"). Irregular plurals (e.g., "mouse" and "mice") must be added separately. Subreddit names should be entered without the "r/" prefix (e.g., "subredditname").</em></p>
            <textarea spellcheck="false" id="blocklist">${blocklistArray.join('\n')}</textarea>
            <div class="button-container">
                <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 blocklistInput = dialog.querySelector('#blocklist').value;

            blocklistArray = blocklistInput
                .split('\n')
                .map(item => item.trim().toLowerCase())
                .filter(item => item.length > 0);

            await GM.setValue('blocklist', blocklistArray);

            closeDialog();
            location.reload();
        });

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

    // Function to update menu commands with the current count of blocked items
    function updateCounter() {
        if (menuCommand !== null) {
            GM_unregisterMenuCommand(menuCommand);
        }

        menuCommand = GM_registerMenuCommand(
            `Configure Blocklist (${filteredCount} blocked)`, // Updated to show blocked count
            showConfig
        );
    }

    // Function to process and filter individual posts
    function processPost(post) {
        if (!post || processedPosts.has(post)) return;
        processedPosts.add(post);

        let shouldHide = false;

        // Check for blocked subreddits
        const subredditElement = post.querySelector('a[data-click-id="subreddit"], a.subreddit');
        if (subredditElement) {
            const subredditName = subredditElement.textContent.trim().replace(/^r\//i, '').toLowerCase();
            if (blocklistArray.includes(subredditName)) {
                shouldHide = true;
            }
        }

        // Check for blocked keywords if not already hidden
        if (!shouldHide && blocklistArray.length > 0) {
            const postContent = post.textContent.toLowerCase();
            for (const keyword of blocklistArray) {
                const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                const pattern = new RegExp(`\\b${escapedKeyword}(s|es|ies)?\\b`, 'i');
                if (pattern.test(postContent)) {
                    shouldHide = true;
                    break;
                }
            }
        }

        if (shouldHide) {
            post.classList.add('content-filtered');
            const parentArticle = post.closest('article, div[data-testid="post-container"], shreddit-post');
            if (parentArticle) {
                parentArticle.classList.add('content-filtered');
            }
            filteredCount++;
            updateCounter();
        }
    }

    // Initialization function
    async function init() {
        blocklistArray = (await GM.getValue('blocklist', [])).map(item => item.toLowerCase());

        // Initialize menu commands
        updateCounter();

        // Set up MutationObserver to handle dynamically loaded content
        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('article, div[data-testid="post-container"], shreddit-post')) {
                            processPost(node);
                        }
                        node.querySelectorAll('article, div[data-testid="post-container"], shreddit-post')
                            .forEach(processPost);
                    }
                }
            }
        });

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

        // Initial processing of existing posts
        document.querySelectorAll('article, div[data-testid="post-container"], shreddit-post')
            .forEach(processPost);
    }

    // Run initialization
    await init();

})();