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 });
+ })();
+})();