From bc4e35e28ec37bf761c404826e5e32a759f2b768 Mon Sep 17 00:00:00 2001 From: Ryahn Date: Mon, 22 Sep 2025 01:48:03 +0000 Subject: [PATCH] Update F95_BRATR_Management_Ratings_Helper.js --- F95_BRATR_Management_Ratings_Helper.js | 237 ++++++++++++++++--------- 1 file changed, 155 insertions(+), 82 deletions(-) diff --git a/F95_BRATR_Management_Ratings_Helper.js b/F95_BRATR_Management_Ratings_Helper.js index 9a3e452..91b7922 100644 --- a/F95_BRATR_Management_Ratings_Helper.js +++ b/F95_BRATR_Management_Ratings_Helper.js @@ -29,7 +29,7 @@ // Cached DOM utilities const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); - + // Optimized text extraction with caching const textCache = new WeakMap(); const text = el => { @@ -54,17 +54,17 @@ const csvEscape = s => `"${(s || '').replace(/"/g, '""')}"`; const avg = arr => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0; const truncate = (s, n = 80) => (s && s.length > n ? s.slice(0, n - 1) + '…' : (s || '')); - + // Optimized Unicode-aware letter counters with caching const letterCountCache = new Map(); function countLetters(str) { if (!str) return { all: 0, ascii: 0, nonAscii: 0 }; if (letterCountCache.has(str)) return letterCountCache.get(str); - + const all = (str.match(/\p{L}/gu) || []).length; const ascii = (str.match(/[A-Za-z]/g) || []).length; const result = { all, ascii, nonAscii: Math.max(0, all - ascii) }; - + // Cache only reasonable length strings to avoid memory bloat if (str.length < 1000) letterCountCache.set(str, result); return result; @@ -84,7 +84,7 @@ const enders = (str.match(/[.!?…]/g) || []).length; return enders < 1; } - + // Optimized review scraping with better error handling const reviews = $$('.message--review').map((msg, i) => { try { @@ -117,7 +117,7 @@ } const deleted = msg.classList.contains('message--deleted'); - + // Batch action link queries const actionLinks = main.querySelectorAll('a[class*="actionBar-action"]'); const links = { @@ -148,7 +148,7 @@ return null; } }).filter(Boolean); // Remove null entries - + // Cliché patterns const cliché = [ { key: 'good game', label: '“good game”', rx: /\bgood game\b/i }, @@ -159,16 +159,16 @@ { key: 'downloads flex', label: '“downloaded hundreds of games”', rx: /\bdownload(ed)? hundreds of games\b/i }, { key: 'mentions 200', label: 'mentions “200” (char/limit/min)', rx: /\b200(?:\s*[- ]?(?:char(?:s|acters?)?|word(?:s)?|limit|minimum|min))?\b/i } ]; - + // Optimized spam analysis with caching and early returns const spamAnalysisCache = new Map(); function analyzeSpam(body) { const s = body || ''; if (!s) return []; - + // Cache results for identical text if (spamAnalysisCache.has(s)) return spamAnalysisCache.get(s); - + const reasons = []; const len = s.length; @@ -182,10 +182,10 @@ const punctRuns = s.match(/([.!?…,_\-])\1{5,}/g); if (punctRuns?.length) { const totalRunChars = punctRuns.reduce((a, b) => a + b.length, 0); - reasons.push({ - code: 'SPAM', - text: `DOTCARPET ×${punctRuns.length}`, - tip: `Punctuation runs total ${totalRunChars} chars` + reasons.push({ + code: 'SPAM', + text: `DOTCARPET ×${punctRuns.length}`, + tip: `Punctuation runs total ${totalRunChars} chars` }); } @@ -193,10 +193,10 @@ const punctChars = (s.match(/[^\w\s]/g) || []).length; const pRatio = punctChars / len; if (pRatio > CONFIG.PUNCTUATION_RATIO_THRESHOLD && len >= 120) { - reasons.push({ - code: 'SPAM', - text: `PUNC ${Math.round(pRatio * 100)}%`, - tip: `Non-alphanumeric ratio ${Math.round(pRatio * 100)}%` + reasons.push({ + code: 'SPAM', + text: `PUNC ${Math.round(pRatio * 100)}%`, + tip: `Non-alphanumeric ratio ${Math.round(pRatio * 100)}%` }); } @@ -209,20 +209,20 @@ const totalTok = tokens.length; const freq = new Map(); - + // Optimized frequency counting for (const token of tokens) { freq.set(token, (freq.get(token) || 0) + 1); } - + const uniq = freq.size; const uniqRatio = uniq / totalTok; if (uniqRatio < CONFIG.UNIQUE_RATIO_THRESHOLD) { - reasons.push({ - code: 'SPAM', - text: `LOW_UNIQUE ${uniq}/${totalTok}`, - tip: `Unique/token ratio ${uniqRatio.toFixed(2)}` + reasons.push({ + code: 'SPAM', + text: `LOW_UNIQUE ${uniq}/${totalTok}`, + tip: `Unique/token ratio ${uniqRatio.toFixed(2)}` }); } @@ -234,12 +234,12 @@ maxWord = word; } } - + if (maxCount / totalTok > CONFIG.MAX_WORD_FREQUENCY_THRESHOLD && totalTok >= 8) { - reasons.push({ - code: 'SPAM', - text: `REP "${truncate(maxWord, 12)}" ×${maxCount}`, - tip: `Word appears ${(maxCount / totalTok * 100).toFixed(0)}% of tokens` + reasons.push({ + code: 'SPAM', + text: `REP "${truncate(maxWord, 12)}" ×${maxCount}`, + tip: `Word appears ${(maxCount / totalTok * 100).toFixed(0)}% of tokens` }); } @@ -247,18 +247,18 @@ function analyzeNgrams(n) { const ngramMap = new Map(); const limit = tokens.length - n; - + for (let i = 0; i <= limit; i++) { const ngram = tokens.slice(i, i + n).join(' '); ngramMap.set(ngram, (ngramMap.get(ngram) || 0) + 1); } - + for (const [ngram, count] of ngramMap) { if (count >= CONFIG.NGRAM_REPEAT_THRESHOLD) { - reasons.push({ - code: 'SPAM', - text: `REP${n} "${truncate(ngram, 20)}" ×${count}`, - tip: `Repeated ${n}-gram ${count} times` + reasons.push({ + code: 'SPAM', + text: `REP${n} "${truncate(ngram, 20)}" ×${count}`, + tip: `Repeated ${n}-gram ${count} times` }); return true; // Found one, stop looking } @@ -274,19 +274,19 @@ // Optimized short sentence analysis const sentences = s.split(/(?<=[.!?])\s+/); const shortFreq = new Map(); - + for (const sentence of sentences) { const toks = sentence.toLowerCase().match(/[a-z0-9']+/g); if (toks && toks.length <= CONFIG.SHORT_SENTENCE_MAX_LENGTH) { const key = toks.join(' '); const count = (shortFreq.get(key) || 0) + 1; shortFreq.set(key, count); - + if (count >= CONFIG.NGRAM_REPEAT_THRESHOLD) { - reasons.push({ - code: 'SPAM', - text: `SHORT_SENT "${truncate(key, 18)}" ×${count}`, - tip: `Short sentence repeated ${count} times` + reasons.push({ + code: 'SPAM', + text: `SHORT_SENT "${truncate(key, 18)}" ×${count}`, + tip: `Short sentence repeated ${count} times` }); break; // Found one, stop looking } @@ -297,10 +297,10 @@ if (spamAnalysisCache.size < 1000) { spamAnalysisCache.set(s, reasons); } - + return reasons; } - + // Optimized low-effort diagnosis with caching const diagnosisCache = new Map(); function diagnoseLowEffort(r, cutoff = CONFIG.DEFAULT_CUTOFF) { @@ -314,10 +314,10 @@ // Length check if (!r.deleted && (r.effLen || 0) < cutoff) { - reasons.push({ - code: 'LEN', - text: `LEN ${r.effLen}<${cutoff}`, - tip: `Effective length ${r.effLen} is below cutoff ${cutoff}` + reasons.push({ + code: 'LEN', + text: `LEN ${r.effLen}<${cutoff}`, + tip: `Effective length ${r.effLen} is below cutoff ${cutoff}` }); } @@ -326,11 +326,11 @@ for (const c of cliché) { const match = body.match(c.rx); if (match) { - reasons.push({ - code: 'CLICHÉ', - text: `CLICHÉ: "${truncate(match[0], 28)}"`, - tip: `Matched: ${match[0]}`, - key: c.key + reasons.push({ + code: 'CLICHÉ', + text: `CLICHÉ: "${truncate(match[0], 28)}"`, + tip: `Matched: ${match[0]}`, + key: c.key }); break; // Found one cliché, no need to check others } @@ -342,7 +342,7 @@ const tip = `letters: ascii=${ascii}, nonAscii=${nonAscii}, share=${(nonAscii / Math.max(1, all)).toFixed(2)}`; reasons.push({ code: 'LANG', text: 'NON-EN', tip }); } - + // Style check if (isRunOnWall(body)) { reasons.push({ code: 'STYLE', text: 'RUN-ON', tip: 'Long block with no sentence enders' }); @@ -363,15 +363,15 @@ ); const result = { low, reasons }; - + // Cache result (limit cache size) if (diagnosisCache.size < 500) { diagnosisCache.set(cacheKey, result); } - + return result; } - + // Panel const panel = document.createElement('div'); panel.id = 'bratr-helper'; @@ -393,12 +393,14 @@ + +
`; document.body.appendChild(panel); - + // Styles const css = document.createElement('style'); css.textContent = ` @@ -412,7 +414,9 @@ #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 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,#bratr-helper button.bh-prev-page,#bratr-helper button.bh-next-page{background:#1e1e1e;border:1px solid #444;color:#ddd;border-radius:8px;padding:4px 10px;cursor:pointer} + #bratr-helper button.bh-prev-page,#bratr-helper button.bh-next-page{background:#2a4a2a;border-color:#4a6a4a;font-size:11px;padding:3px 8px} + #bratr-helper button.bh-prev-page:hover,#bratr-helper button.bh-next-page:hover{background:#3a5a3a} #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 th,#bratr-helper td{padding:6px 8px;border-bottom:1px solid #232323;vertical-align:top} @@ -433,7 +437,7 @@ @media (max-width: 880px){#bratr-helper{left:8px;right:8px;width:auto}} `; document.head.appendChild(css); - + const ui = { min: panel.querySelector('.bh-minrating'), maxlen: panel.querySelector('.bh-maxlen'), @@ -446,7 +450,7 @@ table: panel.querySelector('.bh-table'), metrics: panel.querySelector('.bh-metrics') }; - + // Populate cliché dropdown const frag = document.createDocumentFragment(); cliché.forEach(c => { @@ -456,7 +460,7 @@ frag.appendChild(opt); }); ui.clicheSel.appendChild(frag); - + // collapse default minimized with persistence const COLLAPSE_KEY = 'bh-collapsed'; const bodyEl = panel.querySelector('.bh-body'); @@ -469,7 +473,7 @@ collapseBtn.addEventListener('click', () => setCollapsed(bodyEl.style.display !== 'none')); const stored = (typeof localStorage !== 'undefined') ? localStorage.getItem(COLLAPSE_KEY) : null; setCollapsed(stored === null ? true : stored === '1'); - + // Optimized event handling with debouncing let renderTimeout; const debouncedRender = () => { @@ -494,6 +498,75 @@ } }); + // Navigation helper function + function navigateToPage(pageNumber) { + try { + const currentUrl = window.location.href; + const urlMatch = currentUrl.match(/\/bratr-ratings\/(?:page-(\d+)\/)?management/); + + if (urlMatch) { + let targetUrl; + if (pageNumber === 1) { + // For page 1, remove the page number from URL + targetUrl = currentUrl.replace( + /\/bratr-ratings\/page-\d+\/management/, + '/bratr-ratings/management' + ); + } else { + // For other pages, add or update the page number + targetUrl = currentUrl.replace( + /\/bratr-ratings\/(?:page-\d+\/)?management/, + `/bratr-ratings/page-${pageNumber}/management` + ); + } + + console.log(`Navigating to page ${pageNumber}: ${targetUrl}`); + window.location.href = targetUrl; + } else { + console.warn('Could not determine current page number from URL:', currentUrl); + } + } catch (error) { + console.error('Error navigating to page:', error); + } + } + + // Previous page handler + panel.querySelector('.bh-prev-page').addEventListener('click', () => { + try { + const currentUrl = window.location.href; + const urlMatch = currentUrl.match(/\/bratr-ratings\/(?:page-(\d+)\/)?management/); + + if (urlMatch) { + const currentPage = urlMatch[1] ? parseInt(urlMatch[1], 10) : 1; + const prevPage = currentPage - 1; + + if (prevPage >= 1) { + navigateToPage(prevPage); + } else { + console.log('Already on first page'); + } + } + } catch (error) { + console.error('Error navigating to previous page:', error); + } + }); + + // Next page handler + panel.querySelector('.bh-next-page').addEventListener('click', () => { + try { + const currentUrl = window.location.href; + const urlMatch = currentUrl.match(/\/bratr-ratings\/(?:page-(\d+)\/)?management/); + + if (urlMatch) { + const currentPage = urlMatch[1] ? parseInt(urlMatch[1], 10) : 1; + const nextPage = currentPage + 1; + navigateToPage(nextPage); + } + } catch (error) { + console.error('Error navigating to next page:', error); + } + }); + // Optimized event delegation const controlElements = [ui.min, ui.maxlen, ui.user, ui.lowonly, ui.from, ui.to, ui.clicheSel, ui.clicheOnly]; ['input', 'change'].forEach(eventType => { @@ -503,7 +576,7 @@ } }); }); - + function withinDates(iso, from, to) { if (!iso) return true; const t = new Date(iso).getTime(); @@ -511,7 +584,7 @@ if (to && t > new Date(to).getTime()) return false; return true; } - + function reviewMatchesSelectedCliche(r) { if (!ui.clicheOnly.checked) return true; const selected = ui.clicheSel.value; @@ -520,7 +593,7 @@ if (!def) return true; return def.rx.test(r.bodyTxt || ''); } - + function currentRows() { const min = parseFloat(ui.min.value || '0'); const maxlen = parseInt(ui.maxlen.value || '0', 10); @@ -528,25 +601,25 @@ const lowOnly = ui.lowonly.checked; const from = ui.from.value; const to = ui.to.value; - + return reviews.filter(r => { if (Number.isFinite(min) && r.rating != null && r.rating < min) return false; if (user && !r.author.toLowerCase().includes(user)) return false; if (from || to) { if (!withinDates(r.timeIso, from, to)) return false; } if (!reviewMatchesSelectedCliche(r)) return false; - + const diag = diagnoseLowEffort(r, maxlen); if (lowOnly && !diag.low) return false; return true; }); } - + // Optimized render function with better error handling function render() { try { const maxlen = parseInt(ui.maxlen?.value || CONFIG.DEFAULT_CUTOFF.toString(), 10); const live = reviews.filter(r => !r.deleted); - + // Optimized metrics calculation const lowCount = live.filter(r => { try { @@ -556,12 +629,12 @@ return false; } }).length; - + const avgRating = avg(live.map(r => r.rating || 0)).toFixed(2); const clicheInfo = ui.clicheOnly?.checked ? ` • cliché: ${ui.clicheSel?.options[ui.clicheSel?.selectedIndex]?.text || 'All'}` : ''; - + if (ui.metrics) { ui.metrics.textContent = `on page: ${reviews.length} • avg ★ ${avgRating} • low-effort flagged: ${lowCount}${clicheInfo}`; } @@ -583,7 +656,7 @@ 'LEN': 'low', 'CLICHÉ': 'low', 'SPAM': 'spam', 'DELETED': 'delet', 'LANG': 'lang', 'STYLE': 'style' }; - + const flagHtml = diag.reasons.map(reason => { let pill = 'pill'; const extra = pillClassByCode[reason.code]; @@ -612,7 +685,7 @@ } html.push(``); - + if (ui.table) { ui.table.innerHTML = html.join(''); @@ -627,7 +700,7 @@ console.error('Error in render function:', error); } } - + function onRowClick(e) { if (e.target.closest('a, button')) return; const tr = e.target.closest('tr[data-i]'); @@ -635,14 +708,14 @@ const idx = parseInt(tr.getAttribute('data-i'), 10); const r = reviews[idx]; if (!r || !r.node) return; - + r.node.scrollIntoView({ behavior: 'smooth', block: 'center' }); r.node.classList.remove('bh-target-pulse'); void r.node.offsetWidth; r.node.classList.add('bh-target-pulse'); setTimeout(() => r.node.classList.remove('bh-target-pulse'), 1800); } - + // Tint live page with reasons as tooltip function retint() { const maxlen = parseInt(ui.maxlen?.value || '220', 10); @@ -664,9 +737,9 @@ } }); } - + render(); - + // Optimized CSV export with better error handling panel.querySelector('.bh-copy').addEventListener('click', async () => { try { @@ -674,7 +747,7 @@ const maxlen = parseInt(ui.maxlen?.value || CONFIG.DEFAULT_CUTOFF.toString(), 10); const header = ['author', 'rating', 'thread', 'threadUrl', 'timeIso', 'effLen', 'rawLen', 'deleted', 'low', 'reasons', 'body']; const lines = [header.join(',')]; - + // Optimized CSV generation for (const r of rows) { try { @@ -697,9 +770,9 @@ console.warn('Error processing row for CSV:', error); } } - + const csv = lines.join('\n'); - + // Try multiple clipboard methods try { if (typeof GM_setClipboard !== 'undefined') { @@ -725,4 +798,4 @@ console.error('Error exporting CSV:', error); } }); - })(); \ No newline at end of file + })(); \ No newline at end of file