Update report-organizer.js
Optimized and faster
This commit is contained in:
parent
1796314db6
commit
79a44079cc
@ -1,93 +1,204 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Sort Reports
|
// @name Sort Reports (no-freeze, resilient UI) — fixed WeakMap
|
||||||
// @namespace http://tampermonkey.net/
|
// @namespace http://tampermonkey.net/
|
||||||
// @version 2024-09-18
|
// @version 4.1
|
||||||
// @description Sort Reports
|
// @description Fast, chunked sorting with robust UI injection
|
||||||
// @author You
|
|
||||||
// @match https://f95zone.to/reports/queue/*
|
// @match https://f95zone.to/reports/queue/*
|
||||||
// @icon https://www.google.com/s2/favicons?sz=64&domain=f95zone.to
|
// @icon https://www.google.com/s2/favicons?sz=64&domain=f95zone.to
|
||||||
// @grant none
|
// @grant none
|
||||||
// @updateURL https://git.zonies.xyz/Ryahn/F95Zone-Scripts/raw/branch/main/report-organizer.js
|
// @run-at document-idle
|
||||||
// @downloadURL https://git.zonies.xyz/Ryahn/F95Zone-Scripts/raw/branch/main/report-organizer.js
|
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
(function() {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
window.onload = function() {
|
// Wait for a selector with MutationObserver fallback
|
||||||
const reportTable = document.querySelector('.structItemContainer');
|
function waitForElement(selector, root = document, timeoutMs = 15000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const first = root.querySelector(selector);
|
||||||
|
if (first) return resolve(first);
|
||||||
|
|
||||||
|
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 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); };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const rIC = window.requestIdleCallback || function (cb) { return setTimeout(cb, 17); };
|
||||||
|
|
||||||
|
function debounce(fn, ms) {
|
||||||
|
let t;
|
||||||
|
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
|
||||||
|
}
|
||||||
|
|
||||||
|
const collator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
|
||||||
|
|
||||||
|
// Caches
|
||||||
|
let rowIndex = []; // [{rowEl, i, dateMs, section, prefix}]
|
||||||
|
let rowMap = new WeakMap(); // rowEl -> cached keys
|
||||||
|
|
||||||
|
function computeKeys(rowEl, i) {
|
||||||
|
const dateEl = rowEl.querySelector('.structItem-latestDate');
|
||||||
|
const forumEl = rowEl.querySelector('.structItem-forum a');
|
||||||
|
const titleEl = rowEl.querySelector('.structItem-title');
|
||||||
|
|
||||||
|
const dateMs = dateEl ? (parseInt(dateEl.getAttribute('data-time') || '0', 10) * 1000) : 0;
|
||||||
|
const section = forumEl ? forumEl.textContent.trim() : '';
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = { rowEl, i, dateMs, section, prefix };
|
||||||
|
rowMap.set(rowEl, obj);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
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;
|
if (!reportTable) return;
|
||||||
|
|
||||||
const sortContainer = document.createElement('div');
|
let host = document.querySelector('.block-outer .block-outer-opposite .buttonGroup')
|
||||||
sortContainer.style.textAlign = 'right';
|
|| document.querySelector('.block-outer .block-outer-opposite')
|
||||||
sortContainer.innerHTML = `
|
|| document.querySelector('.block-outer')
|
||||||
<label for="sortType">Sort By:</label>
|
|| document.body;
|
||||||
<select id="sortType">
|
|
||||||
|
const sortWrapper = document.createElement('span');
|
||||||
|
sortWrapper.style.display = 'inline-flex';
|
||||||
|
sortWrapper.style.alignItems = 'center';
|
||||||
|
sortWrapper.style.gap = '6px';
|
||||||
|
sortWrapper.style.marginLeft = '8px';
|
||||||
|
sortWrapper.innerHTML = `
|
||||||
|
<label for="sortType" style="font-weight:500;">Sort By:</label>
|
||||||
|
<select id="sortType"
|
||||||
|
style="background-color:#2f2d34; color:#fff; border-radius:6px; padding:4px 6px; border:1px solid #555;">
|
||||||
|
<option value="dateDesc" selected>Date DESC</option>
|
||||||
<option value="dateAsc">Date ASC</option>
|
<option value="dateAsc">Date ASC</option>
|
||||||
<option value="dateDesc">Date DESC</option>
|
|
||||||
<option value="sectionAsc">Sub-section ASC</option>
|
|
||||||
<option value="sectionDesc">Sub-section DESC</option>
|
<option value="sectionDesc">Sub-section DESC</option>
|
||||||
<option value="prefixAsc">Prefix ASC</option>
|
<option value="sectionAsc">Sub-section ASC</option>
|
||||||
<option value="prefixDesc">Prefix DESC</option>
|
<option value="prefixDesc">Prefix DESC</option>
|
||||||
|
<option value="prefixAsc">Prefix ASC</option>
|
||||||
</select>
|
</select>
|
||||||
`;
|
`;
|
||||||
|
host.insertBefore(sortWrapper, host.firstChild || null);
|
||||||
|
|
||||||
reportTable.parentNode.insertBefore(sortContainer, reportTable);
|
rebuildIndex(reportTable);
|
||||||
|
|
||||||
document.getElementById('sortType').addEventListener('change', function() {
|
const scheduleReindex = debounce(() => rebuildIndex(reportTable), 150);
|
||||||
const sortValue = this.value;
|
const mo = new MutationObserver(muts => {
|
||||||
const rows = Array.from(reportTable.querySelectorAll('.structItem'));
|
for (const m of muts) {
|
||||||
|
if (m.addedNodes.length || m.removedNodes.length) {
|
||||||
switch (sortValue) {
|
scheduleReindex();
|
||||||
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;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
mo.observe(reportTable, { childList: true, subtree: true });
|
||||||
|
|
||||||
function sortRows(rows, selector, asc = true, type = 'text') {
|
const select = document.getElementById('sortType');
|
||||||
const sortedRows = rows.sort((a, b) => {
|
select.addEventListener('change', () => {
|
||||||
let valA = a.querySelector(selector);
|
const v = select.value;
|
||||||
let valB = b.querySelector(selector);
|
if (v === 'dateAsc') sortAndRender(reportTable, 'date', true);
|
||||||
|
else if (v === 'dateDesc') sortAndRender(reportTable, 'date', false);
|
||||||
if (!valA || !valB) {
|
else if (v === 'sectionAsc') sortAndRender(reportTable, 'section', true);
|
||||||
return asc ? (valA ? -1 : 1) : (valB ? -1 : 1);
|
else if (v === 'sectionDesc') sortAndRender(reportTable, 'section', false);
|
||||||
}
|
else if (v === 'prefixAsc') sortAndRender(reportTable, 'prefix', true);
|
||||||
|
else if (v === 'prefixDesc') sortAndRender(reportTable, 'prefix', false);
|
||||||
if (type === 'date') {
|
|
||||||
valA = parseInt(valA.getAttribute('data-time')) * 1000;
|
|
||||||
valB = parseInt(valB.getAttribute('data-time')) * 1000;
|
|
||||||
return asc ? valA - valB : valB - valA;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'prefix') {
|
|
||||||
valA = valA.innerText.trim().split(' - ')[0];
|
|
||||||
valB = valB.innerText.trim().split(' - ')[0];
|
|
||||||
return asc ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
||||||
}
|
|
||||||
|
|
||||||
valA = valA.innerText.trim();
|
|
||||||
valB = valB.innerText.trim();
|
|
||||||
return asc ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
sortedRows.forEach(row => reportTable.appendChild(row));
|
rIC(() => sortAndRender(reportTable, 'date', false), { timeout: 200 });
|
||||||
}
|
})();
|
||||||
};
|
|
||||||
})();
|
})();
|
||||||
Loading…
x
Reference in New Issue
Block a user