From 79a44079ccbe2591dce65d2a5ab42634376e27bb Mon Sep 17 00:00:00 2001 From: Ryahn Date: Thu, 18 Sep 2025 05:52:03 +0000 Subject: [PATCH] Update report-organizer.js Optimized and faster --- report-organizer.js | 257 +++++++++++++++++++++++++++++++------------- 1 file changed, 184 insertions(+), 73 deletions(-) diff --git a/report-organizer.js b/report-organizer.js index ef6842a..8eb3c82 100644 --- a/report-organizer.js +++ b/report-organizer.js @@ -1,93 +1,204 @@ // ==UserScript== -// @name Sort Reports +// @name Sort Reports (no-freeze, resilient UI) — fixed WeakMap // @namespace http://tampermonkey.net/ -// @version 2024-09-18 -// @description Sort Reports -// @author You +// @version 4.1 +// @description Fast, chunked sorting with robust UI injection // @match https://f95zone.to/reports/queue/* // @icon https://www.google.com/s2/favicons?sz=64&domain=f95zone.to // @grant none -// @updateURL https://git.zonies.xyz/Ryahn/F95Zone-Scripts/raw/branch/main/report-organizer.js -// @downloadURL https://git.zonies.xyz/Ryahn/F95Zone-Scripts/raw/branch/main/report-organizer.js +// @run-at document-idle // ==/UserScript== -(function() { - 'use strict'; +(function () { + 'use strict'; - window.onload = function() { - const reportTable = document.querySelector('.structItemContainer'); + // Wait for a selector with MutationObserver fallback + function waitForElement(selector, root = document, timeoutMs = 15000) { + return new Promise((resolve, reject) => { + const first = root.querySelector(selector); + if (first) return resolve(first); - if (!reportTable) return; + const obs = new MutationObserver(() => { + const hit = root.querySelector(selector); + if (hit) { obs.disconnect(); resolve(hit); } + }); + obs.observe(root === document ? document.documentElement : root, { childList: true, subtree: true }); - const sortContainer = document.createElement('div'); - sortContainer.style.textAlign = 'right'; - sortContainer.innerHTML = ` - - - `; + const t = setTimeout(() => { obs.disconnect(); reject(new Error(`Timeout waiting for ${selector}`)); }, timeoutMs); + // ensure timer cleared on resolve + const originalResolve = resolve; + resolve = (v) => { clearTimeout(t); originalResolve(v); }; + }); + } - reportTable.parentNode.insertBefore(sortContainer, reportTable); + const rIC = window.requestIdleCallback || function (cb) { return setTimeout(cb, 17); }; - document.getElementById('sortType').addEventListener('change', function() { - const sortValue = this.value; - const rows = Array.from(reportTable.querySelectorAll('.structItem')); + function debounce(fn, ms) { + let t; + return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; + } - switch (sortValue) { - case 'dateAsc': - sortRows(rows, '.structItem-latestDate', true, 'date'); - break; - case 'dateDesc': - sortRows(rows, '.structItem-latestDate', false, 'date'); - break; - case 'sectionAsc': - sortRows(rows, '.structItem-forum a', true); - break; - case 'sectionDesc': - sortRows(rows, '.structItem-forum a', false); - break; - case 'prefixAsc': - sortRows(rows, '.structItem-title', true, 'prefix'); - break; - case 'prefixDesc': - sortRows(rows, '.structItem-title', false, 'prefix'); - break; - } - }); + const collator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true }); - function sortRows(rows, selector, asc = true, type = 'text') { - const sortedRows = rows.sort((a, b) => { - let valA = a.querySelector(selector); - let valB = b.querySelector(selector); + // Caches + let rowIndex = []; // [{rowEl, i, dateMs, section, prefix}] + let rowMap = new WeakMap(); // rowEl -> cached keys - if (!valA || !valB) { - return asc ? (valA ? -1 : 1) : (valB ? -1 : 1); - } + function computeKeys(rowEl, i) { + const dateEl = rowEl.querySelector('.structItem-latestDate'); + const forumEl = rowEl.querySelector('.structItem-forum a'); + const titleEl = rowEl.querySelector('.structItem-title'); - if (type === 'date') { - valA = parseInt(valA.getAttribute('data-time')) * 1000; - valB = parseInt(valB.getAttribute('data-time')) * 1000; - return asc ? valA - valB : valB - valA; - } + const dateMs = dateEl ? (parseInt(dateEl.getAttribute('data-time') || '0', 10) * 1000) : 0; + const section = forumEl ? forumEl.textContent.trim() : ''; - if (type === 'prefix') { - valA = valA.innerText.trim().split(' - ')[0]; - valB = valB.innerText.trim().split(' - ')[0]; - return asc ? valA.localeCompare(valB) : valB.localeCompare(valA); - } + let prefix = ''; + if (titleEl) { + const t = titleEl.textContent.trim(); + const mBracket = t.match(/^\s*\[([^\]]+)\]/); + if (mBracket && mBracket[1]) prefix = mBracket[1].trim(); + else { + const parts = t.split(' - '); + prefix = (parts.length > 1 ? parts[0] : t).trim(); + } + } - valA = valA.innerText.trim(); - valB = valB.innerText.trim(); - return asc ? valA.localeCompare(valB) : valB.localeCompare(valA); - }); + const obj = { rowEl, i, dateMs, section, prefix }; + rowMap.set(rowEl, obj); + return obj; + } - sortedRows.forEach(row => reportTable.appendChild(row)); + function rebuildIndex(reportTable) { + rowIndex = []; + rowMap = new WeakMap(); // recreate instead of .clear() + const rows = reportTable.querySelectorAll('.structItem'); + let i = 0; + for (const r of rows) rowIndex.push(computeKeys(r, i++)); + } + + function showBusy(where) { + let badge = document.getElementById('sr-busy'); + if (!badge) { + badge = document.createElement('span'); + badge.id = 'sr-busy'; + badge.style.cssText = 'margin-left:8px;font-size:12px;opacity:.8;'; + where.appendChild(badge); + } + badge.textContent = 'Sorting…'; + } + function hideBusy() { + const badge = document.getElementById('sr-busy'); + if (badge) badge.textContent = ''; + } + + // Stream DOM writes across frames + function renderChunked(reportTable, orderedRows) { + return new Promise((resolve) => { + const total = orderedRows.length; + const chunk = Math.max(50, Math.floor(total / 20)); // ~20 frames worst case + let idx = 0; + + reportTable.textContent = ''; + + function step() { + const frag = document.createDocumentFragment(); + for (let n = 0; n < chunk && idx < total; n++, idx++) { + frag.appendChild(orderedRows[idx]); } - }; -})(); \ No newline at end of file + reportTable.appendChild(frag); + if (idx < total) requestAnimationFrame(step); + else resolve(); + } + requestAnimationFrame(step); + }); + } + + // Cancelable sort + let sortAbort = { aborted: false }; + async function sortAndRender(reportTable, kind, asc) { + sortAbort.aborted = true; + sortAbort = { aborted: false }; + const token = sortAbort; + + showBusy(document.getElementById('sortType')?.parentElement || reportTable); + await new Promise(res => rIC(res, { timeout: 100 })); + + let cmp; + if (kind === 'date') { + cmp = (a, b) => (asc ? a.dateMs - b.dateMs : b.dateMs - a.dateMs) || (a.i - b.i); + } else if (kind === 'section') { + cmp = (a, b) => { + const c = asc ? collator.compare(a.section, b.section) : collator.compare(b.section, a.section); + return c || (a.i - b.i); + }; + } else if (kind === 'prefix') { + cmp = (a, b) => { + const c = asc ? collator.compare(a.prefix, b.prefix) : collator.compare(b.prefix, a.prefix); + return c || (a.i - b.i); + }; + } else return; + + const items = rowIndex.slice(); + items.sort(cmp); + if (token.aborted) return; + + const nodes = items.map(it => it.rowEl); + await renderChunked(reportTable, nodes); + hideBusy(); + } + + (async function boot() { + const reportTable = await waitForElement('.structItemContainer').catch(() => null); + if (!reportTable) return; + + let host = document.querySelector('.block-outer .block-outer-opposite .buttonGroup') + || document.querySelector('.block-outer .block-outer-opposite') + || document.querySelector('.block-outer') + || document.body; + + const sortWrapper = document.createElement('span'); + sortWrapper.style.display = 'inline-flex'; + sortWrapper.style.alignItems = 'center'; + sortWrapper.style.gap = '6px'; + sortWrapper.style.marginLeft = '8px'; + sortWrapper.innerHTML = ` + + + `; + host.insertBefore(sortWrapper, host.firstChild || null); + + rebuildIndex(reportTable); + + const scheduleReindex = debounce(() => rebuildIndex(reportTable), 150); + const mo = new MutationObserver(muts => { + for (const m of muts) { + if (m.addedNodes.length || m.removedNodes.length) { + scheduleReindex(); + break; + } + } + }); + mo.observe(reportTable, { childList: true, subtree: true }); + + const select = document.getElementById('sortType'); + select.addEventListener('change', () => { + const v = select.value; + if (v === 'dateAsc') sortAndRender(reportTable, 'date', true); + else if (v === 'dateDesc') sortAndRender(reportTable, 'date', false); + else if (v === 'sectionAsc') sortAndRender(reportTable, 'section', true); + else if (v === 'sectionDesc') sortAndRender(reportTable, 'section', false); + else if (v === 'prefixAsc') sortAndRender(reportTable, 'prefix', true); + else if (v === 'prefixDesc') sortAndRender(reportTable, 'prefix', false); + }); + + rIC(() => sortAndRender(reportTable, 'date', false), { timeout: 200 }); + })(); +})();