The Forum Equalizer: Fixed Boosted Counter, Green/Red UI Gold Master.
当前为
// ==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();
})();