2026-03-27 — adds NSFW toggle filter
// ==UserScript==
// @name Old Reddit FilterTools
// @namespace Violentmonkey Scripts
// @match *://old.reddit.com/*
// @match *://www.reddit.com/*
// @match *://reddit.com/*
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// @version 1.3
// @author Crates
// @description 2026-03-27 — adds NSFW toggle filter
// ==/UserScript==
(function() {
'use strict';
/* - Core logic adapted from source */
let state = {
currentOperator: 'lt',
currentFilterValue: null,
currentKeywords: [],
keywordMode: 'include',
currentFlairs: [],
flairMode: 'include',
expandoTypes: [],
expandoMode: 'include',
blockedUsers: [],
nsfwMode: 'all', // 'all' | 'only' | 'hide'
isFilterActive: false,
filterHash: null
};
let knownFlairs = new Set();
let hasFetchedServerFlairs = false;
// --- UTILS & STORAGE (GM API) ---
function generateFilterHash() {
return `${state.currentOperator}|${state.currentFilterValue}|${state.currentKeywords.join(',')}|${state.keywordMode}|${state.currentFlairs.join(',')}|${state.flairMode}|${state.expandoTypes.join(',')}|${state.expandoMode}|${state.blockedUsers.join(',')}|${state.nsfwMode}`;
}
function loadState() {
try {
const saved = GM_getValue('oldRedditPowerMenu', null);
if (saved) {
const p = JSON.parse(saved);
if (p && typeof p === 'object') {
state.currentOperator = (p.currentOperator === 'lt' || p.currentOperator === 'gt') ? p.currentOperator : 'lt';
state.currentFilterValue = p.currentFilterValue ?? null;
state.currentKeywords = Array.isArray(p.currentKeywords) ? p.currentKeywords : [];
state.keywordMode = ['include', 'exclude'].includes(p.keywordMode) ? p.keywordMode : 'include';
state.currentFlairs = Array.isArray(p.currentFlairs) ? p.currentFlairs : [];
state.flairMode = ['include', 'exclude'].includes(p.flairMode) ? p.flairMode : 'include';
state.expandoTypes = Array.isArray(p.expandoTypes) ? p.expandoTypes : [];
state.expandoMode = ['include', 'exclude'].includes(p.expandoMode) ? p.expandoMode : 'include';
state.blockedUsers = Array.isArray(p.blockedUsers) ? p.blockedUsers : [];
state.nsfwMode = ['all', 'only', 'hide'].includes(p.nsfwMode) ? p.nsfwMode : 'all';
state.isFilterActive = Boolean(p.isFilterActive);
state.filterHash = p.filterHash || null;
state.currentFlairs.forEach(f => knownFlairs.add(f));
}
}
} catch(e) {
console.warn('[FilterTools] State load failed:', e);
}
}
function saveState() {
try {
GM_setValue('oldRedditPowerMenu', JSON.stringify(state));
} catch(e) {
console.warn('[FilterTools] State save failed:', e);
}
}
loadState();
// --- CSS STYLES (GearTools-matched) ---
const style = document.createElement('style');
style.textContent = `
/* ===== FilterTools — GearTools-matched styling ===== */
.orfs-tools-li {
position: relative;
}
#pwr-trig {
cursor: pointer;
color: #369;
}
#pwr-trig:hover {
text-decoration: underline;
}
#pwr-trig svg {
vertical-align: middle;
margin-top: -2px;
}
#orfs-menu {
display: none;
position: absolute;
top: 100%;
left: 0;
background: #fff;
border: 1px solid #c7c7c7;
border-radius: 0 0 3px 3px;
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
z-index: 10001;
width: 310px;
font-size: 13px;
padding: 0;
max-height: 85vh;
display: none;
flex-direction: column;
}
#orfs-menu.active { display: flex; }
.orfs-menu-body {
flex: 1;
overflow-y: auto;
scrollbar-width: thin;
}
.orfs-menu-footer {
border-top: 1px solid #c7c7c7;
padding: 10px 12px;
background: #f6f7f8;
display: flex;
gap: 8px;
flex-shrink: 0;
}
/* --- Sections --- */
.orfs-section {
padding: 10px 12px;
border-bottom: 1px solid #ededed;
}
.orfs-section:last-child { border-bottom: none; }
.orfs-section-header {
background: #f6f7f8;
border-bottom: 1px solid #c7c7c7;
padding: 8px 12px;
font-weight: bold;
color: #333;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: -10px -12px 10px -12px;
}
.orfs-section:first-child .orfs-section-header {
margin-top: -10px;
}
.orfs-label {
display: block;
font-size: 12px;
font-weight: bold;
color: #333;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* --- Inputs --- */
.orfs-input {
width: 100%;
padding: 6px 8px;
border: 1px solid #c7c7c7;
font-size: 13px;
box-sizing: border-box;
border-radius: 3px;
background: #fff;
font-family: inherit;
}
.orfs-input:focus { border-color: #369; outline: none; }
.orfs-controls { display: flex; gap: 6px; margin-top: 6px; }
/* --- Buttons --- */
.orfs-op-btn {
min-width: 36px;
height: 30px;
font-size: 14px;
border-radius: 3px;
border: 1px solid #c7c7c7;
background: #f6f7f8;
color: #333;
font-weight: bold;
cursor: pointer;
}
.orfs-op-btn:hover { background: #eee; }
.orfs-mode-btn {
display: inline-block;
padding: 4px 10px;
font-size: 12px;
border-radius: 3px;
border: 1px solid #c7c7c7;
background: #f6f7f8;
color: #555;
font-weight: 500;
cursor: pointer;
text-transform: uppercase;
margin-bottom: 6px;
}
.orfs-mode-btn:hover { background: #eee; color: #333; }
.orfs-pwr-btn {
flex: 1;
padding: 8px 16px;
border-radius: 3px;
border: 1px solid #c7c7c7;
font-weight: 500;
font-size: 12px;
cursor: pointer;
background: #f6f7f8;
color: #333;
}
.orfs-pwr-btn:hover { background: #eee; }
.orfs-btn-primary {
background: #369;
color: #fff;
border-color: #369;
}
.orfs-btn-primary:hover { background: #2a5a8a; }
.orfs-btn-secondary {
background: #f6f7f8;
color: #333;
}
.orfs-btn-secondary:hover { background: #eee; }
/* --- Chips (Expando / Flair) --- */
.orfs-expando-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
margin-top: 6px;
}
.orfs-flair-grid {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 6px;
max-height: 130px;
overflow-y: auto;
scrollbar-width: thin;
}
.orfs-chip {
padding: 5px 10px;
border-radius: 3px;
border: 1px solid #c7c7c7;
background: #f6f7f8;
color: #555;
font-size: 12px;
font-weight: 500;
text-align: center;
cursor: pointer;
user-select: none;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: background 0.15s, border-color 0.15s;
}
.orfs-chip:hover { background: #eee; border-color: #999; }
.orfs-chip.selected { background: #369; color: #fff; border-color: #369; }
.orfs-chip.empty-state {
background: none;
border: none;
color: #888;
cursor: default;
width: 100%;
font-style: italic;
}
/* --- Loader --- */
.orfs-loader { text-align: center; color: #888; font-size: 11px; padding: 5px; display: none; }
/* --- Active / Hidden indicators --- */
.orfs-active-icon { color: #ff4500 !important; font-weight: bold; }
.orfs-hidden { display: none !important; }
/* --- Live counter --- */
#orfs-counter {
display: inline-block;
margin-left: 5px;
font-size: 10px;
font-weight: normal;
color: #888;
vertical-align: middle;
}
#orfs-counter.orfs-counter-active {
color: #5a9e5a;
font-weight: bold;
}
.orfs-active-badge {
background: #5a9e5a;
color: #fff;
font-size: 9px;
padding: 2px 5px;
border-radius: 3px;
margin-left: 6px;
font-weight: normal;
}
/* --- Blocked Users --- */
.orfs-user-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 6px;
max-height: 90px;
overflow-y: auto;
scrollbar-width: thin;
}
.orfs-user-chip {
padding: 3px 8px;
border-radius: 3px;
border: 1px solid #c7c7c7;
background: #f6f7f8;
color: #555;
font-size: 11px;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
}
.orfs-user-chip .orfs-remove-user {
cursor: pointer;
color: #999;
font-weight: bold;
margin-left: 2px;
line-height: 1;
}
.orfs-user-chip .orfs-remove-user:hover { color: #c44; }
/* --- NSFW Toggle --- */
.orfs-nsfw-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 0;
}
.orfs-nsfw-label {
font-size: 12px;
color: #555;
flex-shrink: 0;
}
.orfs-nsfw-btn-group {
display: flex;
border: 1px solid #c7c7c7;
border-radius: 3px;
overflow: hidden;
flex: 1;
}
.orfs-nsfw-btn {
flex: 1;
padding: 5px 0;
font-size: 11px;
font-weight: 500;
text-align: center;
cursor: pointer;
background: #f6f7f8;
color: #555;
border: none;
border-right: 1px solid #c7c7c7;
text-transform: uppercase;
letter-spacing: 0.4px;
transition: background 0.12s, color 0.12s;
}
.orfs-nsfw-btn:last-child { border-right: none; }
.orfs-nsfw-btn:hover { background: #eee; }
.orfs-nsfw-btn.active-all { background: #555; color: #fff; }
.orfs-nsfw-btn.active-only { background: #c0392b; color: #fff; }
.orfs-nsfw-btn.active-hide { background: #369; color: #fff; }
/* --- Inline block button on posts --- */
`;
document.head.appendChild(style);
// --- PARSING & CLASSIFICATION ---
function parseScore(text) {
if (!text || typeof text !== 'string') return NaN;
text = text.trim().replace(/\s+/g, '').toLowerCase();
if (text.includes('•') || text.includes('scorehidden') || text === '—') return NaN;
text = text.replace(/,/g, '');
const isNeg = text[0] === '-';
if (isNeg) text = text.slice(1);
let mult = 1;
if (text.endsWith('k')) { mult = 1000; text = text.slice(0, -1); }
else if (text.endsWith('m')) { mult = 1000000; text = text.slice(0, -1); }
const num = parseFloat(text);
return isNaN(num) ? NaN : (isNeg ? -num : num) * mult;
}
function getExpandoType(post) {
if (post.dataset.et) return post.dataset.et;
let type = 'other';
const linkEl = post.querySelector('a.title');
if (linkEl?.href && linkEl.href.startsWith('http')) {
try {
const url = new URL(linkEl.href);
const host = url.hostname.toLowerCase();
const path = url.pathname.toLowerCase();
if (host.includes('redgifs')) type = 'redgifs';
else if (host.includes('gfycat')) type = 'gfycat';
else if (host.includes('imgur')) type = 'imgur';
else if (host.includes('youtube') || host === 'youtu.be') type = 'youtube';
else if (host.includes('twitter') || host.includes('x.com')) type = 'twitter';
else if (host === 'i.redd.it' || host === 'i.reddituploads.com') type = 'image';
else if (host === 'v.redd.it') type = 'video';
else if (host.includes('twitch') || host.includes('vimeo') || host.includes('soundcloud') || host.includes('kick')) type = 'iframe';
if (type === 'other') {
if (/\.(jpg|jpeg|png|webp|gif)$/i.test(path)) type = 'image';
else if (/\.(mp4|mov|mkv|webm)$/i.test(path)) type = 'video';
}
} catch(e) {}
}
if (type === 'other') {
const expando = post.querySelector('.expando-button');
if (expando) {
if (expando.classList.contains('selftext')) type = 'selftext';
else if (expando.classList.contains('video')) type = 'video';
else if (expando.classList.contains('image')) type = 'image';
}
}
post.dataset.et = type;
return type;
}
// Returns true if the post is marked NSFW
function isNsfwPost(post) {
if (post.dataset.nw !== undefined) return post.dataset.nw === '1';
const nsfw = Boolean(post.querySelector('.nsfw-stamp'));
post.dataset.nw = nsfw ? '1' : '0';
return nsfw;
}
// --- DATA FETCHING (Subreddit & Flairs) ---
function getCurrentSubreddit() {
const match = window.location.pathname.match(/^\/r\/([^/]+)/);
return match ? match[1] : null;
}
function scanPageFlairs() {
const labels = document.querySelectorAll('.linkflairlabel');
labels.forEach(lbl => {
const txt = lbl.textContent.trim();
if(txt) knownFlairs.add(txt);
});
}
async function fetchServerFlairs(statusEl) {
const sub = getCurrentSubreddit();
if (!sub || ['all', 'popular'].includes(sub)) return;
let uh = document.querySelector('input[name="uh"]')?.value;
if (!uh && typeof unsafeWindow !== 'undefined' && unsafeWindow.reddit) {
uh = unsafeWindow.reddit.modhash;
} else if (!uh && window.wrappedJSObject && window.wrappedJSObject.reddit) {
uh = window.wrappedJSObject.reddit.modhash;
} else if (!uh && window.reddit) {
uh = window.reddit.modhash;
}
if (!uh) return;
if (statusEl) statusEl.style.display = 'block';
try {
const response = await fetch(`https://old.reddit.com/r/${sub}/api/flairselector`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `is_newlink=true&uh=${encodeURIComponent(uh)}`
});
if (response.ok) {
const html = await response.text();
if (html) {
const div = document.createElement('div');
div.innerHTML = html;
const options = div.querySelectorAll('.linkflairlabel, li');
options.forEach(opt => {
if (opt.textContent) knownFlairs.add(opt.textContent.trim());
});
}
}
} catch (e) {
console.warn('[FilterTools] Server flair fetch failed', e);
} finally {
if (statusEl) statusEl.style.display = 'none';
}
}
// --- FILTER LOGIC ---
function updateCounter() {
const counterEl = document.getElementById('orfs-counter');
if (!counterEl) return;
const total = document.querySelectorAll('.thing.link').length;
if (!state.isFilterActive) {
counterEl.textContent = '';
counterEl.classList.remove('orfs-counter-active');
return;
}
const matching = document.querySelectorAll('.thing.link:not(.orfs-hidden)').length;
counterEl.textContent = `${matching} / ${total}`;
counterEl.classList.add('orfs-counter-active');
}
function processPost(post) {
if (!state.isFilterActive) {
post.classList.remove('orfs-hidden');
return;
}
if (post.dataset.fh === state.filterHash) return;
// 1. Expando Type
if (state.expandoTypes.length > 0) {
const type = getExpandoType(post);
const match = state.expandoTypes.includes(type);
if ((state.expandoMode === 'include' && !match) || (state.expandoMode === 'exclude' && match)) {
post.classList.add('orfs-hidden'); post.dataset.fh = state.filterHash; return;
}
}
// 2. Score
if (state.currentFilterValue !== null) {
let score = post.dataset.sc ? parseFloat(post.dataset.sc) : parseScore(post.querySelector('.score.unvoted, .score.likes, .score.dislikes')?.textContent);
post.dataset.sc = score;
if (!isNaN(score)) {
const fail = (state.currentOperator === 'lt' && score >= state.currentFilterValue) ||
(state.currentOperator === 'gt' && score <= state.currentFilterValue);
if (fail) {
post.classList.add('orfs-hidden'); post.dataset.fh = state.filterHash; return;
}
}
}
// 3. Keywords
if (state.currentKeywords.length > 0) {
const title = post.dataset.tt || post.querySelector('a.title')?.textContent.toLowerCase() || '';
post.dataset.tt = title;
const match = state.currentKeywords.some(kw => title.includes(kw));
if ((state.keywordMode === 'include' && !match) || (state.keywordMode === 'exclude' && match)) {
post.classList.add('orfs-hidden'); post.dataset.fh = state.filterHash; return;
}
}
// 4. Flairs
if (state.currentFlairs.length > 0) {
const flair = post.dataset.fl || post.querySelector('.linkflairlabel')?.textContent || '';
post.dataset.fl = flair;
const flairLower = flair.toLowerCase();
const match = state.currentFlairs.some(f => flairLower.includes(f.toLowerCase()));
if ((state.flairMode === 'include' && !match) || (state.flairMode === 'exclude' && match)) {
post.classList.add('orfs-hidden'); post.dataset.fh = state.filterHash; return;
}
}
// 5. Blocked Users
if (state.blockedUsers.length > 0) {
const authorEl = post.querySelector('.author');
const author = authorEl ? authorEl.textContent.toLowerCase() : '';
if (author && state.blockedUsers.some(u => u.toLowerCase() === author)) {
post.classList.add('orfs-hidden'); post.dataset.fh = state.filterHash; return;
}
}
// 6. NSFW
if (state.nsfwMode !== 'all') {
const nsfw = isNsfwPost(post);
if (state.nsfwMode === 'only' && !nsfw) {
post.classList.add('orfs-hidden'); post.dataset.fh = state.filterHash; return;
}
if (state.nsfwMode === 'hide' && nsfw) {
post.classList.add('orfs-hidden'); post.dataset.fh = state.filterHash; return;
}
}
post.classList.remove('orfs-hidden');
post.dataset.fh = state.filterHash;
}
function processAllPosts() {
document.querySelectorAll('.thing.link').forEach(processPost);
updateCounter();
}
// --- NSFW button helper ---
function getNsfwBtnClass(mode) {
if (mode === 'only') return 'active-only';
if (mode === 'hide') return 'active-hide';
return 'active-all';
}
function updateNsfwButtons(container) {
container.querySelectorAll('.orfs-nsfw-btn').forEach(btn => {
btn.classList.remove('active-all', 'active-only', 'active-hide');
if (btn.dataset.mode === state.nsfwMode) {
btn.classList.add(getNsfwBtnClass(state.nsfwMode));
}
});
}
// --- UI RENDERING ---
function goMulti() {
const val = document.getElementById('m-in').value.trim();
if (val) window.location.href = `https://old.reddit.com/r/${val.replace(/[\s,]+/g, '+')}`;
}
function getModeLabel(mode) {
return mode === 'include' ? 'Show Only' : 'Hide';
}
function renderFlairGrid(container) {
container.innerHTML = '';
const sortedFlairs = Array.from(knownFlairs).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
if (sortedFlairs.length === 0) {
container.innerHTML = '<div class="orfs-chip empty-state">No flairs found</div>';
return;
}
sortedFlairs.forEach(fText => {
const chip = document.createElement('div');
chip.className = 'orfs-chip';
chip.textContent = fText;
chip.dataset.flair = fText;
if (state.currentFlairs.includes(fText)) chip.classList.add('selected');
chip.addEventListener('click', (e) => e.target.classList.toggle('selected'));
container.appendChild(chip);
});
}
function renderBlockedUsers(container) {
container.innerHTML = '';
if (state.blockedUsers.length === 0) {
container.innerHTML = '<div class="orfs-chip empty-state">No blocked users</div>';
return;
}
state.blockedUsers.forEach(user => {
const chip = document.createElement('div');
chip.className = 'orfs-user-chip';
const nameSpan = document.createElement('span');
nameSpan.textContent = `u/${user}`;
const removeSpan = document.createElement('span');
removeSpan.className = 'orfs-remove-user';
removeSpan.textContent = '×';
removeSpan.addEventListener('click', () => {
state.blockedUsers = state.blockedUsers.filter(u => u !== user);
renderBlockedUsers(container);
});
chip.appendChild(nameSpan);
chip.appendChild(removeSpan);
container.appendChild(chip);
});
}
function blockUser(username) {
const normalized = username.toLowerCase().trim();
if (normalized && !state.blockedUsers.some(u => u.toLowerCase() === normalized)) {
state.blockedUsers.push(username.trim());
state.filterHash = generateFilterHash();
state.isFilterActive = true;
saveState();
processAllPosts();
const trigger = document.getElementById('pwr-trig');
if (trigger) trigger.classList.add('orfs-active-icon');
const container = document.getElementById('blocked-users-container');
if (container) renderBlockedUsers(container);
}
}
const init = () => {
const tabMenu = document.querySelector('ul.tabmenu');
if (!tabMenu) return;
// Add Toggle Icon inside a positioned <li> (like GearTools)
const triggerLi = document.createElement('li');
triggerLi.className = 'orfs-tools-li';
triggerLi.innerHTML = `<a href="#" class="choice ${state.isFilterActive ? 'orfs-active-icon' : ''}" id="pwr-trig" title="Filter Tools"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="vertical-align:middle;margin-top:-2px;"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg></a><span id="orfs-counter"></span>`;
const gearTrig = document.getElementById('pwr-tools-trig');
const gearLi = gearTrig ? gearTrig.closest('li') : null;
if (gearLi) {
tabMenu.insertBefore(triggerLi, gearLi);
} else {
tabMenu.appendChild(triggerLi);
}
// Build Dropdown Menu (anchored to <li>, like GearTools)
const menu = document.createElement('div');
menu.id = 'orfs-menu';
menu.innerHTML = `
<div class="orfs-menu-body">
<div class="orfs-section">
<div class="orfs-section-header">Score Filter</div>
<div class="orfs-controls" style="margin-top:0">
<button id="op-tog" class="orfs-op-btn">${state.currentOperator === 'lt' ? '<' : '>'}</button>
<input type="number" id="s-val" class="orfs-input" placeholder="e.g. 500" value="${state.currentFilterValue || ''}" enterkeyhint="done">
</div>
</div>
<div class="orfs-section">
<div class="orfs-section-header">Keywords</div>
<div class="orfs-controls" style="margin-top:0;margin-bottom:6px;">
<input type="text" id="k-val" class="orfs-input" placeholder="news, promo..." value="${state.currentKeywords.join(', ')}" enterkeyhint="done">
<button id="mode-tog" class="orfs-mode-btn" style="margin-bottom:0;flex-shrink:0;">${getModeLabel(state.keywordMode)}</button>
</div>
</div>
<div class="orfs-section">
<div class="orfs-section-header">Multi-Hub</div>
<div class="orfs-controls" style="margin-top:0">
<input type="text" id="m-in" class="orfs-input" placeholder="cats+dogs" enterkeyhint="go">
<button class="orfs-op-btn" id="m-go-btn" style="font-size:12px">Go</button>
</div>
</div>
<div class="orfs-section">
<div class="orfs-section-header">Flairs (${getCurrentSubreddit() || 'Local'})</div>
<div id="flair-loader" class="orfs-loader">Fetching subreddit flairs...</div>
<button id="flair-mode-tog" class="orfs-mode-btn">Mode: ${getModeLabel(state.flairMode)}</button>
<div id="flair-container" class="orfs-flair-grid"></div>
</div>
<div class="orfs-section">
<div class="orfs-section-header">Media Type</div>
<button id="exp-mode-tog" class="orfs-mode-btn">Mode: ${getModeLabel(state.expandoMode)}</button>
<div class="orfs-expando-grid">
<div class="orfs-chip" data-type="image">Image</div>
<div class="orfs-chip" data-type="video">Video</div>
<div class="orfs-chip" data-type="redgifs">RedGifs</div>
<div class="orfs-chip" data-type="gfycat">Gfycat</div>
<div class="orfs-chip" data-type="imgur">Imgur</div>
<div class="orfs-chip" data-type="youtube">YouTube</div>
<div class="orfs-chip" data-type="iframe">iFrame</div>
<div class="orfs-chip" data-type="selftext">Self Text</div>
<div class="orfs-chip" data-type="twitter">Twitter</div>
</div>
</div>
<div class="orfs-section">
<div class="orfs-section-header">NSFW</div>
<div class="orfs-nsfw-row">
<span class="orfs-nsfw-label">Show:</span>
<div class="orfs-nsfw-btn-group" id="nsfw-btn-group">
<button class="orfs-nsfw-btn" data-mode="all">All</button>
<button class="orfs-nsfw-btn" data-mode="only">Only NSFW</button>
<button class="orfs-nsfw-btn" data-mode="hide">Hide NSFW</button>
</div>
</div>
</div>
<div class="orfs-section">
<div class="orfs-section-header">Blocked Users</div>
<div class="orfs-controls" style="margin-top:0;margin-bottom:6px;">
<input type="text" id="block-user-input" class="orfs-input" placeholder="username" enterkeyhint="done">
<button class="orfs-op-btn" id="block-user-btn" style="font-size:11px">Block</button>
</div>
<div id="blocked-users-container" class="orfs-user-list"></div>
</div>
</div>
<div class="orfs-menu-footer">
<button id="apply-f" class="orfs-pwr-btn orfs-btn-primary">Apply Filters</button>
<button id="clear-f" class="orfs-pwr-btn orfs-btn-secondary">Clear All</button>
</div>
`;
// Append menu inside the <li> so it anchors like GearTools dropdown
triggerLi.appendChild(menu);
const trigger = document.getElementById('pwr-trig');
const flairContainer = document.getElementById('flair-container');
const flairLoader = document.getElementById('flair-loader');
const nsfwBtnGroup = document.getElementById('nsfw-btn-group');
// Restore UI Selection
state.expandoTypes.forEach(type => {
const chip = menu.querySelector(`.orfs-expando-grid [data-type="${type}"]`);
if (chip) chip.classList.add('selected');
});
// Restore NSFW button state
updateNsfwButtons(nsfwBtnGroup);
// --- EVENT HANDLERS ---
document.addEventListener('click', (e) => {
if (!triggerLi.contains(e.target)) {
menu.classList.remove('active');
}
});
menu.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (e.target.id === 'm-in') goMulti();
else if (e.target.id === 'block-user-input') document.getElementById('block-user-btn').click();
else document.getElementById('apply-f').click();
}
});
document.getElementById('m-go-btn').addEventListener('click', goMulti);
trigger.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
// Close GearTools dropdown if open
const gearDropdown = document.getElementById('pwr-tools-dropdown');
if (gearDropdown) gearDropdown.classList.remove('active');
if (!menu.classList.contains('active')) {
scanPageFlairs();
renderFlairGrid(flairContainer);
if (!hasFetchedServerFlairs) {
hasFetchedServerFlairs = true;
await fetchServerFlairs(flairLoader);
renderFlairGrid(flairContainer);
}
}
menu.classList.toggle('active');
});
document.getElementById('op-tog').addEventListener('click', (e) => {
state.currentOperator = state.currentOperator === 'lt' ? 'gt' : 'lt';
e.target.textContent = state.currentOperator === 'lt' ? '<' : '>';
});
document.getElementById('mode-tog').addEventListener('click', (e) => {
state.keywordMode = state.keywordMode === 'include' ? 'exclude' : 'include';
e.target.textContent = getModeLabel(state.keywordMode);
});
document.getElementById('flair-mode-tog').addEventListener('click', (e) => {
state.flairMode = state.flairMode === 'include' ? 'exclude' : 'include';
e.target.textContent = `Mode: ${getModeLabel(state.flairMode)}`;
});
document.getElementById('exp-mode-tog').addEventListener('click', (e) => {
state.expandoMode = state.expandoMode === 'include' ? 'exclude' : 'include';
e.target.textContent = `Mode: ${getModeLabel(state.expandoMode)}`;
});
menu.querySelectorAll('.orfs-expando-grid .orfs-chip').forEach(chip => {
chip.addEventListener('click', (e) => e.target.classList.toggle('selected'));
});
// NSFW 3-way toggle — clicking the active button resets to 'all'
nsfwBtnGroup.addEventListener('click', (e) => {
const btn = e.target.closest('.orfs-nsfw-btn');
if (!btn) return;
const clicked = btn.dataset.mode;
state.nsfwMode = (state.nsfwMode === clicked && clicked !== 'all') ? 'all' : clicked;
updateNsfwButtons(nsfwBtnGroup);
});
// Blocked Users handlers
const blockedUsersContainer = document.getElementById('blocked-users-container');
renderBlockedUsers(blockedUsersContainer);
document.getElementById('block-user-btn').addEventListener('click', () => {
const input = document.getElementById('block-user-input');
const username = input.value.trim();
if (username) {
blockUser(username);
input.value = '';
}
});
document.getElementById('apply-f').addEventListener('click', () => {
const sIn = document.getElementById('s-val').value.trim();
state.currentFilterValue = (sIn === "") ? null : parseInt(sIn);
const kIn = document.getElementById('k-val').value.trim().toLowerCase();
state.currentKeywords = kIn ? kIn.split(',').map(item => item.trim()).filter(i => i) : [];
state.currentFlairs = Array.from(flairContainer.querySelectorAll('.orfs-chip.selected')).map(c => c.dataset.flair);
state.expandoTypes = Array.from(menu.querySelectorAll('.orfs-expando-grid .orfs-chip.selected')).map(c => c.dataset.type);
// nsfwMode is already updated live via the button group
state.filterHash = generateFilterHash();
state.isFilterActive = true;
trigger.classList.add('orfs-active-icon');
saveState();
processAllPosts();
menu.classList.remove('active');
});
document.getElementById('clear-f').addEventListener('click', () => {
state.isFilterActive = false;
state.currentKeywords = [];
state.currentFlairs = [];
state.currentFilterValue = null;
state.expandoTypes = [];
state.blockedUsers = [];
state.nsfwMode = 'all';
state.filterHash = null;
document.getElementById('k-val').value = "";
document.getElementById('s-val').value = "";
document.getElementById('block-user-input').value = "";
menu.querySelectorAll('.orfs-chip').forEach(chip => chip.classList.remove('selected'));
renderBlockedUsers(blockedUsersContainer);
updateNsfwButtons(nsfwBtnGroup);
trigger.classList.remove('orfs-active-icon');
document.querySelectorAll('.thing.link').forEach(p => {
p.classList.remove('orfs-hidden');
delete p.dataset.fh;
delete p.dataset.sc;
delete p.dataset.tt;
delete p.dataset.et;
delete p.dataset.fl;
delete p.dataset.nw;
});
saveState();
updateCounter();
menu.classList.remove('active');
});
// --- OBSERVER (Infinite Scroll Support) ---
const targetNode = document.getElementById('siteTable');
if (targetNode) {
new MutationObserver((mutations) => {
let found = false;
mutations.forEach(m => {
m.addedNodes.forEach(n => {
if (n.nodeType === 1) {
if (n.classList.contains('thing') && n.classList.contains('link')) {
if(state.isFilterActive) processPost(n);
found = true;
} else if (n.firstElementChild) {
const nested = n.querySelectorAll('.thing.link');
if (nested.length) {
if(state.isFilterActive) nested.forEach(processPost);
found = true;
}
}
}
});
});
if (found) {
scanPageFlairs();
updateCounter();
}
}).observe(targetNode, { childList: true, subtree: true });
}
scanPageFlairs();
if (state.isFilterActive) {
processAllPosts();
updateCounter();
}
};
if (document.readyState === 'interactive' || document.readyState === 'complete') {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();