Compare commits
No commits in common. "571d507889d5f1c705053b9ab5e0eed3690387ad" and "7dc68c76152f03e760d7aecc5ccff5977e6b8051" have entirely different histories.
571d507889
...
7dc68c7615
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "multi-hoster-uploader",
|
||||
"version": "2.8.4",
|
||||
"version": "2.8.3",
|
||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
105
renderer/app.js
105
renderer/app.js
@ -57,9 +57,6 @@ let historySortState = { key: 'date', direction: 'desc' };
|
||||
let sessionFilesData = [];
|
||||
const recentSortState = { key: 'date', direction: 'desc' };
|
||||
const selectedRecentIds = new Set();
|
||||
// Maintained incrementally — avoids O(n) filter() scans every 250ms in the status bar.
|
||||
let _sessionDoneCount = 0;
|
||||
let _sessionErrorCount = 0;
|
||||
|
||||
// --- Init ---
|
||||
async function init() {
|
||||
@ -790,34 +787,6 @@ function scheduleQueueRender() {
|
||||
requestAnimationFrame(() => { _renderQueued = false; renderQueueTable(); });
|
||||
}
|
||||
|
||||
let _recentRenderQueued = false;
|
||||
function scheduleRecentRender() {
|
||||
if (_recentRenderQueued) return;
|
||||
_recentRenderQueued = true;
|
||||
requestAnimationFrame(() => { _recentRenderQueued = false; renderRecentUploadsPanel(); });
|
||||
}
|
||||
|
||||
// Toggle the .selected class on existing rows without rebuilding the table.
|
||||
// Used on click/selection changes — O(rendered rows) instead of O(total rows × sort).
|
||||
function applyQueueSelectionClasses() {
|
||||
const tbody = document.getElementById('queueBody');
|
||||
if (!tbody) return;
|
||||
const rows = tbody.querySelectorAll('.queue-row');
|
||||
for (const tr of rows) {
|
||||
tr.classList.toggle('selected', selectedJobIds.has(tr.dataset.jobId));
|
||||
}
|
||||
}
|
||||
|
||||
function applyRecentSelectionClasses() {
|
||||
const tbody = document.getElementById('recentFilesBody');
|
||||
if (!tbody) return;
|
||||
const rows = tbody.querySelectorAll('.recent-file-row');
|
||||
for (const tr of rows) {
|
||||
const order = parseInt(tr.dataset.order, 10);
|
||||
tr.classList.toggle('selected', selectedRecentIds.has(order));
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleThrottledUIUpdate() {
|
||||
if (_uiUpdateTimer) return;
|
||||
_uiUpdateTimer = setTimeout(() => {
|
||||
@ -1038,8 +1007,8 @@ function getStatusText(job) {
|
||||
// --- Queue interactions ---
|
||||
function handleRowClick(e, row) {
|
||||
const jobId = row.dataset.jobId;
|
||||
// Clear recent panel selection when clicking in queue — class-toggle only.
|
||||
if (selectedRecentIds.size > 0) { selectedRecentIds.clear(); applyRecentSelectionClasses(); }
|
||||
// Clear recent panel selection when clicking in queue
|
||||
if (selectedRecentIds.size > 0) { selectedRecentIds.clear(); renderRecentUploadsPanel(); }
|
||||
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (selectedJobIds.has(jobId)) selectedJobIds.delete(jobId);
|
||||
@ -1058,7 +1027,7 @@ function handleRowClick(e, row) {
|
||||
selectedJobIds.clear();
|
||||
selectedJobIds.add(jobId);
|
||||
// Single click on done job -> copy link
|
||||
const job = _jobIndexById.get(jobId);
|
||||
const job = queueJobs.find(j => j.id === jobId);
|
||||
if (job && job.status === 'done' && job.result) {
|
||||
const link = job.result.download_url || job.result.embed_url || '';
|
||||
if (link) {
|
||||
@ -1067,9 +1036,7 @@ function handleRowClick(e, row) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Selection changes don't change sort order / row content — just toggle classes.
|
||||
applyQueueSelectionClasses();
|
||||
updateQueueActionButtons();
|
||||
renderQueueTable();
|
||||
}
|
||||
|
||||
// --- Context menu ---
|
||||
@ -1081,8 +1048,7 @@ function handleRowContextMenu(e, row) {
|
||||
if (!selectedJobIds.has(jobId)) {
|
||||
selectedJobIds.clear();
|
||||
selectedJobIds.add(jobId);
|
||||
applyQueueSelectionClasses();
|
||||
updateQueueActionButtons();
|
||||
renderQueueTable();
|
||||
}
|
||||
showContextMenu(e.clientX, e.clientY);
|
||||
}
|
||||
@ -1146,14 +1112,7 @@ function hideContextMenu() {
|
||||
|
||||
function deleteSelectedRecentFiles() {
|
||||
if (selectedRecentIds.size === 0) return;
|
||||
let removedDone = 0, removedErr = 0;
|
||||
sessionFilesData = sessionFilesData.filter(r => {
|
||||
if (!selectedRecentIds.has(r.order)) return true;
|
||||
if (r.isError) removedErr++; else removedDone++;
|
||||
return false;
|
||||
});
|
||||
_sessionDoneCount = Math.max(0, _sessionDoneCount - removedDone);
|
||||
_sessionErrorCount = Math.max(0, _sessionErrorCount - removedErr);
|
||||
sessionFilesData = sessionFilesData.filter(r => !selectedRecentIds.has(r.order));
|
||||
selectedRecentIds.clear();
|
||||
renderRecentUploadsPanel();
|
||||
}
|
||||
@ -1162,8 +1121,6 @@ function clearAllRecentFiles() {
|
||||
if (sessionFilesData.length === 0) return;
|
||||
if (!confirm(`Wirklich alle ${sessionFilesData.length} Links aus diesem Panel entfernen?`)) return;
|
||||
sessionFilesData = [];
|
||||
_sessionDoneCount = 0;
|
||||
_sessionErrorCount = 0;
|
||||
selectedRecentIds.clear();
|
||||
renderRecentUploadsPanel();
|
||||
}
|
||||
@ -1930,9 +1887,7 @@ function maybeAddSessionFile(job) {
|
||||
isError: false,
|
||||
order: sessionFilesData.length
|
||||
});
|
||||
_sessionDoneCount++;
|
||||
// Coalesce rapid successive adds into one render per frame.
|
||||
scheduleRecentRender();
|
||||
renderRecentUploadsPanel();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1966,11 +1921,10 @@ function applySummaryResults(summary) {
|
||||
}
|
||||
}
|
||||
|
||||
// Single-pass queue stats computation (shared by status bar + stats panel).
|
||||
// Also tracks inProgressBytes so the status bar doesn't need a second scan.
|
||||
// Single-pass queue stats computation (shared by status bar + stats panel)
|
||||
function _computeQueueStats() {
|
||||
let remaining = 0, inProgress = 0, done = 0, errors = 0;
|
||||
let bytesRemaining = 0, totalSize = 0, remainingSize = 0, inProgressBytes = 0;
|
||||
let bytesRemaining = 0, totalSize = 0, remainingSize = 0;
|
||||
const total = queueJobs.length;
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
@ -1983,7 +1937,6 @@ function _computeQueueStats() {
|
||||
if (s === 'uploading' || s === 'getting-server' || s === 'retrying') {
|
||||
inProgress++;
|
||||
remaining++;
|
||||
inProgressBytes += bu;
|
||||
bytesRemaining += Math.max(0, bt - bu);
|
||||
remainingSize += Math.max(0, bt - bu);
|
||||
} else if (s === 'preview' || s === 'queued') {
|
||||
@ -1999,7 +1952,7 @@ function _computeQueueStats() {
|
||||
}
|
||||
}
|
||||
|
||||
return { total, remaining, inProgress, done, errors, bytesRemaining, totalSize, remainingSize, inProgressBytes };
|
||||
return { total, remaining, inProgress, done, errors, bytesRemaining, totalSize, remainingSize };
|
||||
}
|
||||
|
||||
function updateStatusBar() {
|
||||
@ -2019,7 +1972,15 @@ function updateStatusBar() {
|
||||
|
||||
document.getElementById('sbState').textContent = stateText;
|
||||
document.getElementById('sbSpeed').textContent = formatSpeed(lastUploadStats.globalSpeedKbs || 0);
|
||||
const uploadedSize = _sessionUploadedBytes + stats.inProgressBytes;
|
||||
// Session-based bytes: survive removeFromQueueOnDone
|
||||
// Uploaded = done jobs (session) + in-progress bytes still in queue
|
||||
let inProgressBytes = 0;
|
||||
for (const job of queueJobs) {
|
||||
if (job.status === 'uploading' || job.status === 'getting-server' || job.status === 'retrying') {
|
||||
inProgressBytes += job.bytesUploaded || 0;
|
||||
}
|
||||
}
|
||||
const uploadedSize = _sessionUploadedBytes + inProgressBytes;
|
||||
const totalSize = Math.max(stats.totalSize, _sessionTotalBytes);
|
||||
document.getElementById('sbTotal').textContent = `${formatSize(uploadedSize)} / ${formatSize(totalSize)}`;
|
||||
document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`;
|
||||
@ -2027,8 +1988,10 @@ function updateStatusBar() {
|
||||
document.getElementById('sbQueueCount').textContent = `Total: ${stats.total}`;
|
||||
document.getElementById('sbRemainingCount').textContent = `Remaining: ${stats.remaining}`;
|
||||
document.getElementById('sbInProgressCount').textContent = `In Progress: ${stats.inProgress}`;
|
||||
document.getElementById('sbDoneCount').textContent = `Done: ${_sessionDoneCount}`;
|
||||
document.getElementById('sbErrorCount').textContent = `Error: ${_sessionErrorCount}`;
|
||||
const sessionDone = sessionFilesData.filter(r => !r.isError).length;
|
||||
const sessionErrors = sessionFilesData.filter(r => r.isError).length;
|
||||
document.getElementById('sbDoneCount').textContent = `Done: ${sessionDone}`;
|
||||
document.getElementById('sbErrorCount').textContent = `Error: ${sessionErrors}`;
|
||||
}
|
||||
|
||||
// --- Health Check ---
|
||||
@ -3089,17 +3052,9 @@ async function exportHistory() {
|
||||
showCopyToast(`Verlauf exportiert (${result.totalRows || 0} Zeilen)`);
|
||||
}
|
||||
|
||||
// Memoize sort result: invalidated only when data length changes or sort state changes.
|
||||
// Selection changes and re-renders reuse the cached sorted array — a big win when
|
||||
// the panel has thousands of rows and the sort is stable.
|
||||
let _recentSortCache = { sig: '', result: [] };
|
||||
|
||||
function sortRecentFiles(data) {
|
||||
const { key, direction } = recentSortState;
|
||||
const sig = `${key}|${direction}|${data.length}`;
|
||||
if (_recentSortCache.sig === sig) return _recentSortCache.result;
|
||||
|
||||
const sorted = data.slice();
|
||||
const { key, direction } = recentSortState;
|
||||
const dir = direction === 'asc' ? 1 : -1;
|
||||
sorted.sort((a, b) => {
|
||||
if (key === 'date') return dir * ((a.dateTs - b.dateTs) || (a.order - b.order));
|
||||
@ -3108,7 +3063,6 @@ function sortRecentFiles(data) {
|
||||
if (key === 'link') return dir * _collatorDE.compare(a.link, b.link);
|
||||
return 0;
|
||||
});
|
||||
_recentSortCache = { sig, result: sorted };
|
||||
return sorted;
|
||||
}
|
||||
|
||||
@ -3152,16 +3106,14 @@ function renderRecentUploadsPanel() {
|
||||
tbody.addEventListener('click', (e) => {
|
||||
const tr = e.target.closest('.recent-file-row');
|
||||
if (!tr) return;
|
||||
// Clear queue selection when clicking in recent panel — class-toggle only.
|
||||
if (selectedJobIds.size > 0) { selectedJobIds.clear(); applyQueueSelectionClasses(); updateQueueActionButtons(); }
|
||||
// Clear queue selection when clicking in recent panel
|
||||
if (selectedJobIds.size > 0) { selectedJobIds.clear(); renderQueueTable(); updateQueueActionButtons(); }
|
||||
const id = parseInt(tr.dataset.order, 10);
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (selectedRecentIds.has(id)) selectedRecentIds.delete(id);
|
||||
else selectedRecentIds.add(id);
|
||||
} else if (e.shiftKey && selectedRecentIds.size > 0) {
|
||||
// Use already-sorted DOM order (cheap) instead of re-sorting the full array.
|
||||
const sortedOrders = Array.from(tbody.querySelectorAll('.recent-file-row'))
|
||||
.map(r => parseInt(r.dataset.order, 10));
|
||||
const sortedOrders = sortRecentFiles(sessionFilesData).map(r => r.order);
|
||||
const lastIdx = sortedOrders.findIndex(o => selectedRecentIds.has(o));
|
||||
const curIdx = sortedOrders.indexOf(id);
|
||||
if (lastIdx >= 0 && curIdx >= 0) {
|
||||
@ -3173,8 +3125,7 @@ function renderRecentUploadsPanel() {
|
||||
selectedRecentIds.clear();
|
||||
selectedRecentIds.add(id);
|
||||
}
|
||||
// Selection change — toggle classes, no tbody rebuild.
|
||||
applyRecentSelectionClasses();
|
||||
renderRecentUploadsPanel();
|
||||
});
|
||||
|
||||
tbody.addEventListener('dblclick', (e) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user