perf: lazy history refresh + append-only recent panel + queue-cleanup merge
Three more targeted wins:
- loadHistory() was called unconditionally on every handleBatchDone,
doing an IPC roundtrip + full history-table rebuild even when the
user is on the Upload tab and can't see it. Now it sets a dirty
flag and the actual refresh is deferred until the user switches
to the Verlauf tab. On a fresh tab click it always runs.
- renderRecentUploadsPanel append-only fast path: when the sort is
'date desc' (the default) and the dataset only grew, the panel
inserts the new rows at the top via insertAdjacentHTML instead
of rebuilding the 5000-row tbody from scratch. Length shrinks or
sort-change still trigger a full rebuild.
- handleBatchDone's removeFromQueueOnDone cleanup now does one pass
(build keep-list + detach from index together) instead of two
separate filter() scans over queueJobs.
This commit is contained in:
parent
1bcd7a2078
commit
f16dd9ffa6
@ -200,13 +200,21 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Tab switching ---
|
// --- Tab switching ---
|
||||||
|
let _historyDirty = false;
|
||||||
|
function _isHistoryTabActive() {
|
||||||
|
const tab = document.querySelector('.tab.active');
|
||||||
|
return !!(tab && tab.dataset.view === 'history');
|
||||||
|
}
|
||||||
document.querySelectorAll('.tab').forEach(tab => {
|
document.querySelectorAll('.tab').forEach(tab => {
|
||||||
tab.addEventListener('click', () => {
|
tab.addEventListener('click', () => {
|
||||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
||||||
tab.classList.add('active');
|
tab.classList.add('active');
|
||||||
document.getElementById(`${tab.dataset.view}-view`).classList.add('active');
|
document.getElementById(`${tab.dataset.view}-view`).classList.add('active');
|
||||||
if (tab.dataset.view === 'history') loadHistory();
|
if (tab.dataset.view === 'history') {
|
||||||
|
_historyDirty = false;
|
||||||
|
loadHistory();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1820,16 +1828,25 @@ function handleBatchDone(summary) {
|
|||||||
updateQueueActionButtons();
|
updateQueueActionButtons();
|
||||||
renderQueueTable();
|
renderQueueTable();
|
||||||
renderRecentUploadsPanel();
|
renderRecentUploadsPanel();
|
||||||
loadHistory();
|
// History is only visible on the Verlauf tab. Mark it dirty and refresh when
|
||||||
|
// the user actually switches to it — skips an IPC + full table rebuild per
|
||||||
|
// batch-done when the user is watching the upload view.
|
||||||
|
_historyDirty = true;
|
||||||
|
if (_isHistoryTabActive()) loadHistory();
|
||||||
|
|
||||||
const removeOnDone = config.globalSettings && config.globalSettings.removeFromQueueOnDone;
|
const removeOnDone = config.globalSettings && config.globalSettings.removeFromQueueOnDone;
|
||||||
if (removeOnDone) {
|
if (removeOnDone) {
|
||||||
const doneJobs = queueJobs.filter(j => j.status === 'done');
|
// Single pass: build the keep-list and clean up the index for removed jobs.
|
||||||
for (const job of doneJobs) {
|
const nextJobs = [];
|
||||||
removeJobFromIndex(job);
|
for (const job of queueJobs) {
|
||||||
selectedJobIds.delete(job.id);
|
if (job.status === 'done') {
|
||||||
|
removeJobFromIndex(job);
|
||||||
|
selectedJobIds.delete(job.id);
|
||||||
|
} else {
|
||||||
|
nextJobs.push(job);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
queueJobs = queueJobs.filter(j => j.status !== 'done');
|
queueJobs = nextJobs;
|
||||||
renderQueueTable();
|
renderQueueTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3215,24 +3232,50 @@ function updateRecentSortHeaders() {
|
|||||||
|
|
||||||
let _recentListenersBound = false;
|
let _recentListenersBound = false;
|
||||||
|
|
||||||
|
function _buildRecentRowHtml(row) {
|
||||||
|
const cls = `recent-file-row${row.isError ? ' error' : ''}${selectedRecentIds.has(row.order) ? ' selected' : ''}`;
|
||||||
|
return `<tr class="${cls}" data-order="${row.order}" data-link="${escapeAttr(row.link)}">`
|
||||||
|
+ `<td>${escapeHtml(row.date)}</td>`
|
||||||
|
+ `<td title="${escapeAttr(row.filename)}">${escapeHtml(row.filename)}</td>`
|
||||||
|
+ `<td>${escapeHtml(row.host)}</td>`
|
||||||
|
+ `<td title="${escapeAttr(row.link)}">${escapeHtml(row.link)}</td>`
|
||||||
|
+ `</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracks the last rendered dataset so we can append-only when the user is just
|
||||||
|
// accumulating new uploads (the default case: sort=date desc, rows only grow).
|
||||||
|
let _recentLastRenderedSig = '';
|
||||||
|
let _recentLastRenderedLen = 0;
|
||||||
|
|
||||||
function renderRecentUploadsPanel() {
|
function renderRecentUploadsPanel() {
|
||||||
const tbody = document.getElementById('recentFilesBody');
|
const tbody = document.getElementById('recentFilesBody');
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
if (!sessionFilesData.length) {
|
if (!sessionFilesData.length) {
|
||||||
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">Noch keine Uploads in dieser Session.</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">Noch keine Uploads in dieser Session.</td></tr>';
|
||||||
|
_recentLastRenderedSig = '';
|
||||||
|
_recentLastRenderedLen = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = sortRecentFiles(sessionFilesData);
|
const rows = sortRecentFiles(sessionFilesData);
|
||||||
|
const sig = `${recentSortState.key}|${recentSortState.direction}`;
|
||||||
|
const dateDescAppendOnly = sig === 'date|desc'
|
||||||
|
&& _recentLastRenderedSig === sig
|
||||||
|
&& rows.length > _recentLastRenderedLen
|
||||||
|
&& tbody.querySelectorAll('.recent-file-row').length === _recentLastRenderedLen;
|
||||||
|
|
||||||
tbody.innerHTML = rows.map(row => `
|
if (dateDescAppendOnly) {
|
||||||
<tr class="recent-file-row${row.isError ? ' error' : ''}${selectedRecentIds.has(row.order) ? ' selected' : ''}" data-order="${row.order}" data-link="${escapeAttr(row.link)}">
|
// Fast path: only new rows (date desc puts newest on top) — insert them
|
||||||
<td>${escapeHtml(row.date)}</td>
|
// at the top without rebuilding the 5000-row tbody below.
|
||||||
<td title="${escapeAttr(row.filename)}">${escapeHtml(row.filename)}</td>
|
const added = rows.length - _recentLastRenderedLen;
|
||||||
<td>${escapeHtml(row.host)}</td>
|
let html = '';
|
||||||
<td title="${escapeAttr(row.link)}">${escapeHtml(row.link)}</td>
|
for (let i = 0; i < added; i++) html += _buildRecentRowHtml(rows[i]);
|
||||||
</tr>
|
tbody.insertAdjacentHTML('afterbegin', html);
|
||||||
`).join('');
|
} else {
|
||||||
|
tbody.innerHTML = rows.map(_buildRecentRowHtml).join('');
|
||||||
|
}
|
||||||
|
_recentLastRenderedSig = sig;
|
||||||
|
_recentLastRenderedLen = rows.length;
|
||||||
|
|
||||||
// Event delegation – bind once, not per-row
|
// Event delegation – bind once, not per-row
|
||||||
if (!_recentListenersBound) {
|
if (!_recentListenersBound) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user