您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically hides posts in your Reddit feed based on keywords or subreddits you specify. Supports whitelist entries that override filtering.
// ==UserScript== // @name Reddit Advanced Content Filter // @namespace https://gf.qytechs.cn/en/users/567951-stuart-saddler // @version 2.8 // @description Automatically hides posts in your Reddit feed based on keywords or subreddits you specify. Supports whitelist entries that override filtering. // @author ... // @license MIT // @icon https://clipart-library.com/images_k/smoke-clipart-transparent/smoke-clipart-transparent-6.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== (async function () { 'use strict'; console.log('[DEBUG] Script started. Reddit Advanced Content Filter.'); // ----------------------------------------------- // Utility: Debounce function to prevent spam calls // ----------------------------------------------- function debounce(func, wait) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } // ----------------------- // Selectors & Script Vars // ----------------------- // NOTE: .thing => old.reddit.com // article, div[data-testid="post-container"], shreddit-post => new.reddit.com const postSelector = 'article, div[data-testid="post-container"], shreddit-post, .thing'; let filteredCount = 0; let menuCommand = null; // track the menu command ID, so we can unregister if needed let processedPosts = new WeakSet(); let blocklistSet = new Set(); let keywordPattern = null; let whitelistSet = new Set(); let whitelistPattern = null; let pendingUpdates = 0; // ----------------------------------- // Attempt to (re)register the menu item // ----------------------------------- function updateMenuEntry() { // If GM_registerMenuCommand is unavailable, just ensure fallback button is present if (typeof GM_registerMenuCommand !== 'function') { createFallbackButton(); return; } // If it is available, let's try to unregister the old one (if supported) try { if (menuCommand !== null && typeof GM_unregisterMenuCommand === 'function') { GM_unregisterMenuCommand(menuCommand); } } catch (err) { // Some userscript managers might not support GM_unregisterMenuCommand at all console.warn('[DEBUG] Could not unregister menu command:', err); } // Register the new menu command with updated blocked count menuCommand = GM_registerMenuCommand(`Configure Filter (${filteredCount} blocked)`, showConfig); } // ---------------------------------------- // Fallback Button (if menu is unsupported) // ---------------------------------------- function createFallbackButton() { // Check if it’s already on the page if (document.getElementById('reddit-filter-fallback-btn')) { // Just update the label with the new count document.getElementById('reddit-filter-fallback-btn').textContent = `Configure Filter (${filteredCount} blocked)`; return; } // Otherwise create a brand new button const button = document.createElement('button'); button.id = 'reddit-filter-fallback-btn'; button.textContent = `Configure Filter (${filteredCount} blocked)`; button.style.cssText = 'position:fixed;top:10px;right:10px;z-index:999999;padding:8px;'; button.addEventListener('click', showConfig); document.body.appendChild(button); } // --------------------------------------------------------------------- // Debounced function to update the menu/fallback button (blocking count) // --------------------------------------------------------------------- const batchUpdateCounter = debounce(() => { updateMenuEntry(); }, 16); // ----------------- // CSS for Hide Class // ----------------- if (!document.querySelector('style[data-reddit-filter]')) { const style = document.createElement('style'); style.textContent = ` .content-filtered { display: none !important; height: 0 !important; overflow: hidden !important; } `; style.setAttribute('data-reddit-filter', 'true'); document.head.appendChild(style); } // --------------- // Build Patterns // --------------- function getKeywordPattern(keywords) { if (keywords.length === 0) return null; const escapedKeywords = keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); // The trailing (s|es|ies)? is used to match common plurals return new RegExp(`\\b(${escapedKeywords.join('|')})(s|es|ies)?\\b`, 'i'); } // -------------------------------------------- // Show the Config Dialog for Block/Whitelist // -------------------------------------------- async function showConfig() { const overlay = document.createElement('div'); overlay.className = 'reddit-filter-overlay'; Object.assign(overlay.style, { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.5)', zIndex: '999999' }); const dialog = document.createElement('div'); dialog.className = 'reddit-filter-dialog'; Object.assign(dialog.style, { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', background: 'white', padding: '20px', borderRadius: '8px', zIndex: '1000000', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', minWidth: '300px', maxWidth: '350px', fontFamily: 'Arial, sans-serif', color: '#333' }); // Basic styling for elements inside the dialog dialog.innerHTML = ` <h2 style="margin-top:0; color:#0079d3;">Reddit Filter: Settings</h2> <p><strong>Blocklist:</strong> One entry per line. Matching posts will be hidden.</p> <textarea spellcheck="false" id="blocklist" style="width:100%; height:80px; margin-bottom:10px;"></textarea> <p><strong>Whitelist:</strong> One entry per line. If matched, post is NOT hidden.</p> <textarea spellcheck="false" id="whitelist" style="width:100%; height:80px;"></textarea> <div style="display:flex; justify-content:flex-end; margin-top:10px; gap:10px;"> <button id="cancel-btn" style="padding:6px 12px;">Cancel</button> <button id="save-btn" style="padding:6px 12px; background:#0079d3; color:white;">Save</button> </div> `; document.body.appendChild(overlay); document.body.appendChild(dialog); // Populate with existing data dialog.querySelector('#blocklist').value = Array.from(blocklistSet).join('\n'); dialog.querySelector('#whitelist').value = Array.from(whitelistSet).join('\n'); const closeDialog = () => { overlay.remove(); dialog.remove(); }; // Cancel / overlay click => close dialog.querySelector('#cancel-btn').addEventListener('click', closeDialog); overlay.addEventListener('click', (e) => { // Close if user clicks the overlay, but not if user clicked inside the dialog if (e.target === overlay) { closeDialog(); } }); // Save => persist dialog.querySelector('#save-btn').addEventListener('click', async () => { const blocklistInput = dialog.querySelector('#blocklist').value; blocklistSet = new Set( blocklistInput .split('\n') .map(x => x.trim().toLowerCase()) .filter(x => x.length > 0) ); keywordPattern = getKeywordPattern(Array.from(blocklistSet)); await GM.setValue('blocklist', Array.from(blocklistSet)); const whitelistInput = dialog.querySelector('#whitelist').value; whitelistSet = new Set( whitelistInput .split('\n') .map(x => x.trim().toLowerCase()) .filter(x => x.length > 0) ); whitelistPattern = getKeywordPattern(Array.from(whitelistSet)); await GM.setValue('whitelist', Array.from(whitelistSet)); closeDialog(); location.reload(); // easiest way to re-filter everything }); } // ----------------------------------------- // Process an Individual Post (Hide or Not) // ----------------------------------------- function processPost(post) { if (!post || processedPosts.has(post)) return; processedPosts.add(post); const contentText = post.textContent.toLowerCase(); // If whitelisted => skip if (whitelistPattern && whitelistPattern.test(contentText)) return; let shouldHide = false; // Old + New Reddit subreddit link // old.reddit => .tagline a.subreddit // new.reddit => a[data-click-id="subreddit"] or a.subreddit const subredditElement = post.querySelector('a[data-click-id="subreddit"], a.subreddit, .tagline a.subreddit'); if (subredditElement) { const subName = subredditElement.textContent.trim().replace(/^r\//i, '').toLowerCase(); if (blocklistSet.has(subName)) { shouldHide = true; } } // If not yet hidden => check keywords if (!shouldHide && keywordPattern) { if (keywordPattern.test(contentText)) { shouldHide = true; } } if (shouldHide) { hidePost(post); } } // --------------- // Hide Post Helper // --------------- function hidePost(post) { post.classList.add('content-filtered'); const parentArticle = post.closest(postSelector); if (parentArticle) { parentArticle.classList.add('content-filtered'); } filteredCount++; pendingUpdates++; batchUpdateCounter(); } // ------------------------------------------- // Process a Batch of Posts (in small chunks) // ------------------------------------------- async function processPostsBatch(posts) { const batchSize = 5; for (let i = 0; i < posts.length; i += batchSize) { const batch = posts.slice(i, i + batchSize); // Use requestIdleCallback to keep page responsive await new Promise(resolve => requestIdleCallback(resolve, { timeout: 800 })); batch.forEach(processPost); } } const debouncedProcess = debounce((posts) => { processPostsBatch(Array.from(posts)); }, 100); // ---------------------------- // Initialization (load config) // ---------------------------- async function init() { try { const loadedBlocklist = await GM.getValue('blocklist', []); blocklistSet = new Set(loadedBlocklist.map(x => x.toLowerCase())); keywordPattern = getKeywordPattern(Array.from(blocklistSet)); const loadedWhitelist = await GM.getValue('whitelist', []); whitelistSet = new Set(loadedWhitelist.map(x => x.toLowerCase())); whitelistPattern = getKeywordPattern(Array.from(whitelistSet)); } catch (err) { console.error('[DEBUG] Error loading saved data:', err); } // Try to create a menu entry or fallback button (zero blocked initially) updateMenuEntry(); // On old Reddit, top-level posts appear under #siteTable // On new Reddit, there's .main-content const observerTarget = document.querySelector('.main-content') || document.querySelector('#siteTable') || document.body; const observer = new MutationObserver((mutations) => { const newPosts = new Set(); for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches?.(postSelector)) { newPosts.add(node); } node.querySelectorAll?.(postSelector).forEach(p => newPosts.add(p)); } } } if (newPosts.size > 0) { debouncedProcess(newPosts); } }); observer.observe(observerTarget, { childList: true, subtree: true }); // Process any existing posts on load const initialPosts = document.querySelectorAll(postSelector); if (initialPosts.length > 0) { debouncedProcess(initialPosts); } console.log('[DEBUG] Initialization complete. Now filtering posts...'); } await init(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址