NeoGAF EQ

The Forum Equalizer: Restored Import/Export. Full Visual EQ. Live Remarks. Tiered Deep Black.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         NeoGAF EQ
// @namespace    http://tampermonkey.net/
// @version      2.9.4
// @author       bj00rn & Gemini AI
// @description  The Forum Equalizer: Restored Import/Export. Full Visual EQ. Live Remarks. Tiered Deep Black.
// @match        *://*.neogaf.com/*
// @license      MIT
// @homepageURL  https://greasyfork.org/scripts/521313-neogaf-eq
// @grant        none
// ==/UserScript==

/*
 * MIT License
 * Copyright (c) 2025 bj00rn
 */

(function() {
    'use strict';

    const loadData = (key) => {
        try { return JSON.parse(localStorage.getItem(key) || "{}"); } catch(e) { return {}; }
    };

    let mutedData = loadData('ng_muted_users');
    let boostedData = loadData('ng_boosted_users');
    
    let hideLabels = localStorage.getItem('ng_hide_labels') === 'true';
    let hideOrbs = localStorage.getItem('ng_hide_orbs') === 'true'; 
    let muteGifted = localStorage.getItem('ng_mute_gifted') === 'true';
    let symmetricDates = localStorage.getItem('ng_symmetric_dates') === 'true';
    let hideDesc = localStorage.getItem('ng_hide_desc') === 'true';
    let minimalBread = localStorage.getItem('ng_minimal_bread') === 'true';
    let minimalNav = localStorage.getItem('ng_minimal_nav') === 'true';
    let fadedDetails = localStorage.getItem('ng_faded_details') === 'true';
    let use24h = localStorage.getItem('ng_24h_clock') === 'true';
    let useEuroDates = localStorage.getItem('ng_euro_dates') === 'true';
    let cleanTitles = localStorage.getItem('ng_clean_titles') === 'true';
    let greyTitles = localStorage.getItem('ng_grey_titles') === 'true';
    let deepBlack = localStorage.getItem('ng_deep_black') === 'true';
    let showHighlights = localStorage.getItem('ng_show_highlights') !== 'false';
    
    let needsRefresh = false;
    let isLocked = false;

    const injectStyles = () => {
        if (document.getElementById('ng-power-styles')) return;
        const styleSheet = document.createElement("style");
        styleSheet.id = 'ng-power-styles';
        styleSheet.textContent = `
            #ng-management-hub { background: rgba(128, 128, 128, 0.1) !important; border: 1px solid rgba(128, 128, 128, 0.2) !important; padding: 8px 15px !important; margin: 10px 0 !important; color: #aaa !important; font-family: monospace; font-size: 11px; display: flex; justify-content: space-between; align-items: center; border-radius: 4px; clear: both; }
            .ng-muted-row { background: rgba(128, 128, 128, 0.08); border: 1px solid rgba(128, 128, 128, 0.15); padding: 4px 12px; margin: 6px 0 !important; display: flex; justify-content: space-between; color: gray; font-size: 11px; border-radius: 2px; height: 18px; align-items: center; clear: both; }
            
            .ng-deep-black [style*="background-color: #27282a"], .ng-deep-black .p-body, .ng-deep-black .p-pageWrapper { background-color: #0a0a0a !important; }
            .ng-deep-black .p-nav-inner { background-color: #141414 !important; }

            .ng-faded-details .structItem-cell--meta, .ng-faded-details .structItem-cell--latest { opacity: 0.4 !important; transition: opacity 0.2s; }
            .ng-faded-details .structItem:hover .structItem-cell--meta, .ng-faded-details .structItem:hover .structItem-cell--latest { opacity: 1 !important; }

            .ng-clean-titles .structItem-title a:hover { text-decoration: none !important; }
            .ng-grey-titles .structItem-title a { color: #ccc !important; }
            
            .ng-hide-labels .label { display: none !important; }
            .ng-hide-orbs .structItem-cell--statuses, .ng-hide-orbs .icon-orb { display: none !important; }
            .ng-mute-gifted .structItem.is-gifted { background: transparent !important; background-image: none !important; }
            .ng-symmetric-dates .structItem-latestDate { font-size: 85% !important; color: #8c8c8c !important; }
            .ng-hide-desc .p-description { display: none !important; }
            .ng-minimal-bread .p-breadcrumbs { background: transparent !important; border: none !important; box-shadow: none !important; }
            .ng-minimal-nav .structItem-pageJump a { display: none !important; }
            .ng-minimal-nav .structItem-pageJump a:last-child { display: inline-block !important; padding: 1px 12px !important; background: rgba(128,128,128,0.15) !important; }

            .ng-show-highlights .ng-boosted-thread { border-left: 4px solid rgba(40, 167, 69, 0.6) !important; background: rgba(40, 167, 69, 0.03) !important; }
            .ng-show-highlights .ng-muted-thread { border-left: 4px solid rgba(204, 51, 51, 0.4) !important; opacity: 0.6; }
            .ng-boosted-name { color: #28a745 !important; font-weight: bold !important; }
            
            #ng-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 10005; display: none; align-items: center; justify-content: center; }
            #ng-modal-window { background: #1a1a1a; border: 1px solid #444; color: #eee; width: 95%; max-width: 950px; height: 85vh; border-radius: 6px; display: flex; flex-direction: column; overflow: hidden; }
            #ng-modal-sidebar { width: 200px; background: #222; border-right: 1px solid #333; padding: 15px; display: flex; flex-direction: column; gap: 4px; overflow-y: auto; }
            .ng-sidebar-label { font-size: 10px; color: #888; font-weight: bold; text-transform: uppercase; margin-top: 8px; }
            .ng-toggle-item { display: flex; align-items: center; gap: 8px; font-size: 11px; cursor: pointer; color: #ccc; }
            .ng-data-btn { background: #333; color: #fff; border: 1px solid #444; padding: 5px; font-size: 10px; cursor: pointer; text-align: center; border-radius: 3px; margin-top: 5px; }
            .ng-modal-row { background: #2a2a2a; padding: 6px 12px; border-radius: 3px; display: flex; justify-content: space-between; font-size: 11px; align-items: center; border: 1px solid #333; margin-bottom: 3px; }
            .ng-editable-remark { flex-grow: 1; color: #888; font-style: italic; padding: 0 15px; cursor: text; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
            .ng-edit-input { background: #111; color: #fff; border: 1px solid #cc3333; font-size: 11px; width: 100%; }
        `;
        document.head.appendChild(styleSheet);
        applyVisualToggles();
    };

    const applyVisualToggles = () => {
        const doc = document.documentElement;
        doc.classList.toggle('ng-hide-labels', hideLabels);
        doc.classList.toggle('ng-hide-orbs', hideOrbs);
        doc.classList.toggle('ng-mute-gifted', muteGifted);
        doc.classList.toggle('ng-symmetric-dates', symmetricDates);
        doc.classList.toggle('ng-hide-desc', hideDesc);
        doc.classList.toggle('ng-minimal-bread', minimalBread);
        doc.classList.toggle('ng-minimal-nav', minimalNav);
        doc.classList.toggle('ng-faded-details', fadedDetails);
        doc.classList.toggle('ng-clean-titles', cleanTitles);
        doc.classList.toggle('ng-grey-titles', greyTitles);
        doc.classList.toggle('ng-deep-black', deepBlack);
        doc.classList.toggle('ng-show-highlights', showHighlights);
    };

    const updateStorage = (type, data) => {
        localStorage.setItem(type === 'mute' ? 'ng_muted_users' : 'ng_boosted_users', JSON.stringify(data));
        needsRefresh = true;
    };

    const exportData = () => {
        const bundle = { boosted: boostedData, muted: mutedData };
        const blob = new Blob([JSON.stringify(bundle, null, 2)], { type: 'application/json' });
        const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'NeoGAF_EQ_Backup.json'; a.click();
    };

    const importData = () => {
        const input = document.createElement('input'); input.type = 'file'; input.accept = '.json';
        input.onchange = e => {
            const reader = new FileReader();
            reader.onload = f => {
                const data = JSON.parse(f.target.result);
                if (data.boosted) { boostedData = { ...boostedData, ...data.boosted }; updateStorage('boost', boostedData); }
                if (data.muted) { mutedData = { ...mutedData, ...data.muted }; updateStorage('mute', mutedData); }
                alert("Imported. Reloading..."); location.reload();
            };
            reader.readAsText(e.target.files[0]);
        };
        input.click();
    };

    window.openControlPanel = () => {
        let overlay = document.getElementById('ng-modal-overlay');
        if (!overlay) {
            overlay = document.createElement('div'); overlay.id = 'ng-modal-overlay';
            overlay.innerHTML = `
                <div id="ng-modal-window">
                    <div style="display:flex; justify-content:space-between; background: #111; padding: 12px 20px; border-bottom: 1px solid #333;">
                        <strong>NEOGAF EQ CONTROL PANEL</strong>
                        <span onclick="window.closeControlPanel()" style="cursor:pointer; color:gray; font-size:10px;">[Close]</span>
                    </div>
                    <div style="display:flex; flex-grow:1; overflow:hidden;">
                        <div id="ng-modal-sidebar">
                            <span class="ng-sidebar-label">Visual EQ</span>
                            <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-labels" ${hideLabels ? 'checked' : ''}> Hide Prefixes</label>
                            <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-orbs" ${hideOrbs ? 'checked' : ''}> Hide Orbs</label>
                            <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-gifted" ${muteGifted ? 'checked' : ''}> Mute Gifted</label>
                            <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-symmetric" ${symmetricDates ? 'checked' : ''}> Symmetry</label>
                            <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-desc" ${hideDesc ? 'checked' : ''}> Hide Desc</label>
                            <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-bread" ${minimalBread ? 'checked' : ''}> Min Bread</label>
                            <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-nav" ${minimalNav ? 'checked' : ''}> Min Nav</label>
                            <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-fade" ${fadedDetails ? 'checked' : ''}> Fade Details</label>
                            <span class="ng-sidebar-label">Quiet Mode</span>
                            <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-clean" ${cleanTitles ? 'checked' : ''}> Clean Titles</label>
                            <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-grey" ${greyTitles ? 'checked' : ''}> Grey Titles</label>
                            <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-black" ${deepBlack ? 'checked' : ''}> Deep Black</label>
                            <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-highlights" ${showHighlights ? 'checked' : ''}> Highlights</label>
                            <span class="ng-sidebar-label">Data Utility</span>
                            <div class="ng-data-btn" id="ng-btn-export">Export Data</div>
                            <div class="ng-data-btn" id="ng-btn-import">Import Data</div>
                        </div>
                        <div style="flex-grow:1; overflow-y:auto; padding: 20px;" id="ng-modal-grid"></div>
                    </div>
                </div>`;
            document.body.appendChild(overlay);
            document.getElementById('ng-toggle-labels').onchange = (e) => { hideLabels = e.target.checked; localStorage.setItem('ng_hide_labels', hideLabels); applyVisualToggles(); };
            document.getElementById('ng-toggle-orbs').onchange = (e) => { hideOrbs = e.target.checked; localStorage.setItem('ng_hide_orbs', hideOrbs); applyVisualToggles(); };
            document.getElementById('ng-toggle-gifted').onchange = (e) => { muteGifted = e.target.checked; localStorage.setItem('ng_mute_gifted', muteGifted); applyVisualToggles(); };
            document.getElementById('ng-toggle-symmetric').onchange = (e) => { symmetricDates = e.target.checked; localStorage.setItem('ng_symmetric_dates', symmetricDates); applyVisualToggles(); };
            document.getElementById('ng-toggle-desc').onchange = (e) => { hideDesc = e.target.checked; localStorage.setItem('ng_hide_desc', hideDesc); applyVisualToggles(); };
            document.getElementById('ng-toggle-bread').onchange = (e) => { minimalBread = e.target.checked; localStorage.setItem('ng_minimal_bread', minimalBread); applyVisualToggles(); };
            document.getElementById('ng-toggle-nav').onchange = (e) => { minimalNav = e.target.checked; localStorage.setItem('ng_minimal_nav', minimalNav); applyVisualToggles(); };
            document.getElementById('ng-toggle-fade').onchange = (e) => { fadedDetails = e.target.checked; localStorage.setItem('ng_faded_details', fadedDetails); applyVisualToggles(); };
            document.getElementById('ng-toggle-clean').onchange = (e) => { cleanTitles = e.target.checked; localStorage.setItem('ng_clean_titles', cleanTitles); applyVisualToggles(); };
            document.getElementById('ng-toggle-grey').onchange = (e) => { greyTitles = e.target.checked; localStorage.setItem('ng_grey_titles', greyTitles); applyVisualToggles(); };
            document.getElementById('ng-toggle-black').onchange = (e) => { deepBlack = e.target.checked; localStorage.setItem('ng_deep_black', deepBlack); applyVisualToggles(); };
            document.getElementById('ng-toggle-highlights').onchange = (e) => { showHighlights = e.target.checked; localStorage.setItem('ng_show_highlights', showHighlights); applyVisualToggles(); };
            document.getElementById('ng-btn-export').onclick = exportData;
            document.getElementById('ng-btn-import').onclick = importData;
        }
        const grid = document.getElementById('ng-modal-grid'); grid.innerHTML = '';
        const renderSection = (title, data, type) => {
            const h = document.createElement('div'); h.style.padding = '10px 0'; h.style.color = '#888'; h.innerText = title; grid.appendChild(h);
            Object.keys(data).sort((a,b)=>a.localeCompare(b)).forEach(user => {
                const row = document.createElement('div'); row.className = 'ng-modal-row';
                const remarkSpan = document.createElement('span'); remarkSpan.className = 'ng-editable-remark';
                remarkSpan.innerText = data[user].remark ? `"${data[user].remark}"` : '"Click to add note"';
                remarkSpan.onclick = () => {
                    const input = document.createElement('input'); input.className = 'ng-edit-input'; input.value = data[user].remark || "";
                    input.onblur = () => { data[user].remark = input.value; updateStorage(type, data); remarkSpan.innerText = input.value ? `"${input.value}"` : '"Click to add note"'; input.replaceWith(remarkSpan); };
                    input.onkeydown = (e) => { if (e.key === 'Enter') input.blur(); };
                    remarkSpan.replaceWith(input); input.focus();
                };
                row.innerHTML = `<span style="width: 150px;"><b>${user}</b></span>`;
                row.appendChild(remarkSpan);
                const del = document.createElement('span'); del.style.cursor='pointer'; del.style.color='gray'; del.innerText='✕';
                del.onclick = () => { delete data[user]; updateStorage(type, data); row.remove(); };
                row.appendChild(del);
                grid.appendChild(row);
            });
        };
        renderSection('BOOSTED', boostedData, 'boost'); renderSection('MUTED', mutedData, 'mute');
        overlay.style.display = 'flex';
    };

    window.closeControlPanel = () => { document.getElementById('ng-modal-overlay').style.display = 'none'; if (needsRefresh) location.reload(); };

    function runEQ() {
        if (isLocked) return; isLocked = true; injectStyles();
        let hub = document.getElementById('ng-management-hub');
        if (!hub) {
            const target = document.querySelector('.p-body-main');
            if (target) { hub = document.createElement('div'); hub.id = 'ng-management-hub'; target.parentNode.insertBefore(hub, target); }
        }

        document.querySelectorAll('.structItem--thread').forEach(thread => {
            const userAnchor = thread.querySelector('.structItem-minor li:first-child a.username') || thread.querySelector('.structItem-minor a[data-user-id]');
            if (userAnchor) {
                const user = userAnchor.innerText.trim();
                if (boostedData[user]) { thread.classList.add('ng-boosted-thread'); userAnchor.classList.add('ng-boosted-name'); }
                else if (mutedData[user]) { thread.classList.add('ng-muted-thread'); }
            }
        });

        document.querySelectorAll('.message:not(.ng-processed)').forEach(msg => {
            msg.classList.add('ng-processed');
            const nameEl = msg.querySelector('a[itemprop="name"]') || msg.querySelector('.username');
            const titleArea = msg.querySelector('.message-userTitle') || msg.querySelector('.userTitle');
            if (!nameEl || !titleArea) return;
            const user = nameEl.innerText.trim();
            const isM = !!mutedData[user], isB = !!boostedData[user];
            
            if (isM) {
                msg.style.display = 'none';
                const row = document.createElement('div'); row.className = 'ng-muted-row';
                row.innerHTML = `<span>Muted: <b>${user}</b> ("${mutedData[user].remark || ""}")</span><span style="cursor:pointer;" onclick="this.closest('.ng-muted-row').nextElementSibling.style.display='block'; this.remove();">[Show]</span>`;
                msg.parentNode.insertBefore(row, msg);
            }

            const cl = document.createElement('div'); cl.style.fontSize = '10px'; cl.style.opacity = '0.6';
            cl.innerHTML = `<span style="cursor:pointer;">${isM ? '[UM]' : '[M]'}</span> | <span style="cursor:pointer;">${isB ? '[UB]' : '[B]'}</span>`;
            cl.querySelectorAll('span')[0].onclick = () => { if (isM) delete mutedData[user]; else { const r = prompt("Reason?"); if (r !== null) mutedData[user] = { remark: r || "" }; else return; } updateStorage('mute', mutedData); location.reload(); };
            cl.querySelectorAll('span')[1].onclick = () => { if (isB) delete boostedData[user]; else { boostedData[user] = { remark: "" }; delete mutedData[user]; } updateStorage('boost', boostedData); location.reload(); };
            titleArea.parentNode.insertBefore(cl, titleArea.nextSibling);
        });

        if (hub) hub.innerHTML = `<div>NEOGAF EQ | Muted ${Object.keys(mutedData).length} | Boosted ${Object.keys(boostedData).length}</div><div><span onclick="window.openControlPanel()" style="cursor:pointer; color:gray;">[Control Panel]</span></div>`;
        setTimeout(() => { isLocked = false; }, 100);
    }

    const observer = new MutationObserver(runEQ);
    observer.observe(document.body, { childList: true, subtree: true });
    runEQ();
})();