Old Reddit FilterTools

2026-03-27 — adds NSFW toggle filter

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==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' ? '&lt;' : '&gt;'}</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);
    }
})();