NeoGAF EQ

The Forum Equalizer: Fixed Boosted Counter, Green/Red UI Gold Master.

当前为 2025-12-30 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         NeoGAF EQ
// @namespace    http://tampermonkey.net/
// @version      2.2.2
// @author       bj00rn & Gemini AI
// @description  The Forum Equalizer: Fixed Boosted Counter, Green/Red UI Gold Master.
// @match        *://*.neogaf.com/*
// @license      MIT
// @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 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: 6px 15px !important; margin: 10px 0 !important; color: inherit !important; font-family: monospace; font-size: 11px; 
                display: flex; justify-content: space-between; align-items: center; border-radius: 4px;
            }
            .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-placeholder-actions { display: flex; gap: 10px; }
            .ng-hide-labels .label--primary, .ng-hide-labels .label, .ng-hide-labels .labelLink { display: none !important; }
            .ng-hide-orbs .structItem-cell--statuses, .ng-hide-orbs .structItem-status, .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 > li { background: transparent !important; border: none !important; padding: 0 4px !important; box-shadow: none !important; }
            .ng-minimal-bread .p-breadcrumbs > li i { display: none !important; }

            .ng-boosted-thread { border-left: 4px solid rgba(40, 167, 69, 0.6) !important; background: rgba(40, 167, 69, 0.03) !important; }
            .ng-boosted-name { color: #28a745 !important; font-weight: bold !important; }
            .ng-muted-thread { border-left: 4px solid rgba(204, 51, 51, 0.4) !important; opacity: 0.6; }
            .ng-muted-name { color: #cc3333 !important; font-weight: bold !important; }
            
            .ng-status-tag { font-weight: bold; font-size: 10px; text-transform: uppercase; margin-top: 4px; display: block; }
            .ng-boosted-tag { color: #28a745 !important; }
            .ng-muted-tag { color: #cc3333 !important; }

            #ng-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 10000; display: none; align-items: center; justify-content: center; }
            #ng-modal-window { background: #1a1a1a; border: 1px solid #444; color: #eee; width: 95%; max-width: 900px; max-height: 85vh; border-radius: 6px; display: flex; flex-direction: column; overflow: hidden; }
            #ng-modal-body { display: flex; flex-grow: 1; overflow: hidden; }
            #ng-modal-sidebar { width: 180px; background: #222; border-right: 1px solid #333; padding: 20px 15px; display: flex; flex-direction: column; gap: 20px; }
            .ng-sidebar-label { font-size: 10px; color: #888; font-weight: bold; text-transform: uppercase; }
            .ng-toggle-item { display: flex; align-items: center; gap: 8px; font-size: 11px; cursor: pointer; color: #ccc; }
            #ng-modal-content { flex-grow: 1; padding: 20px; overflow-y: auto; }
            #ng-modal-grid { display: grid; grid-template-columns: 1fr; gap: 5px; }
            .ng-modal-row { background: #2a2a2a; padding: 8px 12px; border-radius: 3px; display: flex; justify-content: space-between; font-size: 11px; align-items: center; border: 1px solid #333; }
            .ng-editable-remark { flex-grow: 1; color: #aaa; font-style: italic; padding: 2px 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
            .ng-edit-input { background: #111; color: #fff; border: 1px solid #cc3333; font-size: 11px; width: 95%; padding: 2px; }
            .ng-action-link { cursor: pointer; color: gray; font-size: 10px; text-decoration: none; }
            .ng-action-cluster { display: flex; justify-content: center; gap: 5px; margin-top: 6px; font-weight: bold; font-size: 10px; color: gray; opacity: 0.6; }
            .ng-btn-item { cursor: pointer; transition: 0.2s; }
            .ng-btn-item:hover { color: #fff; opacity: 1; }
        `;
        document.head.appendChild(styleSheet);
        applyVisualToggles();
    };

    const applyVisualToggles = () => {
        document.documentElement.classList.toggle('ng-hide-labels', hideLabels);
        document.documentElement.classList.toggle('ng-hide-orbs', hideOrbs);
        document.documentElement.classList.toggle('ng-mute-gifted', muteGifted);
        document.documentElement.classList.toggle('ng-symmetric-dates', symmetricDates);
        document.documentElement.classList.toggle('ng-hide-desc', hideDesc);
        document.documentElement.classList.toggle('ng-minimal-bread', minimalBread);
    };

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

    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 class="ng-action-link" onclick="window.closeControlPanel()">[Close]</span>
                    </div>
                    <div id="ng-modal-body">
                        <div id="ng-modal-sidebar">
                            <div style="display:flex; flex-direction:column; gap:10px;">
                                <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 Tint</label>
                                <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-symmetric" ${symmetricDates ? 'checked' : ''}> Symmetric Dates</label>
                                <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-desc" ${hideDesc ? 'checked' : ''}> Hide Description</label>
                                <label class="ng-toggle-item"><input type="checkbox" id="ng-toggle-bread" ${minimalBread ? 'checked' : ''}> Minimal Breadcrumbs</label>
                            </div>
                        </div>
                        <div id="ng-modal-content"><div id="ng-modal-grid"></div></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(); };
        }
        const grid = document.getElementById('ng-modal-grid');
        grid.innerHTML = '';
        const renderSection = (title, data, type) => {
            const header = document.createElement('div'); header.style.padding = '10px 0'; header.style.color = '#888'; header.innerText = title; grid.appendChild(header);
            Object.keys(data).sort().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: 140px; font-weight: bold;">${user}</span>`;
                row.appendChild(remarkSpan);
                const del = document.createElement('span'); del.className = 'ng-action-link'; 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') || document.querySelector('.p-main');
            if (target) { hub = document.createElement('div'); hub.id = 'ng-management-hub'; target.parentNode.insertBefore(hub, target); }
        }

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

        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 (isB) {
                msg.classList.add('ng-boosted-post');
                const tag = document.createElement('span'); tag.className = 'ng-status-tag ng-boosted-tag'; tag.innerText = 'Boosted';
                titleArea.appendChild(tag);
            } else if (isM) {
                const postAnchor = msg.querySelector('ul.message-attribution-opposite li:last-child a');
                const isOP = postAnchor && postAnchor.innerText.trim() === '#1';

                if (isOP) {
                    const tsTag = document.createElement('span'); tsTag.className = 'ng-status-tag ng-muted-tag'; tsTag.innerText = 'Threadstarter';
                    titleArea.appendChild(tsTag);
                } else {
                    msg.style.display = 'none';
                    const row = document.createElement('div'); row.className = 'ng-muted-row';
                    const remark = mutedData[user].remark || "No reason";
                    row.innerHTML = `<span>Post by <b>${user}</b> Muted ("${remark}")</span>
                        <div class="ng-placeholder-actions">
                            <span class="ng-action-link" onclick="const p=this.closest('.ng-muted-row').nextElementSibling; p.style.display=(p.style.display==='none'?'block':'none'); this.innerText=(p.style.display==='none'?'[Show]':'[Hide]');">[Show]</span>
                            <span class="ng-action-link" onclick="window.unmute('${user}')">[Unmute]</span>
                        </div>`;
                    msg.parentNode.insertBefore(row, msg);
                }
            }

            const cluster = document.createElement('div'); cluster.className = 'ng-action-cluster';
            cluster.innerHTML = `<span class="ng-btn-item">${isM ? '[UM]' : '[M]'}</span> | <span class="ng-btn-item">${isB ? '[UB]' : '[B]'}</span>`;
            cluster.querySelectorAll('.ng-btn-item')[0].onclick = () => {
                if (isM) delete mutedData[user];
                else { const r = prompt("Reason?", ""); if (r !== null) { mutedData[user] = { remark: r || "No reason" }; delete boostedData[user]; } else return; }
                updateStorage('mute', mutedData); updateStorage('boost', boostedData); location.reload();
            };
            cluster.querySelectorAll('.ng-btn-item')[1].onclick = () => {
                if (isB) delete boostedData[user];
                else { boostedData[user] = { remark: "" }; delete mutedData[user]; }
                updateStorage('boost', boostedData); updateStorage('mute', mutedData); location.reload();
            };
            titleArea.parentNode.insertBefore(cluster, titleArea.nextSibling);
        });

        window.unmute = (u) => { delete mutedData[u]; updateStorage('mute', mutedData); location.reload(); };

        if (hub) {
            const mCount = document.querySelectorAll('.ng-muted-row, .ng-muted-thread').length;
            const bCount = document.querySelectorAll('.ng-boosted-post, .ng-boosted-thread').length;
            hub.innerHTML = `<div>NEOGAF EQ | Muted ${Object.keys(mutedData).length} (${mCount}) | Boosted ${Object.keys(boostedData).length} (${bCount})</div><div><span class="ng-action-link" onclick="window.openControlPanel()">[Control Panel]</span></div>`;
        }
        setTimeout(() => { isLocked = false; }, 100);
    }

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