NeoGAF EQ

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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

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

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

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

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

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

(I already have a user style manager, let me install it!)

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