old.reddit.com regex Post Filter (Block subreddits by regex, users, block titles by regex)

Filters posts on old.reddit.com by subreddit, user, or title regex. Persistently stores all rules and hides or colors posts with minimal CPU usage.

目前為 2025-11-23 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         old.reddit.com regex Post Filter (Block subreddits by regex, users, block titles by regex)
// @namespace    jjenkx
// @version      1.0
// @license      MIT
// @description  Filters posts on old.reddit.com by subreddit, user, or title regex. Persistently stores all rules and hides or colors posts with minimal CPU usage.
// @match        https://old.reddit.com/*
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
    'use strict';

    // The script uses color codes to highlight why a post matched a rule
    // when filters are disabled. When filters are enabled posts are hidden.
    const COLORS = {
        titleRegex: '#ff9999',
        blockedSubreddits: '#dcdcdc',
        subredditRegexList: '#ffd27f',
        blockedUsers: '#a6c8ff',
        default: ''
    };

    // Keys for persistent storage. All rule lists and state are saved through GM.setValue.
    const KEY_BLOCKED_SUBS = 'blockedSubreddits';
    const KEY_BLOCKED_USERS = 'blockedUsers';
    const KEY_SUB_REGEX_LIST = 'subredditRegexList';
    const KEY_TITLE_REGEX_LIST = 'titleRegexList';
    const KEY_FILTERS_ENABLED = 'filtersEnabled';

    // Working sets and arrays for rules. Regex lists are compiled at load for efficiency.
    let blockedSubreddits = new Set();
    let blockedUsers = new Set();
    let subredditRegexList = [];
    let titleRegexList = [];
    let compiledSubredditRegexList = [];
    let compiledTitleRegexList = [];
    let filtersEnabled = true;
    let hiddenPostCount = 0;

    // These caches store recent subreddit and user checks to avoid repeating work.
    const subCache = new Map();
    const userCache = new Map();

    let observer, postContainer;

    // Load all persistent values. Everything is read once at startup.
    const loadAll = async () => {
        const [subs, users, subRegexes, titleRegexes, enabled] = await Promise.all([
            GM.getValue(KEY_BLOCKED_SUBS, '[]'),
            GM.getValue(KEY_BLOCKED_USERS, '[]'),
            GM.getValue(KEY_SUB_REGEX_LIST, '[]'),
            GM.getValue(KEY_TITLE_REGEX_LIST, '[]'),
            GM.getValue(KEY_FILTERS_ENABLED, true)
        ]);

        blockedSubreddits = new Set(JSON.parse(subs));
        blockedUsers = new Set(JSON.parse(users));

        try { subredditRegexList = JSON.parse(subRegexes); } catch { subredditRegexList = []; }
        try { titleRegexList = JSON.parse(titleRegexes); } catch { titleRegexList = []; }

        compiledSubredditRegexList = subredditRegexList.map(compileRegexSafely).filter(Boolean);
        compiledTitleRegexList = titleRegexList.map(compileRegexSafely).filter(Boolean);
        filtersEnabled = enabled;
    };

    // Save helpers
    const save = (key, value) => GM.setValue(key, JSON.stringify([...value]));
    const saveFiltersEnabled = () => GM.setValue(KEY_FILTERS_ENABLED, filtersEnabled);

    const saveRegexList = async () => {
        await GM.setValue(KEY_SUB_REGEX_LIST, JSON.stringify(subredditRegexList));
        compiledSubredditRegexList = subredditRegexList.map(compileRegexSafely).filter(Boolean);
    };

    const saveTitleRegexList = async () => {
        await GM.setValue(KEY_TITLE_REGEX_LIST, JSON.stringify(titleRegexList));
        compiledTitleRegexList = titleRegexList.map(compileRegexSafely).filter(Boolean);
    };

    // Turns a user provided regex string into a RegExp object without throwing.
    function compileRegexSafely(src) {
        if (typeof src !== 'string' || !src.trim()) return null;
        const s = src.trim();
        try {
            const m = s.match(/^\/(.+)\/([a-z]*)$/i);
            return m ? new RegExp(m[1], m[2]) : new RegExp(s, 'i');
        } catch {
            return null;
        }
    }

    // Matches a subreddit string against the compiled regex list.
    function matchSubredditRegex(sub) {
        for (let i = 0; i < compiledSubredditRegexList.length; i++) {
            const rx = compiledSubredditRegexList[i];
            try { if (rx.test(sub)) return i; } catch {}
        }
        return -1;
    }

    // Matches a title against the compiled title regex list.
    function matchTitleRegex(title) {
        for (let i = 0; i < compiledTitleRegexList.length; i++) {
            const rx = compiledTitleRegexList[i];
            try { if (rx.test(title)) return i; } catch {}
        }
        return -1;
    }

    // Updates the counter displayed on the toggle button.
    const updateButtonCounter = () => {
        const toggle = document.querySelector('.toggle-filters-button');
        if (toggle) {
            toggle.textContent = filtersEnabled
                ? 'Disable Filters (' + hiddenPostCount + ')'
                : 'Enable Filters';
        }
    };

    // Ensures a button exists next to a subreddit or user link. The action toggles block rules.
    const ensureButton = (el, selector, text, onClick) => {
        let btn = el.parentNode.querySelector(selector);
        if (!btn) {
            btn = document.createElement('button');
            btn.className = selector.replace('.', '');
            Object.assign(btn.style, {
                marginLeft: '3px', cursor: 'pointer', fontSize: '5px', padding: '0 1px'
            });
            btn.addEventListener('click', onClick);
            el.insertAdjacentElement('afterend', btn);
        }
        btn.textContent = text;
    };

    // Applies or updates block buttons for subreddit and user for a specific post.
    function manageButtons(post, sub, subEl, user, userEl) {
        if (subEl) {
            const blocked = blockedSubreddits.has(sub);
            ensureButton(subEl, '.block-subreddit-button', blocked ? 'Unblock Sub' : 'Block Sub', async () => {
                if (blocked) {
                    blockedSubreddits.delete(sub);
                } else {
                    blockedSubreddits.add(sub);
                    post.remove();
                }
                await save(KEY_BLOCKED_SUBS, blockedSubreddits);
                queueMicrotask(() => hideAllPosts());
            });
        }

        if (userEl) {
            const blocked = blockedUsers.has(user);
            ensureButton(userEl, '.block-user-button', blocked ? 'Unblock User' : 'Block User', async () => {
                blocked ? blockedUsers.delete(user) : blockedUsers.add(user);
                await save(KEY_BLOCKED_USERS, blockedUsers);
                hideSinglePost(post);
            });
        }
    }

    // Adds a toggle button to enable or disable all filters without removing them.
    function addFilterToggleButton() {
        const menu = document.querySelector('.tabmenu');
        if (menu && !document.querySelector('.toggle-filters-button')) {
            const btn = document.createElement('button');
            btn.className = 'toggle-filters-button';
            Object.assign(btn.style, {
                marginLeft: '5px', cursor: 'pointer', fontSize: '10px', padding: '0 2px'
            });
            btn.addEventListener('click', async () => {
                filtersEnabled = !filtersEnabled;
                await saveFiltersEnabled();
                hideAllPosts();
            });
            menu.appendChild(btn);
            updateButtonCounter();
        }
    }

    // Checks if a subreddit is blocked by name or by regex. Uses cache to avoid repeats.
    function isBlockedSub(sub) {
        if (subCache.has(sub)) return subCache.get(sub);
        const idx = matchSubredditRegex(sub);
        const res = { exact: blockedSubreddits.has(sub), regexIndex: idx };
        subCache.set(sub, res);
        return res;
    }

    // Checks if a user is blocked. Uses cache.
    function isBlockedUser(user) {
        if (userCache.has(user)) return userCache.get(user);
        const res = blockedUsers.has(user);
        userCache.set(user, res);
        return res;
    }

    // This function evaluates a single post, decides if it should be hidden, colored,
    // or left alone, and applies user control buttons.
    function hideSinglePost(post) {
        const titleEl = post.querySelector('a.title');
        const subEl = post.querySelector('a.subreddit');
        const userEl = post.querySelector('a.author');
        if (!titleEl || !subEl || !userEl) return;

        const title = titleEl.textContent;
        const sub = subEl.textContent.replace('/r/', '').trim();
        const user = userEl.textContent.trim();

        const tMatchIndex = matchTitleRegex(title);
        const tMatch = tMatchIndex >= 0;
        const subRes = isBlockedSub(sub);
        const sMatch = subRes.exact;
        const rMatch = subRes.regexIndex >= 0;
        const uMatch = isBlockedUser(user);

        const shouldHide = filtersEnabled && (tMatch || sMatch || rMatch || uMatch);

        if (shouldHide) {
            if (post.style.display !== 'none') post.style.display = 'none';
            hiddenPostCount++;
        } else {
            if (post.style.display === 'none') post.style.display = '';
            let color = COLORS.default;
            if (!filtersEnabled) {
                if (tMatch) color = COLORS.titleRegex;
                else if (sMatch) color = COLORS.blockedSubreddits;
                else if (rMatch) color = COLORS.subredditRegexList;
                else if (uMatch) color = COLORS.blockedUsers;
            }
            if (post.style.backgroundColor !== color) post.style.backgroundColor = color;
            manageButtons(post, sub, subEl, user, userEl);
        }
    }

    // Reprocesses all posts in the listing and reconnects the observer afterward.
    function hideAllPosts() {
        if (observer) observer.disconnect();
        hiddenPostCount = 0;
        for (const post of document.querySelectorAll('.thing')) hideSinglePost(post);
        updateButtonCounter();
        if (observer) observePostContainer();
    }

    // Mutation observer callback. Only reacts to new post nodes, minimizing CPU usage.
    function observePostContainer() {
        postContainer = document.querySelector('#siteTable');
        if (!postContainer) return;

        observer = new MutationObserver((mutations) => {
            for (const m of mutations) {
                for (const node of m.addedNodes) {
                    if (node.nodeType === 1 && node.matches('.thing')) hideSinglePost(node);
                }
            }
        });

        observer.observe(postContainer, { childList: true, subtree: false });
    }

    // Exposes menus to modify regex lists interactively.
    function registerMenu() {
        if (typeof GM_registerMenuCommand !== 'function') return;
        if (window.__regexMenuBound) return;
        window.__regexMenuBound = true;

        GM_registerMenuCommand('Add Subreddit Regex', async () => {
            const input = prompt('Enter subreddit regex to add:');
            if (input === null) return;
            subredditRegexList.push(input);
            await saveRegexList();
            hideAllPosts();
        });

        GM_registerMenuCommand('List Subreddit Regexes', () => {
            alert(subredditRegexList.length
                ? subredditRegexList.map((s, i) => i + ': ' + s).join('\n')
                : '(none)');
        });

        GM_registerMenuCommand('Remove Subreddit Regex', async () => {
            if (!subredditRegexList.length) return alert('Empty list.');
            const idx = Number(prompt(subredditRegexList.map((s, i) => i + ': ' + s).join('\n')));
            if (Number.isInteger(idx) && idx >= 0 && idx < subredditRegexList.length) {
                subredditRegexList.splice(idx, 1);
                await saveRegexList();
                hideAllPosts();
                alert('Removed.');
            }
        });

        GM_registerMenuCommand('Clear All Subreddit Regexes', async () => {
            if (!subredditRegexList.length) return alert('Already empty.');
            if (!confirm('Clear all subreddit regex entries?')) return;
            subredditRegexList = [];
            await saveRegexList();
            hideAllPosts();
        });

        GM_registerMenuCommand('Add Title Regex', async () => {
            const input = prompt('Enter title regex to add:');
            if (input === null) return;
            titleRegexList.push(input);
            await saveTitleRegexList();
            hideAllPosts();
        });

        GM_registerMenuCommand('List Title Regexes', () => {
            alert(titleRegexList.length
                ? titleRegexList.map((s, i) => i + ': ' + s).join('\n')
                : '(none)');
        });

        GM_registerMenuCommand('Remove Title Regex', async () => {
            if (!titleRegexList.length) return alert('Empty list.');
            const idx = Number(prompt(titleRegexList.map((s, i) => i + ': ' + s).join('\n')));
            if (Number.isInteger(idx) && idx >= 0 && idx < titleRegexList.length) {
                titleRegexList.splice(idx, 1);
                await saveTitleRegexList();
                hideAllPosts();
                alert('Removed.');
            }
        });

        GM_registerMenuCommand('Clear All Title Regexes', async () => {
            if (!titleRegexList.length) return alert('Already empty.');
            if (!confirm('Clear all title regex entries?')) return;
            titleRegexList = [];
            await saveTitleRegexList();
            hideAllPosts();
        });
    }

    // Initialization sequence: loads state, registers menus, and sets up filtering once the page is idle.
    (async () => {
        await loadAll();
        registerMenu();
        window.addEventListener('load', () => {
            requestIdleCallback(() => {
                addFilterToggleButton();
                hideAllPosts();
                observePostContainer();
            });
        });
    })();

})();