FunnyJunk Ultimate Manager

Automated Thumbs Up/Down. Settings page with themes, list transfer, and more.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

Advertisement:

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         FunnyJunk Ultimate Manager
// @namespace    http://tampermonkey.net/
// @version      10.0
// @description  Automated Thumbs Up/Down. Settings page with themes, list transfer, and more.
// @author       Emanon
// @match        *://funnyjunk.com/*
// @match        *://*.funnyjunk.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    console.log("FJ Manager: Script execution started.");

    const loadInterval = setInterval(() => {
        if (document.body) {
            clearInterval(loadInterval);
            try {
                initScript();
            } catch (e) {
                console.error("FJ Manager: CRITICAL FAILURE during init:", e);
                alert("FJ Manager Error: " + e.message);
            }
        }
    }, 100);

    function initScript() {

        // ─── STATE ───────────────────────────────────────────────────────────────
        let isRunning = false;
        let currentMode = 'skip';
        let permanentBlocklist = new Set();
        let tempTargetSet = new Set();
        let isSettingsOpen = false;

        // ─── SETTINGS STORAGE KEY ────────────────────────────────────────────────
        const STORAGE_KEY   = 'fj_user_blocklist';
        const SETTINGS_KEY  = 'fj_manager_settings';

        // ─── THEMES ──────────────────────────────────────────────────────────────
        const THEMES = {
            green:  { name: 'Green',   accent: '#4CAF50', accentHover: '#66BB6A', accentText: '#fff' },
            blue:   { name: 'Blue',    accent: '#2196F3', accentHover: '#42A5F5', accentText: '#fff' },
            purple: { name: 'Purple',  accent: '#9C27B0', accentHover: '#AB47BC', accentText: '#fff' },
            red:    { name: 'Red',     accent: '#F44336', accentHover: '#EF5350', accentText: '#fff' },
            orange: { name: 'Orange',  accent: '#FF9800', accentHover: '#FFA726', accentText: '#fff' },
            cyan:   { name: 'Cyan',    accent: '#00BCD4', accentHover: '#26C6DA', accentText: '#fff' },
            pink:   { name: 'Pink',    accent: '#E91E63', accentHover: '#EC407A', accentText: '#fff' },
            mono:   { name: 'Mono',    accent: '#9E9E9E', accentHover: '#BDBDBD', accentText: '#fff' },
        };

        // ─── DEFAULT SETTINGS ────────────────────────────────────────────────────
        let settings = {
            theme: 'green',
            defaultMode: 'skip',
            defaultMinimized: false,
        };

        function loadSettings() {
            try {
                const saved = localStorage.getItem(SETTINGS_KEY);
                if (saved) settings = Object.assign(settings, JSON.parse(saved));
            } catch(e) {}
            currentMode = settings.defaultMode;
        }

        function saveSettings() {
            localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
            applyTheme();
            modeSelect.value = currentMode;
        }

        // ─── BLOCKLIST STORAGE ───────────────────────────────────────────────────
        function loadBlocklist() {
            try {
                const saved = localStorage.getItem(STORAGE_KEY);
                if (saved) permanentBlocklist = new Set(JSON.parse(saved));
            } catch (e) { permanentBlocklist = new Set(); }
        }

        function saveBlocklist() {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(permanentBlocklist)));
            renderBlocklistUI();
        }

        // ─── HELPERS ─────────────────────────────────────────────────────────────
        function getUsernameFromLink(linkElement) {
            if (!linkElement) return null;
            const href = linkElement.getAttribute('href');
            if (!href) return null;
            const match = href.match(/\/user\/([^\/]+)/);
            return (match && match[1]) ? match[1].toLowerCase() : null;
        }

        function addToBlocklist(username) {
            if (!username) return;
            permanentBlocklist.add(username.toLowerCase().trim());
            saveBlocklist();
        }

        function removeFromBlocklist(username) {
            permanentBlocklist.delete(username);
            saveBlocklist();
        }

        function triggerHumanClick(element) {
            if (!element) return;
            ['mouseover', 'mousedown', 'mouseup', 'click'].forEach(type => {
                element.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
            });
        }

        // ─── THEME APPLICATION ───────────────────────────────────────────────────
        function applyTheme() {
            const t = THEMES[settings.theme] || THEMES.green;
            mainPanel.style.borderColor = isRunning ? '#FF0000' : t.accent;
            btn.style.backgroundColor = t.accent;
            settingsBtn.style.backgroundColor = t.accent;
            restoreBtn.style.backgroundColor = t.accent;

            // Update theme swatch selections in settings if open
            document.querySelectorAll('.fj-theme-swatch').forEach(sw => {
                sw.style.outline = (sw.dataset.theme === settings.theme)
                    ? `3px solid #fff` : '3px solid transparent';
            });

            // Update mode select highlight
            if (modeSelect.value !== currentMode) modeSelect.value = currentMode;
        }

        // ─── SCAN & INJECT ───────────────────────────────────────────────────────
        function performScan() {
            const thumbs = document.querySelectorAll('span[id^="up_"]');
            const usersOnPage = new Set();
            let injectedCount = 0;

            thumbs.forEach(thumb => {
                const container = thumb.closest('div[id^="c"]');
                if (!container) return;
                const userLink = container.querySelector('a[href*="/user/"]');
                if (userLink) {
                    const cleanName = getUsernameFromLink(userLink);
                    if (cleanName) usersOnPage.add(cleanName);
                    if (!container.querySelector('.fj-auto-checkbox')) {
                        const checkbox = document.createElement('input');
                        checkbox.type = 'checkbox';
                        checkbox.className = 'fj-auto-checkbox';
                        checkbox.dataset.cleanName = cleanName;
                        Object.assign(checkbox.style, {
                            marginRight: '8px', cursor: 'pointer',
                            width: '18px', height: '18px',
                            accentColor: THEMES[settings.theme].accent,
                            verticalAlign: 'middle'
                        });
                        checkbox.title = `Target User: ${cleanName}`;
                        userLink.parentNode.insertBefore(checkbox, userLink);
                        injectedCount++;
                    }
                }
            });

            updatePageUserDropdown(usersOnPage);
            return injectedCount;
        }

        function buildTempList() {
            tempTargetSet.clear();
            document.querySelectorAll('.fj-auto-checkbox:checked').forEach(box => {
                if (box.dataset.cleanName) tempTargetSet.add(box.dataset.cleanName);
            });
        }

        // ─── CLICK ENGINE ────────────────────────────────────────────────────────
        function clickNextThumb() {
            if (!isRunning) return;
            const allThumbs = document.querySelectorAll('span[id^="up_"]');
            const visibleThumbs = Array.from(allThumbs)
                .filter(el => !el.hasAttribute('data-processed') && el.offsetParent !== null);

            if (visibleThumbs.length > 0) {
                const upBtn = visibleThumbs[0];
                const container = upBtn.closest('div[id^="c"]');
                let currentName = null;
                let isDirectlyChecked = false;

                if (container) {
                    currentName = getUsernameFromLink(container.querySelector('a[href*="/user/"]'));
                    const localCheckbox = container.querySelector('.fj-auto-checkbox');
                    if (localCheckbox && localCheckbox.checked) isDirectlyChecked = true;
                }

                const isTargeted = (currentName && permanentBlocklist.has(currentName))
                    || (currentName && tempTargetSet.has(currentName))
                    || isDirectlyChecked;

                if (isTargeted) {
                    if (currentMode === 'downvote') {
                        let downBtn = upBtn.id?.startsWith('up_')
                            ? document.getElementById(upBtn.id.replace('up_', 'dn_'))
                            : null;
                        if (!downBtn && container) downBtn = container.querySelector('.thDn, .thDn_i');
                        if (downBtn) {
                            if (!(downBtn.className.includes('_i') || downBtn.classList.length > 1))
                                triggerHumanClick(downBtn);
                        }
                    }
                    upBtn.setAttribute('data-processed', 'true');
                } else {
                    const isUpActive = upBtn.className.includes('_i') || upBtn.classList.length > 1;
                    if (!isUpActive) triggerHumanClick(upBtn);
                    else upBtn.setAttribute('data-processed', 'true');
                }

                statusText.innerText = `${visibleThumbs.length - 1} left`;
                setTimeout(clickNextThumb, 150);
            } else {
                finishClicking();
            }
        }

        function finishClicking() {
            isRunning = false;
            const t = THEMES[settings.theme] || THEMES.green;
            statusText.innerText = 'Finished!';
            mainPanel.style.borderColor = t.accent;
            setTimeout(() => { statusText.innerText = 'Ready'; }, 3000);
        }

        function toggleProcess() {
            if (isRunning) {
                isRunning = false;
                statusText.innerText = 'Stopped';
                mainPanel.style.borderColor = THEMES[settings.theme].accent;
                return;
            }
            performScan();
            buildTempList();
            statusText.innerText = 'Running...';
            mainPanel.style.borderColor = '#FF0000';
            isRunning = true;
            clickNextThumb();
        }

        // ─── EXPORT / IMPORT HELPERS ─────────────────────────────────────────────
        function exportList() {
            if (permanentBlocklist.size === 0) return '';
            const payload = JSON.stringify({ v: 1, users: Array.from(permanentBlocklist).sort() });
            return btoa(unescape(encodeURIComponent(payload)));
        }

        function importList(str) {
            // Returns { added: number, dupes: number } or null on error
            try {
                const decoded = decodeURIComponent(escape(atob(str.trim())));
                const parsed = JSON.parse(decoded);
                if (!parsed.users || !Array.isArray(parsed.users)) return null;
                let added = 0, dupes = 0;
                parsed.users.forEach(u => {
                    const clean = u.toLowerCase().trim();
                    if (permanentBlocklist.has(clean)) dupes++;
                    else { permanentBlocklist.add(clean); added++; }
                });
                if (added > 0) saveBlocklist();
                return { added, dupes };
            } catch(e) { return null; }
        }

        function bulkAdd(raw) {
            const names = raw.split(/[\n,]+/).map(s => s.toLowerCase().trim()).filter(Boolean);
            let added = 0;
            names.forEach(n => { if (!permanentBlocklist.has(n)) { permanentBlocklist.add(n); added++; } });
            if (added > 0) saveBlocklist();
            return { added, total: names.length };
        }

        // ═══════════════════════════════════════════════════════════════════════
        // ─── UI CONSTRUCTION ──────────────────────────────────────────────────
        // ═══════════════════════════════════════════════════════════════════════

        // Inject global styles for the settings overlay
        const styleEl = document.createElement('style');
        styleEl.textContent = `
            .fj-settings-overlay {
                position: fixed; inset: 0; background: rgba(0,0,0,0.7);
                z-index: 2147483646; display: flex;
                align-items: center; justify-content: center;
            }
            .fj-settings-panel {
                background: #1a1a1a; color: #fff;
                border-radius: 12px; width: 380px; max-height: 85vh;
                overflow-y: auto; box-shadow: 0 8px 32px rgba(0,0,0,0.9);
                font-family: Arial, sans-serif; font-size: 13px;
            }
            .fj-settings-panel::-webkit-scrollbar { width: 5px; }
            .fj-settings-panel::-webkit-scrollbar-track { background: #111; }
            .fj-settings-panel::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
            .fj-settings-header {
                display: flex; align-items: center; justify-content: space-between;
                padding: 14px 16px; border-bottom: 1px solid #333;
                position: sticky; top: 0; background: #1a1a1a; z-index: 1;
            }
            .fj-settings-header h2 { margin: 0; font-size: 15px; font-weight: bold; }
            .fj-settings-close {
                cursor: pointer; background: #333; border: none; color: #fff;
                width: 28px; height: 28px; border-radius: 50%;
                font-size: 16px; display: flex; align-items: center; justify-content: center;
            }
            .fj-settings-close:hover { background: #555; }
            .fj-section {
                padding: 14px 16px; border-bottom: 1px solid #2a2a2a;
            }
            .fj-section-title {
                font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
                color: #888; margin-bottom: 10px; font-weight: bold;
            }
            .fj-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
            .fj-theme-swatch {
                width: 28px; height: 28px; border-radius: 50%; cursor: pointer;
                border: none; transition: outline 0.15s;
            }
            .fj-toggle-row {
                display: flex; align-items: center; justify-content: space-between;
                padding: 6px 0;
            }
            .fj-toggle-label { color: #ccc; font-size: 13px; }
            .fj-toggle {
                position: relative; width: 38px; height: 21px;
            }
            .fj-toggle input { opacity: 0; width: 0; height: 0; }
            .fj-slider {
                position: absolute; inset: 0; background: #444;
                border-radius: 21px; cursor: pointer; transition: background 0.2s;
            }
            .fj-slider:before {
                content: ''; position: absolute; width: 15px; height: 15px;
                left: 3px; top: 3px; background: #fff; border-radius: 50%;
                transition: transform 0.2s;
            }
            .fj-toggle input:checked + .fj-slider { background: var(--fj-accent, #4CAF50); }
            .fj-toggle input:checked + .fj-slider:before { transform: translateX(17px); }
            .fj-mode-btn {
                flex: 1; padding: 7px 10px; border-radius: 6px; cursor: pointer;
                border: 1.5px solid #444; background: #2a2a2a; color: #ccc;
                font-size: 12px; font-weight: bold; text-align: center; transition: all 0.15s;
            }
            .fj-mode-btn.active {
                border-color: var(--fj-accent, #4CAF50);
                background: color-mix(in srgb, var(--fj-accent, #4CAF50) 20%, #1a1a1a);
                color: #fff;
            }
            .fj-mode-btn:hover:not(.active) { border-color: #666; color: #fff; }
            .fj-input {
                width: 100%; background: #2a2a2a; border: 1px solid #444;
                color: #fff; border-radius: 6px; padding: 7px 10px;
                font-size: 12px; box-sizing: border-box; resize: vertical;
            }
            .fj-input:focus { outline: none; border-color: var(--fj-accent, #4CAF50); }
            .fj-btn {
                padding: 7px 12px; border-radius: 6px; cursor: pointer; font-size: 12px;
                font-weight: bold; border: none; background: #333; color: #fff; transition: background 0.15s;
            }
            .fj-btn:hover { background: #444; }
            .fj-btn.primary { background: var(--fj-accent, #4CAF50); }
            .fj-btn.primary:hover { background: var(--fj-accent-hover, #66BB6A); }
            .fj-btn.danger { background: #c62828; }
            .fj-btn.danger:hover { background: #e53935; }
            .fj-notice {
                padding: 6px 10px; border-radius: 6px; font-size: 12px;
                margin-top: 6px; display: none;
            }
            .fj-notice.ok { background: #1b5e20; color: #a5d6a7; display: block; }
            .fj-notice.err { background: #b71c1c; color: #ffcdd2; display: block; }
            .fj-notice.info { background: #0d47a1; color: #bbdefb; display: block; }
            .fj-bl-item {
                display: flex; justify-content: space-between; align-items: center;
                padding: 3px 6px; border-radius: 4px; font-size: 12px;
            }
            .fj-bl-item:hover { background: #2a2a2a; }
            .fj-bl-del { cursor: pointer; color: #888; font-size: 14px; padding: 0 4px; }
            .fj-bl-del:hover { color: #f44336; }
            .fj-bl-scroll {
                max-height: 130px; overflow-y: auto; background: #111;
                border: 1px solid #333; border-radius: 6px; padding: 4px;
            }
            .fj-bl-scroll::-webkit-scrollbar { width: 4px; }
            .fj-bl-scroll::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; }
        `;
        document.head.appendChild(styleEl);

        // ─── RESTORE BUTTON ──────────────────────────────────────────────────────
        const restoreBtn = document.createElement('div');
        restoreBtn.innerText = 'M';
        Object.assign(restoreBtn.style, {
            position: 'fixed', top: '100px', right: '10px', zIndex: '2147483647',
            backgroundColor: THEMES[settings.theme].accent, color: 'white',
            width: '30px', height: '30px', borderRadius: '50%',
            display: 'none', justifyContent: 'center', alignItems: 'center',
            cursor: 'pointer', fontWeight: 'bold', fontSize: '14px',
            boxShadow: '0 2px 5px rgba(0,0,0,0.5)', border: '2px solid white'
        });
        restoreBtn.title = 'Restore Manager';

        // ─── MAIN PANEL ──────────────────────────────────────────────────────────
        const mainPanel = document.createElement('div');
        Object.assign(mainPanel.style, {
            position: 'fixed', top: '100px', right: '10px', zIndex: '2147483647',
            backgroundColor: '#222', color: 'white', padding: '10px',
            borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.8)',
            fontFamily: 'Arial, sans-serif', fontSize: '12px', width: '220px',
            border: '2px solid #4CAF50', display: 'flex', flexDirection: 'column', gap: '8px'
        });

        function minimizePanel() {
            mainPanel.style.display = 'none';
            restoreBtn.style.display = 'flex';
        }

        function restorePanel() {
            mainPanel.style.display = 'flex';
            restoreBtn.style.display = 'none';
        }

        restoreBtn.addEventListener('click', restorePanel);

        // ─── HEADER ROW ──────────────────────────────────────────────────────────
        const headerRow = document.createElement('div');
        Object.assign(headerRow.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center' });

        const statusText = document.createElement('div');
        statusText.innerText = 'Ready';
        statusText.style.fontWeight = 'bold';
        statusText.style.fontSize = '14px';

        const headerBtns = document.createElement('div');
        headerBtns.style.display = 'flex';
        headerBtns.style.gap = '4px';

        // Settings button
        const settingsBtn = document.createElement('div');
        settingsBtn.innerHTML = '⚙';
        settingsBtn.title = 'Settings';
        Object.assign(settingsBtn.style, {
            cursor: 'pointer', fontWeight: 'bold', padding: '0 5px',
            backgroundColor: THEMES[settings.theme].accent,
            color: '#fff', border: '1px solid #555', borderRadius: '3px',
            fontSize: '13px', lineHeight: '20px'
        });

        // Minimize button
        const minBtn = document.createElement('div');
        minBtn.innerText = '_';
        minBtn.style.cursor = 'pointer';
        minBtn.style.fontWeight = 'bold';
        minBtn.style.padding = '0 5px';
        minBtn.style.color = '#aaa';
        minBtn.title = 'Hide Panel';
        minBtn.style.border = '1px solid #555';
        minBtn.style.borderRadius = '3px';
        minBtn.addEventListener('click', minimizePanel);

        headerBtns.appendChild(settingsBtn);
        headerBtns.appendChild(minBtn);
        headerRow.appendChild(statusText);
        headerRow.appendChild(headerBtns);

        // ─── START/STOP BUTTON ───────────────────────────────────────────────────
        const btn = document.createElement('button');
        btn.innerText = 'START / STOP';
        Object.assign(btn.style, {
            backgroundColor: THEMES[settings.theme].accent, color: 'white',
            border: 'none', padding: '8px', cursor: 'pointer',
            fontWeight: 'bold', borderRadius: '4px'
        });
        btn.addEventListener('click', toggleProcess);

        // ─── MODE SELECT ─────────────────────────────────────────────────────────
        const modeSelect = document.createElement('select');
        modeSelect.style.padding = '5px';
        modeSelect.add(new Option('Mode: Skip Targets', 'skip'));
        modeSelect.add(new Option('Mode: Downvote Targets', 'downvote'));
        modeSelect.addEventListener('change', (e) => { currentMode = e.target.value; });

        // ─── DIVIDER ─────────────────────────────────────────────────────────────
        const divider = document.createElement('hr');
        Object.assign(divider.style, { width: '100%', borderColor: '#555', margin: '5px 0' });

        // ─── BLOCKLIST SECTION ───────────────────────────────────────────────────
        const listHeader = document.createElement('div');
        listHeader.innerText = 'Permanent List [+]';
        Object.assign(listHeader.style, {
            fontWeight: 'bold', cursor: 'pointer', userSelect: 'none',
            padding: '5px', backgroundColor: '#333', borderRadius: '4px', textAlign: 'center'
        });

        const listContent = document.createElement('div');
        Object.assign(listContent.style, { display: 'none', flexDirection: 'column', gap: '8px' });

        let isListExpanded = false;
        listHeader.addEventListener('click', () => {
            isListExpanded = !isListExpanded;
            listContent.style.display = isListExpanded ? 'flex' : 'none';
            listHeader.innerText = isListExpanded ? 'Permanent List [-]' : 'Permanent List [+]';
        });

        const refreshRow = document.createElement('div');
        refreshRow.style.display = 'flex';
        refreshRow.style.gap = '5px';

        const pageUserSelect = document.createElement('select');
        pageUserSelect.style.width = '100%';
        pageUserSelect.innerHTML = '<option value="">-- Load Users --</option>';

        const refreshBtn = document.createElement('button');
        refreshBtn.innerText = '⟳';
        refreshBtn.title = 'Scan page for new users';
        refreshBtn.style.cursor = 'pointer';
        refreshBtn.style.fontWeight = 'bold';
        refreshBtn.addEventListener('click', () => {
            const count = performScan();
            statusText.innerText = `Scanned ${count || 'page'}`;
            setTimeout(() => { if (!isRunning) statusText.innerText = 'Ready'; }, 2000);
        });

        refreshRow.appendChild(pageUserSelect);
        refreshRow.appendChild(refreshBtn);

        const addFromPageBtn = document.createElement('button');
        addFromPageBtn.innerText = 'Add Selected User';
        addFromPageBtn.style.cursor = 'pointer';
        addFromPageBtn.addEventListener('click', () => {
            if (pageUserSelect.value) {
                addToBlocklist(pageUserSelect.value);
                pageUserSelect.value = '';
            }
        });

        const manualInput = document.createElement('input');
        manualInput.placeholder = 'Type username...';
        manualInput.style.width = '95%';

        const addManualBtn = document.createElement('button');
        addManualBtn.innerText = 'Add Manual User';
        addManualBtn.style.cursor = 'pointer';
        addManualBtn.addEventListener('click', () => {
            if (manualInput.value) { addToBlocklist(manualInput.value); manualInput.value = ''; }
        });

        const blocklistView = document.createElement('div');
        Object.assign(blocklistView.style, {
            maxHeight: '100px', overflowY: 'auto', backgroundColor: '#333',
            padding: '5px', borderRadius: '4px', border: '1px solid #555'
        });

        function renderBlocklistUI() {
            blocklistView.innerHTML = '';
            if (permanentBlocklist.size === 0) { blocklistView.innerText = '(List empty)'; return; }
            permanentBlocklist.forEach(user => {
                const row = document.createElement('div');
                Object.assign(row.style, { display: 'flex', justifyContent: 'space-between', marginBottom: '2px' });
                const nameSpan = document.createElement('span');
                nameSpan.innerText = user;
                const delBtn = document.createElement('span');
                delBtn.innerText = '❌';
                delBtn.style.cursor = 'pointer';
                delBtn.onclick = () => removeFromBlocklist(user);
                row.appendChild(nameSpan);
                row.appendChild(delBtn);
                blocklistView.appendChild(row);
            });
        }

        function updatePageUserDropdown(usersSet) {
            const previousVal = pageUserSelect.value;
            pageUserSelect.innerHTML = '<option value="">-- Select from Page --</option>';
            const sortedUsers = Array.from(usersSet).sort();
            if (sortedUsers.length === 0) {
                pageUserSelect.innerHTML = '<option value="">No users found yet</option>';
            }
            sortedUsers.forEach(user => pageUserSelect.add(new Option(user, user)));
            if (usersSet.has(previousVal)) pageUserSelect.value = previousVal;
        }

        // ─── ASSEMBLE MAIN PANEL ─────────────────────────────────────────────────
        mainPanel.appendChild(headerRow);
        mainPanel.appendChild(btn);
        mainPanel.appendChild(modeSelect);
        mainPanel.appendChild(divider);
        mainPanel.appendChild(listHeader);
        listContent.appendChild(refreshRow);
        listContent.appendChild(addFromPageBtn);
        listContent.appendChild(manualInput);
        listContent.appendChild(addManualBtn);
        listContent.appendChild(blocklistView);
        mainPanel.appendChild(listContent);

        // ═══════════════════════════════════════════════════════════════════════
        // ─── SETTINGS OVERLAY ────────────────────────────────────────────────
        // ═══════════════════════════════════════════════════════════════════════

        function buildSettingsOverlay() {
            const overlay = document.createElement('div');
            overlay.className = 'fj-settings-overlay';
            overlay.addEventListener('click', (e) => { if (e.target === overlay) closeSettings(overlay); });

            const panel = document.createElement('div');
            panel.className = 'fj-settings-panel';
            // Inject CSS variable for accent
            panel.style.setProperty('--fj-accent', THEMES[settings.theme].accent);
            panel.style.setProperty('--fj-accent-hover', THEMES[settings.theme].accentHover);

            // ── Header ──
            const hdr = document.createElement('div');
            hdr.className = 'fj-settings-header';
            hdr.innerHTML = '<h2>⚙ FJ Manager Settings</h2>';
            const closeX = document.createElement('button');
            closeX.className = 'fj-settings-close';
            closeX.innerHTML = '✕';
            closeX.addEventListener('click', () => closeSettings(overlay));
            hdr.appendChild(closeX);
            panel.appendChild(hdr);

            // ── Section: Defaults ────────────────────────────────────────────
            const secDefaults = document.createElement('div');
            secDefaults.className = 'fj-section';
            secDefaults.innerHTML = '<div class="fj-section-title">Defaults</div>';

            // Default Mode
            const modeLabel = document.createElement('div');
            modeLabel.style.cssText = 'color:#aaa;font-size:12px;margin-bottom:6px;';
            modeLabel.innerText = 'Default mode on startup';
            secDefaults.appendChild(modeLabel);

            const modeRow = document.createElement('div');
            modeRow.className = 'fj-row';
            modeRow.style.marginBottom = '12px';

            const mkModeBtn = (label, val) => {
                const b = document.createElement('div');
                b.className = 'fj-mode-btn' + (settings.defaultMode === val ? ' active' : '');
                b.dataset.val = val;
                b.innerText = label;
                b.addEventListener('click', () => {
                    settings.defaultMode = val;
                    currentMode = val;
                    modeRow.querySelectorAll('.fj-mode-btn').forEach(x => {
                        x.classList.toggle('active', x.dataset.val === val);
                    });
                    modeSelect.value = val;
                });
                return b;
            };
            modeRow.appendChild(mkModeBtn('Skip Targets', 'skip'));
            modeRow.appendChild(mkModeBtn('Downvote Targets', 'downvote'));
            secDefaults.appendChild(modeRow);

            // Default Minimized toggle
            const minToggleRow = document.createElement('div');
            minToggleRow.className = 'fj-toggle-row';
            const minToggleLabel = document.createElement('span');
            minToggleLabel.className = 'fj-toggle-label';
            minToggleLabel.innerText = 'Start minimized by default';
            const minToggleWrap = document.createElement('label');
            minToggleWrap.className = 'fj-toggle';
            const minToggleInput = document.createElement('input');
            minToggleInput.type = 'checkbox';
            minToggleInput.checked = settings.defaultMinimized;
            minToggleInput.addEventListener('change', () => { settings.defaultMinimized = minToggleInput.checked; });
            const minSlider = document.createElement('span');
            minSlider.className = 'fj-slider';
            minToggleWrap.appendChild(minToggleInput);
            minToggleWrap.appendChild(minSlider);
            minToggleRow.appendChild(minToggleLabel);
            minToggleRow.appendChild(minToggleWrap);
            secDefaults.appendChild(minToggleRow);

            panel.appendChild(secDefaults);

            // ── Section: Theme ───────────────────────────────────────────────
            const secTheme = document.createElement('div');
            secTheme.className = 'fj-section';
            secTheme.innerHTML = '<div class="fj-section-title">Color Theme</div>';

            const swatchRow = document.createElement('div');
            swatchRow.className = 'fj-row';
            swatchRow.style.flexWrap = 'wrap';
            swatchRow.style.gap = '10px';

            Object.entries(THEMES).forEach(([key, t]) => {
                const swatch = document.createElement('button');
                swatch.className = 'fj-theme-swatch';
                swatch.dataset.theme = key;
                swatch.style.backgroundColor = t.accent;
                swatch.style.outline = (key === settings.theme) ? '3px solid #fff' : '3px solid transparent';
                swatch.style.outlineOffset = '2px';
                swatch.title = t.name;
                swatch.addEventListener('click', () => {
                    settings.theme = key;
                    panel.style.setProperty('--fj-accent', t.accent);
                    panel.style.setProperty('--fj-accent-hover', t.accentHover);
                    swatchRow.querySelectorAll('.fj-theme-swatch').forEach(sw => {
                        sw.style.outline = (sw.dataset.theme === key) ? '3px solid #fff' : '3px solid transparent';
                    });
                });
                swatchRow.appendChild(swatch);
            });

            const themeNames = document.createElement('div');
            themeNames.style.cssText = 'font-size:11px;color:#666;margin-top:4px;';
            themeNames.innerText = Object.values(THEMES).map(t => t.name).join('  ·  ');

            secTheme.appendChild(swatchRow);
            secTheme.appendChild(themeNames);
            panel.appendChild(secTheme);

            // ── Section: Export ──────────────────────────────────────────────
            const secExport = document.createElement('div');
            secExport.className = 'fj-section';
            secExport.innerHTML = `<div class="fj-section-title">Export List (${permanentBlocklist.size} users)</div>`;

            const exportTA = document.createElement('textarea');
            exportTA.className = 'fj-input';
            exportTA.readOnly = true;
            exportTA.rows = 3;
            exportTA.placeholder = 'Click Generate to create export string...';
            exportTA.style.fontFamily = 'monospace';

            const exportRow = document.createElement('div');
            exportRow.className = 'fj-row';
            exportRow.style.marginTop = '6px';

            const genBtn = document.createElement('button');
            genBtn.className = 'fj-btn primary';
            genBtn.innerText = 'Generate';
            genBtn.addEventListener('click', () => {
                const str = exportList();
                if (!str) { showNotice(exportNotice, 'List is empty', 'err'); return; }
                exportTA.value = str;
                showNotice(exportNotice, `${permanentBlocklist.size} users exported`, 'ok');
            });

            const copyBtn = document.createElement('button');
            copyBtn.className = 'fj-btn';
            copyBtn.innerText = '📋 Copy';
            copyBtn.addEventListener('click', () => {
                if (!exportTA.value) { showNotice(exportNotice, 'Generate first', 'err'); return; }
                navigator.clipboard.writeText(exportTA.value).then(() => {
                    showNotice(exportNotice, 'Copied to clipboard!', 'ok');
                }).catch(() => {
                    exportTA.select();
                    showNotice(exportNotice, 'Select all + Ctrl+C to copy', 'info');
                });
            });

            const exportNotice = document.createElement('div');
            exportNotice.className = 'fj-notice';

            exportRow.appendChild(genBtn);
            exportRow.appendChild(copyBtn);
            secExport.appendChild(exportTA);
            secExport.appendChild(exportRow);
            secExport.appendChild(exportNotice);
            panel.appendChild(secExport);

            // ── Section: Import ──────────────────────────────────────────────
            const secImport = document.createElement('div');
            secImport.className = 'fj-section';
            secImport.innerHTML = '<div class="fj-section-title">Import &amp; Merge</div>';

            const importTA = document.createElement('textarea');
            importTA.className = 'fj-input';
            importTA.rows = 3;
            importTA.placeholder = 'Paste an exported list string here...';
            importTA.style.fontFamily = 'monospace';

            const importRow = document.createElement('div');
            importRow.className = 'fj-row';
            importRow.style.marginTop = '6px';

            const importBtn = document.createElement('button');
            importBtn.className = 'fj-btn primary';
            importBtn.innerText = 'Merge into List';
            const importNotice = document.createElement('div');
            importNotice.className = 'fj-notice';

            importBtn.addEventListener('click', () => {
                const result = importList(importTA.value);
                if (!result) {
                    showNotice(importNotice, 'Invalid export string — check and try again', 'err');
                    return;
                }
                importTA.value = '';
                showNotice(importNotice, `Added ${result.added} user(s) · ${result.dupes} already in list`, 'ok');
                renderBlocklistUI();
                // Update export section heading
                secExport.querySelector('.fj-section-title').innerText = `Export List (${permanentBlocklist.size} users)`;
            });

            importRow.appendChild(importBtn);
            secImport.appendChild(importTA);
            secImport.appendChild(importRow);
            secImport.appendChild(importNotice);
            panel.appendChild(secImport);

            // ── Section: Bulk Add ────────────────────────────────────────────
            const secBulk = document.createElement('div');
            secBulk.className = 'fj-section';
            secBulk.innerHTML = '<div class="fj-section-title">Bulk Add Usernames</div>';

            const bulkTA = document.createElement('textarea');
            bulkTA.className = 'fj-input';
            bulkTA.rows = 3;
            bulkTA.placeholder = 'One per line or comma-separated\nuser1\nuser2, user3';

            const bulkRow = document.createElement('div');
            bulkRow.className = 'fj-row';
            bulkRow.style.marginTop = '6px';

            const bulkBtn = document.createElement('button');
            bulkBtn.className = 'fj-btn primary';
            bulkBtn.innerText = 'Add All';
            const bulkNotice = document.createElement('div');
            bulkNotice.className = 'fj-notice';

            bulkBtn.addEventListener('click', () => {
                if (!bulkTA.value.trim()) { showNotice(bulkNotice, 'Enter some usernames first', 'err'); return; }
                const result = bulkAdd(bulkTA.value);
                bulkTA.value = '';
                showNotice(bulkNotice, `Added ${result.added} new · ${result.total - result.added} already in list`, 'ok');
                renderBlocklistUI();
            });

            bulkRow.appendChild(bulkBtn);
            secBulk.appendChild(bulkTA);
            secBulk.appendChild(bulkRow);
            secBulk.appendChild(bulkNotice);
            panel.appendChild(secBulk);

            // ── Section: Current List Preview ────────────────────────────────
            const secList = document.createElement('div');
            secList.className = 'fj-section';
            secList.innerHTML = `<div class="fj-section-title">Current Blocklist (${permanentBlocklist.size})</div>`;

            const blScroll = document.createElement('div');
            blScroll.className = 'fj-bl-scroll';

            const renderSettingsBL = () => {
                blScroll.innerHTML = '';
                if (permanentBlocklist.size === 0) {
                    blScroll.innerHTML = '<div style="color:#666;font-size:12px;padding:4px;">(empty)</div>';
                    return;
                }
                Array.from(permanentBlocklist).sort().forEach(u => {
                    const row = document.createElement('div');
                    row.className = 'fj-bl-item';
                    const nameEl = document.createElement('span');
                    nameEl.innerText = u;
                    const delEl = document.createElement('span');
                    delEl.className = 'fj-bl-del';
                    delEl.innerText = '✕';
                    delEl.onclick = () => {
                        removeFromBlocklist(u);
                        renderSettingsBL();
                        secList.querySelector('.fj-section-title').innerText = `Current Blocklist (${permanentBlocklist.size})`;
                    };
                    row.appendChild(nameEl);
                    row.appendChild(delEl);
                    blScroll.appendChild(row);
                });
            };
            renderSettingsBL();
            secList.appendChild(blScroll);
            panel.appendChild(secList);

            // ── Footer: Save ─────────────────────────────────────────────────
            const footer = document.createElement('div');
            footer.className = 'fj-section';
            footer.style.display = 'flex';
            footer.style.gap = '8px';

            const saveBtn = document.createElement('button');
            saveBtn.className = 'fj-btn primary';
            saveBtn.style.flex = '1';
            saveBtn.innerText = '✔ Save Settings';
            saveBtn.addEventListener('click', () => {
                saveSettings();
                closeSettings(overlay);
            });

            const cancelBtn = document.createElement('button');
            cancelBtn.className = 'fj-btn';
            cancelBtn.innerText = 'Cancel';
            cancelBtn.addEventListener('click', () => closeSettings(overlay));

            footer.appendChild(saveBtn);
            footer.appendChild(cancelBtn);
            panel.appendChild(footer);

            overlay.appendChild(panel);
            return overlay;
        }

        function showNotice(el, msg, type) {
            el.className = `fj-notice ${type}`;
            el.innerText = msg;
        }

        function openSettings() {
            if (isSettingsOpen) return;
            isSettingsOpen = true;
            const overlay = buildSettingsOverlay();
            document.body.appendChild(overlay);
        }

        function closeSettings(overlay) {
            isSettingsOpen = false;
            overlay.remove();
        }

        settingsBtn.addEventListener('click', openSettings);

        // ─── INJECT INTO PAGE ────────────────────────────────────────────────────
        document.body.appendChild(restoreBtn);
        document.body.appendChild(mainPanel);
        console.log("FJ Manager: UI Injected successfully.");

        // ─── INIT ────────────────────────────────────────────────────────────────
        loadSettings();
        loadBlocklist();
        renderBlocklistUI();
        modeSelect.value = currentMode;
        applyTheme();

        if (settings.defaultMinimized) {
            minimizePanel();
        }

        setTimeout(performScan, 2000);
    }
})();