Update F95_BRATR_Management_Ratings_Helper.js
This commit is contained in:
parent
8821dad70d
commit
bc4e35e28e
@ -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 @@
|
||||
<label><input type="checkbox" class="bh-cliche-only"> Only reviews matching selected cliché</label>
|
||||
<button class="bh-copy">Copy CSV</button>
|
||||
<button class="bh-reset">Reset</button>
|
||||
<button class="bh-prev-page">← Prev</button>
|
||||
<button class="bh-next-page">Next →</button>
|
||||
</div>
|
||||
<div class="bh-table"></div>
|
||||
</div>
|
||||
`;
|
||||
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(`</tbody></table>`);
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
})();
|
||||
})();
|
||||
Loading…
x
Reference in New Issue
Block a user