Update F95_BRATR_Management_Ratings_Helper.js

Minimize by default, remember it as well
This commit is contained in:
Ryahn 2025-09-18 03:31:04 +00:00
parent b3fac15c8f
commit 1796314db6

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name F95 BRATR Management Ratings Helper // @name F95 BRATR Management Ratings Helper
// @namespace Ryahn // @namespace Ryahn
// @version 1.4.2 // @version 1.5.1
// @description Triage panel for /bratr-ratings/management: highlight low-effort reviews, filter, export CSV. // @description Triage panel for /bratr-ratings/management: highlight low-effort reviews, filter, export CSV.
// @match https://f95zone.to/bratr-ratings/management* // @match https://f95zone.to/bratr-ratings/management*
// @match https://f95zone.to/bratr-ratings/*/management* // @match https://f95zone.to/bratr-ratings/*/management*
@ -23,7 +23,7 @@
}; };
const csvEscape = s => `"${(s||'').replace(/"/g,'""')}"`; const csvEscape = s => `"${(s||'').replace(/"/g,'""')}"`;
const avg = arr => arr.length ? arr.reduce((a,b)=>a+b,0)/arr.length : 0; const avg = arr => arr.length ? arr.reduce((a,b)=>a+b,0)/arr.length : 0;
const truncate = (s, n=80) => (s.length > n ? s.slice(0, n-1) + '…' : s); const truncate = (s, n=80) => (s && s.length > n ? s.slice(0, n-1) + '…' : (s||''));
// Scrape reviews on page; stash stable index i for row->DOM mapping // Scrape reviews on page; stash stable index i for row->DOM mapping
const reviews = $$('.message--review').map((msg, i) => { const reviews = $$('.message--review').map((msg, i) => {
@ -66,15 +66,15 @@
return { i, node: msg, author, rating, thread, threadUrl, timeIso, timeTxt, bodyTxt, rawLen, effLen, deleted, links }; return { i, node: msg, author, rating, thread, threadUrl, timeIso, timeTxt, bodyTxt, rawLen, effLen, deleted, links };
}); });
// Cliché patterns with labels // Cliché patterns (includes fixed 200 detector)
const cliché = [ const cliché = [
{ label: 'good game', rx: /\bgood game\b/i }, { key:'good game', label:'good game', rx:/\bgood game\b/i },
{ label: 'very good', rx: /\bit was very good\b/i }, { key:'very good', label:'“it was very good', rx:/\bit was very good\b/i },
{ label: 'amazing', rx: /\bamazing!?(\b|$)/i }, { key:'amazing', label:'amazing', rx:/\bamazing!?(\b|$)/i },
{ label: 'top N', rx: /\btop\s+\d+\b/i }, { key:'top N', label:'“top <number>”', rx:/\btop\s+\d+\b/i },
{ label: 'pretty sex scenes', rx: /\bpretty sex scenes\b/i }, { key:'pretty sex scenes', label:'pretty sex scenes', rx:/\bpretty sex scenes\b/i },
{ label: 'downloads flex', rx: /\bdownload(ed)? hundreds of games\b/i }, { key:'downloads flex', label:'“downloaded hundreds of games”', rx:/\bdownload(ed)? hundreds of games\b/i },
{ label: 'mentions 200 (char rule)', rx: /\b200(?:\s*[- ]?(?:char(?:s|acters?)?|word(?:s)?|limit|minimum|min))?\b/i } { key:'mentions 200', label:'mentions “200” (char/limit/min)', rx:/\b200(?:\s*[- ]?(?:char(?:s|acters?)?|word(?:s)?|limit|minimum|min))?\b/i }
]; ];
// Spam/bypass heuristics // Spam/bypass heuristics
@ -157,22 +157,18 @@
} }
// Diagnose low-effort // Diagnose low-effort
function diagnoseLowEffort(r, cutoff=200) { function diagnoseLowEffort(r, cutoff=220) {
const reasons = []; const reasons = [];
const body = r.bodyTxt || ''; const body = r.bodyTxt || '';
if (!r.deleted && (r.effLen || 0) < cutoff) { if (!r.deleted && (r.effLen || 0) < cutoff) {
reasons.push({ reasons.push({ code:'LEN', text:`LEN ${r.effLen}<${cutoff}`, tip:`Effective length ${r.effLen} is below cutoff ${cutoff}` });
code: 'LEN',
text: `LEN ${r.effLen}<${cutoff}`,
tip: `Effective length ${r.effLen} is below cutoff ${cutoff}`
});
} }
if (!r.deleted) { if (!r.deleted) {
cliché.forEach(c => { cliché.forEach(c => {
const m = body.match(c.rx); const m = body.match(c.rx);
if (m) reasons.push({ code: 'CLICHÉ', text: `CLICHÉ: "${truncate(m[0], 28)}"`, tip: `Matched phrase: ${m[0]}` }); if (m) reasons.push({ code:'CLICHÉ', text:`CLICHÉ: "${truncate(m[0], 28)}"`, tip:`Matched: ${m[0]}`, key:c.key });
}); });
} }
@ -196,11 +192,13 @@
<div class="bh-body"> <div class="bh-body">
<div class="bh-controls"> <div class="bh-controls">
<label>Min <input type="number" step="0.5" min="0" max="5" class="bh-minrating" value="0"></label> <label>Min <input type="number" step="0.5" min="0" max="5" class="bh-minrating" value="0"></label>
<label>Low-effort length cutoff <input type="number" min="0" class="bh-maxlen" value="200"></label> <label>Low-effort length cutoff <input type="number" min="0" class="bh-maxlen" value="220"></label>
<label>User <input type="text" class="bh-user" placeholder="author contains"></label> <label>User <input type="text" class="bh-user" placeholder="author contains"></label>
<label>Date from <input type="datetime-local" class="bh-from"></label> <label>Date from <input type="datetime-local" class="bh-from"></label>
<label>Date to <input type="datetime-local" class="bh-to"></label> <label>Date to <input type="datetime-local" class="bh-to"></label>
<label><input type="checkbox" class="bh-lowonly" checked> Low-effort only</label> <label><input type="checkbox" class="bh-lowonly" checked> Low-effort only</label>
<label>Cliché <select class="bh-cliche"><option value="__ALL__">All clichés</option></select></label>
<label><input type="checkbox" class="bh-cliche-only"> Only reviews matching selected cliché</label>
<button class="bh-copy">Copy CSV</button> <button class="bh-copy">Copy CSV</button>
<button class="bh-reset">Reset</button> <button class="bh-reset">Reset</button>
</div> </div>
@ -209,10 +207,10 @@
`; `;
document.body.appendChild(panel); document.body.appendChild(panel);
// Styles (+ clickable rows + pulse highlight) // Styles
const css = document.createElement('style'); const css = document.createElement('style');
css.textContent = ` css.textContent = `
#bratr-helper{position:fixed;right:12px;bottom:12px;width:780px;max-height:70vh;z-index:99999; #bratr-helper{position:fixed;right:12px;bottom:12px;width:820px;max-height:70vh;z-index:99999;
background:#111;border:1px solid #333;border-radius:12px;color:#ddd;font:12px/1.4 system-ui;box-shadow:0 8px 24px rgba(0,0,0,.4)} background:#111;border:1px solid #333;border-radius:12px;color:#ddd;font:12px/1.4 system-ui;box-shadow:0 8px 24px rgba(0,0,0,.4)}
#bratr-helper .bh-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid #2a2a2a} #bratr-helper .bh-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid #2a2a2a}
#bratr-helper .bh-head strong{font-size:13px} #bratr-helper .bh-head strong{font-size:13px}
@ -221,7 +219,7 @@
#bratr-helper .bh-body{padding:8px} #bratr-helper .bh-body{padding:8px}
#bratr-helper .bh-controls{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px} #bratr-helper .bh-controls{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px}
#bratr-helper .bh-controls label{display:flex;gap:6px;align-items:center;background:#161616;border:1px solid #2a2a2a;border-radius:8px;padding:4px 8px} #bratr-helper .bh-controls label{display:flex;gap:6px;align-items:center;background:#161616;border:1px solid #2a2a2a;border-radius:8px;padding:4px 8px}
#bratr-helper input{background:#0f0f0f;border:1px solid #333;color:#ddd;border-radius:6px;padding:3px 6px} #bratr-helper input, #bratr-helper select{background:#0f0f0f;border:1px solid #333;color:#ddd;border-radius:6px;padding:3px 6px}
#bratr-helper button.bh-copy,#bratr-helper button.bh-reset{background:#1e1e1e;border:1px solid #444;color:#ddd;border-radius:8px;padding:4px 10px;cursor:pointer} #bratr-helper button.bh-copy,#bratr-helper button.bh-reset{background:#1e1e1e;border:1px solid #444;color:#ddd;border-radius:8px;padding:4px 10px;cursor:pointer}
#bratr-helper .bh-table{overflow:auto;max-height:48vh;border:1px solid #2a2a2a;border-radius:8px} #bratr-helper .bh-table{overflow:auto;max-height:48vh;border:1px solid #2a2a2a;border-radius:8px}
#bratr-helper table{width:100%;border-collapse:collapse} #bratr-helper table{width:100%;border-collapse:collapse}
@ -236,10 +234,7 @@
#bratr-helper .pill.delet{border-color:#8aa;color:#8aa} #bratr-helper .pill.delet{border-color:#8aa;color:#8aa}
#bratr-helper .pill.spam{border-color:#ff9d00;color:#ff9d00} #bratr-helper .pill.spam{border-color:#ff9d00;color:#ff9d00}
#bratr-helper .links a{margin-right:8px} #bratr-helper .links a{margin-right:8px}
@keyframes bh-pulse { @keyframes bh-pulse {0%{box-shadow:0 0 0 0 rgba(245,180,0,.6);}100%{box-shadow:0 0 0 12px rgba(245,180,0,0);}}
0% { box-shadow: 0 0 0 0 rgba(245,180,0,.6); }
100% { box-shadow: 0 0 0 12px rgba(245,180,0,0); }
}
.bh-target-pulse { animation: bh-pulse 1s ease-out 0s 2; outline: 2px solid #f5b400 !important; } .bh-target-pulse { animation: bh-pulse 1s ease-out 0s 2; outline: 2px solid #f5b400 !important; }
@media (max-width: 880px){#bratr-helper{left:8px;right:8px;width:auto}} @media (max-width: 880px){#bratr-helper{left:8px;right:8px;width:auto}}
`; `;
@ -252,21 +247,54 @@
lowonly: panel.querySelector('.bh-lowonly'), lowonly: panel.querySelector('.bh-lowonly'),
from: panel.querySelector('.bh-from'), from: panel.querySelector('.bh-from'),
to: panel.querySelector('.bh-to'), to: panel.querySelector('.bh-to'),
clicheSel: panel.querySelector('.bh-cliche'),
clicheOnly: panel.querySelector('.bh-cliche-only'),
table: panel.querySelector('.bh-table'), table: panel.querySelector('.bh-table'),
metrics: panel.querySelector('.bh-metrics') metrics: panel.querySelector('.bh-metrics')
}; };
panel.querySelector('.bh-collapse').addEventListener('click', () => { // Populate cliché dropdown
const body = panel.querySelector('.bh-body'); const frag = document.createDocumentFragment();
if (body.style.display === 'none') { body.style.display = ''; panel.querySelector('.bh-collapse').textContent = ''; } cliché.forEach(c => {
else { body.style.display = 'none'; panel.querySelector('.bh-collapse').textContent = '+'; } const opt = document.createElement('option');
opt.value = c.key;
opt.textContent = c.label;
frag.appendChild(opt);
}); });
ui.clicheSel.appendChild(frag);
const COLLAPSE_KEY = 'bh-collapsed';
const bodyEl = panel.querySelector('.bh-body');
const collapseBtn = panel.querySelector('.bh-collapse');
function setCollapsed(collapsed) {
bodyEl.style.display = collapsed ? 'none' : '';
collapseBtn.textContent = collapsed ? '+' : '';
try { localStorage.setItem(COLLAPSE_KEY, collapsed ? '1' : '0'); } catch {}
}
collapseBtn.addEventListener('click', () => {
setCollapsed(bodyEl.style.display !== 'none');
});
// initialize from storage; default to collapsed if unset
const stored = (typeof localStorage !== 'undefined') ? localStorage.getItem(COLLAPSE_KEY) : null;
setCollapsed(stored === null ? true : stored === '1');
panel.querySelector('.bh-reset').addEventListener('click', () => { panel.querySelector('.bh-reset').addEventListener('click', () => {
ui.min.value = 0; ui.maxlen.value = 200; ui.user.value = ''; ui.lowonly.checked = true; ui.from.value = ''; ui.to.value = ''; ui.min.value = 0;
ui.maxlen.value = 220;
ui.user.value = '';
ui.lowonly.checked = true;
ui.from.value = '';
ui.to.value = '';
ui.clicheSel.value = '__ALL__';
ui.clicheOnly.checked = false;
render(); render();
}); });
['input','change'].forEach(ev => { ['input','change'].forEach(ev => {
[ui.min, ui.maxlen, ui.user, ui.lowonly, ui.from, ui.to].forEach(el => el.addEventListener(ev, render)); [ui.min, ui.maxlen, ui.user, ui.lowonly, ui.from, ui.to, ui.clicheSel, ui.clicheOnly]
.forEach(el => el.addEventListener(ev, render));
}); });
function withinDates(iso, from, to) { function withinDates(iso, from, to) {
@ -277,6 +305,15 @@
return true; return true;
} }
function reviewMatchesSelectedCliche(r) {
if (!ui.clicheOnly.checked) return true;
const selected = ui.clicheSel.value;
if (!selected || selected === '__ALL__') return true;
const def = cliché.find(c => c.key === selected);
if (!def) return true;
return def.rx.test(r.bodyTxt || '');
}
function currentRows() { function currentRows() {
const min = parseFloat(ui.min.value || '0'); const min = parseFloat(ui.min.value || '0');
const maxlen = parseInt(ui.maxlen.value || '0', 10); const maxlen = parseInt(ui.maxlen.value || '0', 10);
@ -289,6 +326,8 @@
if (Number.isFinite(min) && r.rating != null && r.rating < min) return false; if (Number.isFinite(min) && r.rating != null && r.rating < min) return false;
if (user && !r.author.toLowerCase().includes(user)) return false; if (user && !r.author.toLowerCase().includes(user)) return false;
if (from || to) { if (!withinDates(r.timeIso, from, to)) return false; } if (from || to) { if (!withinDates(r.timeIso, from, to)) return false; }
if (!reviewMatchesSelectedCliche(r)) return false;
const diag = diagnoseLowEffort(r, maxlen); const diag = diagnoseLowEffort(r, maxlen);
if (lowOnly && !diag.low) return false; if (lowOnly && !diag.low) return false;
return true; return true;
@ -296,11 +335,14 @@
} }
function render() { function render() {
const maxlen = parseInt(ui.maxlen.value || '200', 10); const maxlen = parseInt(ui.maxlen.value || '220', 10);
const live = reviews.filter(r => !r.deleted); const live = reviews.filter(r => !r.deleted);
const lowCount = live.filter(r => diagnoseLowEffort(r, maxlen).low).length; const lowCount = live.filter(r => diagnoseLowEffort(r, maxlen).low).length;
const avgRating = avg(live.map(r => r.rating || 0)).toFixed(2); const avgRating = avg(live.map(r => r.rating || 0)).toFixed(2);
ui.metrics.textContent = `on page: ${reviews.length} • avg ★ ${avgRating} • low-effort flagged: ${lowCount}`; const clicheInfo = ui.clicheOnly.checked
? ` • cliché: ${ui.clicheSel.options[ui.clicheSel.selectedIndex]?.text || 'All'}`
: '';
ui.metrics.textContent = `on page: ${reviews.length} • avg ★ ${avgRating} • low-effort flagged: ${lowCount}${clicheInfo}`;
const rows = currentRows(); const rows = currentRows();
const html = [`<table><thead><tr> const html = [`<table><thead><tr>
@ -311,6 +353,7 @@
const low = diag.low; const low = diag.low;
const one = Math.round(r.rating||0) === 1; const one = Math.round(r.rating||0) === 1;
const cls = `${low ? 'low' : ''} ${r.deleted ? 'deleted': ''}`.trim(); const cls = `${low ? 'low' : ''} ${r.deleted ? 'deleted': ''}`.trim();
const flagHtml = diag.reasons.map(reason => { const flagHtml = diag.reasons.map(reason => {
let pill = 'pill'; let pill = 'pill';
if (reason.code === 'LEN' || reason.code === 'CLICHÉ') pill += ' low'; if (reason.code === 'LEN' || reason.code === 'CLICHÉ') pill += ' low';
@ -319,6 +362,7 @@
const tip = reason.tip ? ` title="${reason.tip.replace(/"/g, '&quot;')}"` : ''; const tip = reason.tip ? ` title="${reason.tip.replace(/"/g, '&quot;')}"` : '';
return `<span class="${pill}"${tip}>${reason.text}</span>`; return `<span class="${pill}"${tip}>${reason.text}</span>`;
}).join(''); }).join('');
html.push(`<tr class="${cls}" data-i="${r.i}"> html.push(`<tr class="${cls}" data-i="${r.i}">
<td>${r.author || ''}</td> <td>${r.author || ''}</td>
<td>${r.rating != null ? r.rating.toFixed(2) : ''}</td> <td>${r.rating != null ? r.rating.toFixed(2) : ''}</td>
@ -338,11 +382,12 @@
html.push(`</tbody></table>`); html.push(`</tbody></table>`);
ui.table.innerHTML = html.join(''); ui.table.innerHTML = html.join('');
// Bind row click delegation exactly once and keep it // Keep single delegated click handler alive
if (!ui.table._bhBound) { if (!ui.table._bhBound) {
ui.table.addEventListener('click', onRowClick); ui.table.addEventListener('click', onRowClick);
ui.table._bhBound = true; ui.table._bhBound = true;
} }
retint(); retint();
} }
@ -354,17 +399,16 @@
const r = reviews[idx]; const r = reviews[idx];
if (!r || !r.node) return; if (!r || !r.node) return;
// Smooth scroll and pulse
r.node.scrollIntoView({ behavior: 'smooth', block: 'center' }); r.node.scrollIntoView({ behavior: 'smooth', block: 'center' });
r.node.classList.remove('bh-target-pulse'); r.node.classList.remove('bh-target-pulse');
void r.node.offsetWidth; // reflow to restart animation void r.node.offsetWidth;
r.node.classList.add('bh-target-pulse'); r.node.classList.add('bh-target-pulse');
setTimeout(() => r.node.classList.remove('bh-target-pulse'), 1800); setTimeout(() => r.node.classList.remove('bh-target-pulse'), 1800);
} }
// Tint live page with reasons as tooltip // Tint live page with reasons as tooltip
function retint() { function retint() {
const maxlen = parseInt(ui.maxlen?.value || '200', 10); const maxlen = parseInt(ui.maxlen?.value || '220', 10);
reviews.forEach(r => { reviews.forEach(r => {
if (r.deleted) { if (r.deleted) {
r.node.style.outline = ''; r.node.style.outline = '';
@ -389,7 +433,7 @@
// Copy CSV of current view, with reasons column // Copy CSV of current view, with reasons column
panel.querySelector('.bh-copy').addEventListener('click', () => { panel.querySelector('.bh-copy').addEventListener('click', () => {
const rows = currentRows(); const rows = currentRows();
const maxlen = parseInt(ui.maxlen?.value || '200', 10); const maxlen = parseInt(ui.maxlen?.value || '220', 10);
const header = ['author','rating','thread','threadUrl','timeIso','effLen','rawLen','deleted','low','reasons','body']; const header = ['author','rating','thread','threadUrl','timeIso','effLen','rawLen','deleted','low','reasons','body'];
const lines = [header.join(',')]; const lines = [header.join(',')];
rows.forEach(r => { rows.forEach(r => {