Old Reddit FilterTools

2026-03-27 — adds NSFW toggle filter

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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);
    }
})();