perf: major rendering optimization for large concurrent uploads
- Throttle progress events to 250ms intervals (was every byte chunk) - Batch UI updates during uploads (render/statusbar/stats every 200ms) - In-place row updates instead of full innerHTML table rebuild - Single-pass queue stats computation (was 9 separate array filters) - Remove CSS transition on progress bars (caused layout thrashing) - Event delegation for recent files table (was per-row listener rebind) - Increase persist debounce to 10s during uploads (was 3s) - Remove redundant "Ziele auswählen" button (hoster selection on file add) - Dark title bar via nativeTheme Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b4c786cf04
commit
6b2b2ca04c
@ -59,10 +59,12 @@
|
|||||||
"alwaysOnTop": false,
|
"alwaysOnTop": false,
|
||||||
"shutdownAfterFinish": "nothing",
|
"shutdownAfterFinish": "nothing",
|
||||||
"logFilePath": "",
|
"logFilePath": "",
|
||||||
|
"sessionLog": false,
|
||||||
"resumeQueueOnLaunch": true,
|
"resumeQueueOnLaunch": true,
|
||||||
"parallelUploadCount": 0,
|
"parallelUploadCount": 0,
|
||||||
"scaleParallelUploads": true,
|
"scaleParallelUploads": true,
|
||||||
"removeFromQueueOnDone": false,
|
"removeFromQueueOnDone": false,
|
||||||
|
"globalMaxSpeedKbs": 0,
|
||||||
"pendingQueue": {
|
"pendingQueue": {
|
||||||
"selectedUploadHosters": [
|
"selectedUploadHosters": [
|
||||||
"doodstream.com",
|
"doodstream.com",
|
||||||
@ -79,7 +81,7 @@
|
|||||||
],
|
],
|
||||||
"queueJobs": [
|
"queueJobs": [
|
||||||
{
|
{
|
||||||
"id": "preview-1773193741983-c1qa7p",
|
"id": "preview-1773271047205-k8l83r",
|
||||||
"file": "C:\\Users\\ploet\\Downloads\\Einfach mal die Fresse halten!!!.mp4",
|
"file": "C:\\Users\\ploet\\Downloads\\Einfach mal die Fresse halten!!!.mp4",
|
||||||
"fileName": "Einfach mal die Fresse halten!!!.mp4",
|
"fileName": "Einfach mal die Fresse halten!!!.mp4",
|
||||||
"hoster": "doodstream.com",
|
"hoster": "doodstream.com",
|
||||||
@ -90,7 +92,7 @@
|
|||||||
"maxAttempts": 0
|
"maxAttempts": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "preview-1773193741983-bvlsvn",
|
"id": "preview-1773271047206-npnpph",
|
||||||
"file": "C:\\Users\\ploet\\Downloads\\Einfach mal die Fresse halten!!!.mp4",
|
"file": "C:\\Users\\ploet\\Downloads\\Einfach mal die Fresse halten!!!.mp4",
|
||||||
"fileName": "Einfach mal die Fresse halten!!!.mp4",
|
"fileName": "Einfach mal die Fresse halten!!!.mp4",
|
||||||
"hoster": "voe.sx",
|
"hoster": "voe.sx",
|
||||||
@ -101,7 +103,7 @@
|
|||||||
"maxAttempts": 0
|
"maxAttempts": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "preview-1773193741983-a7aixs",
|
"id": "preview-1773271047206-q2skl1",
|
||||||
"file": "C:\\Users\\ploet\\Downloads\\Einfach mal die Fresse halten!!!.mp4",
|
"file": "C:\\Users\\ploet\\Downloads\\Einfach mal die Fresse halten!!!.mp4",
|
||||||
"fileName": "Einfach mal die Fresse halten!!!.mp4",
|
"fileName": "Einfach mal die Fresse halten!!!.mp4",
|
||||||
"hoster": "vidmoly.me",
|
"hoster": "vidmoly.me",
|
||||||
@ -112,7 +114,7 @@
|
|||||||
"maxAttempts": 0
|
"maxAttempts": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "preview-1773193741983-39jnfg",
|
"id": "preview-1773271047206-cek27b",
|
||||||
"file": "C:\\Users\\ploet\\Downloads\\Einfach mal die Fresse halten!!!.mp4",
|
"file": "C:\\Users\\ploet\\Downloads\\Einfach mal die Fresse halten!!!.mp4",
|
||||||
"fileName": "Einfach mal die Fresse halten!!!.mp4",
|
"fileName": "Einfach mal die Fresse halten!!!.mp4",
|
||||||
"hoster": "byse.sx",
|
"hoster": "byse.sx",
|
||||||
|
|||||||
@ -324,6 +324,9 @@ class UploadManager extends EventEmitter {
|
|||||||
|
|
||||||
this.activeJobs.set(uploadId, { jobId, speedKbs: 0, bytesUploaded: 0 });
|
this.activeJobs.set(uploadId, { jobId, speedKbs: 0, bytesUploaded: 0 });
|
||||||
|
|
||||||
|
let lastEmitTime = 0;
|
||||||
|
const PROGRESS_EMIT_INTERVAL = 250; // ms – throttle UI updates
|
||||||
|
|
||||||
const progressCb = (bytesUploaded, bytesTotal) => {
|
const progressCb = (bytesUploaded, bytesTotal) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const elapsed = Math.round((now - jobStart) / 1000);
|
const elapsed = Math.round((now - jobStart) / 1000);
|
||||||
@ -335,12 +338,16 @@ class UploadManager extends EventEmitter {
|
|||||||
lastSpeedTime = now;
|
lastSpeedTime = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.activeJobs.set(uploadId, { jobId, speedKbs: currentSpeedKbs, bytesUploaded });
|
||||||
|
|
||||||
|
// Throttle progress emissions to reduce IPC + rendering overhead
|
||||||
|
if (now - lastEmitTime < PROGRESS_EMIT_INTERVAL) return;
|
||||||
|
lastEmitTime = now;
|
||||||
|
|
||||||
const remaining = currentSpeedKbs > 0
|
const remaining = currentSpeedKbs > 0
|
||||||
? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024))
|
? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
this.activeJobs.set(uploadId, { jobId, speedKbs: currentSpeedKbs, bytesUploaded });
|
|
||||||
|
|
||||||
this._emitProgress(uploadId, fileName, task.hoster, {
|
this._emitProgress(uploadId, fileName, task.hoster, {
|
||||||
jobId,
|
jobId,
|
||||||
status: 'uploading',
|
status: 'uploading',
|
||||||
|
|||||||
3
main.js
3
main.js
@ -1,4 +1,5 @@
|
|||||||
const { app, BrowserWindow, ipcMain, dialog, clipboard, powerSaveBlocker } = require('electron');
|
const { app, BrowserWindow, ipcMain, dialog, clipboard, powerSaveBlocker, nativeTheme } = require('electron');
|
||||||
|
nativeTheme.themeSource = 'dark';
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const ConfigStore = require('./lib/config-store');
|
const ConfigStore = require('./lib/config-store');
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "1.8.4",
|
"version": "1.8.5",
|
||||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
183
renderer/app.js
183
renderer/app.js
@ -363,7 +363,7 @@ function persistQueueStateSoon(immediate) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Use longer debounce during uploads to reduce disk I/O
|
// Use longer debounce during uploads to reduce disk I/O
|
||||||
const delay = uploading ? 3000 : 500;
|
const delay = uploading ? 10000 : 500;
|
||||||
queuePersistTimer = setTimeout(() => {
|
queuePersistTimer = setTimeout(() => {
|
||||||
persistQueueStateNow().catch(() => {});
|
persistQueueStateNow().catch(() => {});
|
||||||
}, delay);
|
}, delay);
|
||||||
@ -570,12 +570,27 @@ const VIRTUAL_OVERSCAN = 10;
|
|||||||
let _lastVisibleRange = { start: -1, end: -1 };
|
let _lastVisibleRange = { start: -1, end: -1 };
|
||||||
let _queueListenersBound = false;
|
let _queueListenersBound = false;
|
||||||
|
|
||||||
|
// Throttled UI update scheduling – max one render per 200ms during uploads
|
||||||
|
let _uiUpdateTimer = null;
|
||||||
|
const UI_UPDATE_INTERVAL = 200; // ms
|
||||||
|
|
||||||
function scheduleQueueRender() {
|
function scheduleQueueRender() {
|
||||||
if (_renderQueued) return;
|
if (_renderQueued) return;
|
||||||
_renderQueued = true;
|
_renderQueued = true;
|
||||||
requestAnimationFrame(() => { _renderQueued = false; renderQueueTable(); });
|
requestAnimationFrame(() => { _renderQueued = false; renderQueueTable(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scheduleThrottledUIUpdate() {
|
||||||
|
if (_uiUpdateTimer) return;
|
||||||
|
_uiUpdateTimer = setTimeout(() => {
|
||||||
|
_uiUpdateTimer = null;
|
||||||
|
scheduleQueueRender();
|
||||||
|
updateQueueActionButtons();
|
||||||
|
updateStatusBar();
|
||||||
|
updateStatsPanel();
|
||||||
|
}, UI_UPDATE_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
function buildRowHtml(job) {
|
function buildRowHtml(job) {
|
||||||
const statusClass = `status-${job.status}`;
|
const statusClass = `status-${job.status}`;
|
||||||
const rowClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`;
|
const rowClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`;
|
||||||
@ -608,6 +623,42 @@ function buildRowHtml(job) {
|
|||||||
</tr>`;
|
</tr>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In-place update of a single row's cells (avoids full innerHTML rebuild)
|
||||||
|
function _updateRowInPlace(tr, job) {
|
||||||
|
const statusClass = `status-${job.status}`;
|
||||||
|
const uploadedSize = job.status === 'preview'
|
||||||
|
? (job.bytesTotal > 0 ? formatSize(job.bytesTotal) : '...')
|
||||||
|
: `${formatSize(job.bytesUploaded)} / ${formatSize(job.bytesTotal)}`;
|
||||||
|
const statusText = getStatusText(job);
|
||||||
|
const elapsed = formatTime(job.elapsed);
|
||||||
|
const remaining = formatTime(job.remaining);
|
||||||
|
const speed = job.speedKbs > 0 ? `${formatSpeed(job.speedKbs)}` : '';
|
||||||
|
const pct = Math.min(100, Math.round((job.progress || 0) * 100));
|
||||||
|
const link = job.result ? (job.result.download_url || job.result.embed_url || '') : '';
|
||||||
|
|
||||||
|
// Update row class
|
||||||
|
tr.className = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`;
|
||||||
|
tr.dataset.link = link;
|
||||||
|
|
||||||
|
const cells = tr.children;
|
||||||
|
if (cells.length < 8) return false; // structure mismatch, needs full rebuild
|
||||||
|
|
||||||
|
cells[1].textContent = uploadedSize;
|
||||||
|
// cells[0] (filename) and cells[2] (hoster) don't change during upload
|
||||||
|
const badge = cells[3].querySelector('.status-badge');
|
||||||
|
if (badge) { badge.className = `status-badge ${statusClass}`; badge.textContent = statusText; }
|
||||||
|
cells[4].textContent = elapsed;
|
||||||
|
cells[5].textContent = remaining;
|
||||||
|
cells[6].textContent = speed;
|
||||||
|
|
||||||
|
const fill = cells[7].querySelector('.progress-bar-fill');
|
||||||
|
if (fill) { fill.style.width = pct + '%'; fill.className = `progress-bar-fill ${statusClass}`; }
|
||||||
|
const pctSpan = cells[7].querySelector('.progress-pct');
|
||||||
|
if (pctSpan) pctSpan.textContent = job.status === 'preview' ? '' : pct + '%';
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function renderQueueTable() {
|
function renderQueueTable() {
|
||||||
const tbody = document.getElementById('queueBody');
|
const tbody = document.getElementById('queueBody');
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
@ -615,10 +666,27 @@ function renderQueueTable() {
|
|||||||
_sortedJobsCache = sortQueueJobs(queueJobs);
|
_sortedJobsCache = sortQueueJobs(queueJobs);
|
||||||
const totalRows = _sortedJobsCache.length;
|
const totalRows = _sortedJobsCache.length;
|
||||||
|
|
||||||
// For small queues (<200 rows), use simple rendering
|
|
||||||
if (totalRows < 200) {
|
if (totalRows < 200) {
|
||||||
|
// Try in-place update if row count matches (fast path)
|
||||||
|
const existingRows = tbody.querySelectorAll('.queue-row');
|
||||||
|
if (existingRows.length === totalRows && totalRows > 0) {
|
||||||
|
// In-place update – no DOM destruction
|
||||||
|
for (let i = 0; i < totalRows; i++) {
|
||||||
|
const tr = existingRows[i];
|
||||||
|
const job = _sortedJobsCache[i];
|
||||||
|
// If row identity changed (different job), fall back to full rebuild
|
||||||
|
if (tr.dataset.jobId !== job.id) {
|
||||||
tbody.innerHTML = _sortedJobsCache.map(buildRowHtml).join('');
|
tbody.innerHTML = _sortedJobsCache.map(buildRowHtml).join('');
|
||||||
_lastVisibleRange = { start: -1, end: -1 };
|
_lastVisibleRange = { start: -1, end: -1 };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_updateRowInPlace(tr, job);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Full rebuild needed (row count changed)
|
||||||
|
tbody.innerHTML = _sortedJobsCache.map(buildRowHtml).join('');
|
||||||
|
_lastVisibleRange = { start: -1, end: -1 };
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Virtual scrolling for large queues — force re-render
|
// Virtual scrolling for large queues — force re-render
|
||||||
_lastVisibleRange = { start: -1, end: -1 };
|
_lastVisibleRange = { start: -1, end: -1 };
|
||||||
@ -1185,10 +1253,15 @@ function handleProgress(data) {
|
|||||||
queueJobs = queueJobs.filter(j => j !== job);
|
queueJobs = queueJobs.filter(j => j !== job);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status changes (done/error/etc) get immediate render; ongoing progress is throttled
|
||||||
|
if (data.status === 'uploading') {
|
||||||
|
scheduleThrottledUIUpdate();
|
||||||
|
} else {
|
||||||
scheduleQueueRender();
|
scheduleQueueRender();
|
||||||
updateQueueActionButtons();
|
updateQueueActionButtons();
|
||||||
updateStatusBar();
|
updateStatusBar();
|
||||||
updateStatsPanel();
|
updateStatsPanel();
|
||||||
|
}
|
||||||
persistQueueStateSoon();
|
persistQueueStateSoon();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1442,19 +1515,45 @@ function applySummaryResults(summary) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatusBar() {
|
// Single-pass queue stats computation (shared by status bar + stats panel)
|
||||||
const counts = {
|
function _computeQueueStats() {
|
||||||
total: queueJobs.length,
|
let remaining = 0, inProgress = 0, done = 0, errors = 0;
|
||||||
remaining: queueJobs.filter((job) => ['preview', 'queued', 'getting-server', 'uploading', 'retrying'].includes(job.status)).length,
|
let bytesRemaining = 0, totalSize = 0, remainingSize = 0;
|
||||||
inProgress: queueJobs.filter((job) => ['getting-server', 'uploading', 'retrying'].includes(job.status)).length,
|
const total = queueJobs.length;
|
||||||
error: queueJobs.filter((job) => job.status === 'error').length
|
|
||||||
};
|
for (let i = 0; i < total; i++) {
|
||||||
|
const job = queueJobs[i];
|
||||||
|
const s = job.status;
|
||||||
|
const bt = job.bytesTotal || 0;
|
||||||
|
const bu = job.bytesUploaded || 0;
|
||||||
|
totalSize += bt;
|
||||||
|
|
||||||
|
if (s === 'uploading' || s === 'getting-server' || s === 'retrying') {
|
||||||
|
inProgress++;
|
||||||
|
remaining++;
|
||||||
|
bytesRemaining += Math.max(0, bt - bu);
|
||||||
|
remainingSize += Math.max(0, bt - bu);
|
||||||
|
} else if (s === 'preview' || s === 'queued') {
|
||||||
|
remaining++;
|
||||||
|
bytesRemaining += Math.max(0, bt - bu);
|
||||||
|
remainingSize += Math.max(0, bt - bu);
|
||||||
|
} else if (s === 'done') {
|
||||||
|
done++;
|
||||||
|
} else if (s === 'error') {
|
||||||
|
errors++;
|
||||||
|
} else if (s !== 'skipped') {
|
||||||
|
remainingSize += Math.max(0, bt - bu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { total, remaining, inProgress, done, errors, bytesRemaining, totalSize, remainingSize };
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatusBar() {
|
||||||
|
const stats = _computeQueueStats();
|
||||||
|
|
||||||
const bytesRemaining = queueJobs
|
|
||||||
.filter((job) => ['getting-server', 'uploading', 'retrying', 'queued', 'preview'].includes(job.status))
|
|
||||||
.reduce((sum, job) => sum + Math.max(0, (job.bytesTotal || 0) - (job.bytesUploaded || 0)), 0);
|
|
||||||
const etaSeconds = lastUploadStats.globalSpeedKbs > 0
|
const etaSeconds = lastUploadStats.globalSpeedKbs > 0
|
||||||
? Math.round(bytesRemaining / (lastUploadStats.globalSpeedKbs * 1024))
|
? Math.round(stats.bytesRemaining / (lastUploadStats.globalSpeedKbs * 1024))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const stateText = lastUploadStats.state === 'uploading'
|
const stateText = lastUploadStats.state === 'uploading'
|
||||||
@ -1467,14 +1566,13 @@ function updateStatusBar() {
|
|||||||
|
|
||||||
document.getElementById('sbState').textContent = stateText;
|
document.getElementById('sbState').textContent = stateText;
|
||||||
document.getElementById('sbSpeed').textContent = formatSpeed(lastUploadStats.globalSpeedKbs || 0);
|
document.getElementById('sbSpeed').textContent = formatSpeed(lastUploadStats.globalSpeedKbs || 0);
|
||||||
const queueTotalBytes = queueJobs.reduce((sum, j) => sum + (j.bytesTotal || 0), 0);
|
document.getElementById('sbTotal').textContent = `${formatSize(lastUploadStats.totalBytes || 0)} / ${formatSize(stats.totalSize)}`;
|
||||||
document.getElementById('sbTotal').textContent = `${formatSize(lastUploadStats.totalBytes || 0)} / ${formatSize(queueTotalBytes)}`;
|
|
||||||
document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`;
|
document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`;
|
||||||
document.getElementById('sbConnections').textContent = `Aktive Verbindungen ${lastUploadStats.activeJobs || 0}`;
|
document.getElementById('sbConnections').textContent = `Aktive Verbindungen ${lastUploadStats.activeJobs || 0}`;
|
||||||
document.getElementById('sbQueueCount').textContent = `Gesamt ${counts.total}`;
|
document.getElementById('sbQueueCount').textContent = `Gesamt ${stats.total}`;
|
||||||
document.getElementById('sbRemainingCount').textContent = `Remaining ${counts.remaining}`;
|
document.getElementById('sbRemainingCount').textContent = `Remaining ${stats.remaining}`;
|
||||||
document.getElementById('sbInProgressCount').textContent = `In Progress ${counts.inProgress}`;
|
document.getElementById('sbInProgressCount').textContent = `In Progress ${stats.inProgress}`;
|
||||||
document.getElementById('sbErrorCount').textContent = `Error ${counts.error}`;
|
document.getElementById('sbErrorCount').textContent = `Error ${stats.errors}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Health Check ---
|
// --- Health Check ---
|
||||||
@ -2102,6 +2200,8 @@ function updateRecentSortHeaders() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _recentListenersBound = false;
|
||||||
|
|
||||||
function renderRecentUploadsPanel() {
|
function renderRecentUploadsPanel() {
|
||||||
const tbody = document.getElementById('recentFilesBody');
|
const tbody = document.getElementById('recentFilesBody');
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
@ -2121,14 +2221,18 @@ function renderRecentUploadsPanel() {
|
|||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
tbody.querySelectorAll('.recent-file-row').forEach(tr => {
|
// Event delegation – bind once, not per-row
|
||||||
tr.addEventListener('click', (e) => {
|
if (!_recentListenersBound) {
|
||||||
|
_recentListenersBound = true;
|
||||||
|
tbody.addEventListener('click', (e) => {
|
||||||
|
const tr = e.target.closest('.recent-file-row');
|
||||||
|
if (!tr) return;
|
||||||
const id = parseInt(tr.dataset.order, 10);
|
const id = parseInt(tr.dataset.order, 10);
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
if (selectedRecentIds.has(id)) selectedRecentIds.delete(id);
|
if (selectedRecentIds.has(id)) selectedRecentIds.delete(id);
|
||||||
else selectedRecentIds.add(id);
|
else selectedRecentIds.add(id);
|
||||||
} else if (e.shiftKey && selectedRecentIds.size > 0) {
|
} else if (e.shiftKey && selectedRecentIds.size > 0) {
|
||||||
const sortedOrders = rows.map(r => r.order);
|
const sortedOrders = sortRecentFiles(sessionFilesData).map(r => r.order);
|
||||||
const lastIdx = sortedOrders.findIndex(o => selectedRecentIds.has(o));
|
const lastIdx = sortedOrders.findIndex(o => selectedRecentIds.has(o));
|
||||||
const curIdx = sortedOrders.indexOf(id);
|
const curIdx = sortedOrders.indexOf(id);
|
||||||
if (lastIdx >= 0 && curIdx >= 0) {
|
if (lastIdx >= 0 && curIdx >= 0) {
|
||||||
@ -2143,12 +2247,13 @@ function renderRecentUploadsPanel() {
|
|||||||
renderRecentUploadsPanel();
|
renderRecentUploadsPanel();
|
||||||
});
|
});
|
||||||
|
|
||||||
tr.addEventListener('dblclick', () => {
|
tbody.addEventListener('dblclick', (e) => {
|
||||||
if (tr.classList.contains('error')) return;
|
const tr = e.target.closest('.recent-file-row');
|
||||||
|
if (!tr || tr.classList.contains('error')) return;
|
||||||
const link = tr.dataset.link;
|
const link = tr.dataset.link;
|
||||||
if (link) { window.api.copyToClipboard(link); showCopyToast('Link kopiert'); }
|
if (link) { window.api.copyToClipboard(link); showCopyToast('Link kopiert'); }
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
updateRecentSortHeaders();
|
updateRecentSortHeaders();
|
||||||
}
|
}
|
||||||
@ -2222,7 +2327,6 @@ window.addEventListener('beforeunload', () => {
|
|||||||
// --- Setup Listeners ---
|
// --- Setup Listeners ---
|
||||||
function setupListeners() {
|
function setupListeners() {
|
||||||
document.getElementById('addFilesBtn').addEventListener('click', pickFiles);
|
document.getElementById('addFilesBtn').addEventListener('click', pickFiles);
|
||||||
document.getElementById('chooseHostersBtn').addEventListener('click', openHosterModal);
|
|
||||||
document.getElementById('startUploadBtn').addEventListener('click', startUpload);
|
document.getElementById('startUploadBtn').addEventListener('click', startUpload);
|
||||||
document.getElementById('startSelectedBtn').addEventListener('click', startSelectedUpload);
|
document.getElementById('startSelectedBtn').addEventListener('click', startSelectedUpload);
|
||||||
|
|
||||||
@ -2598,30 +2702,23 @@ function formatDuration(seconds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateStatsPanel() {
|
function updateStatsPanel() {
|
||||||
const total = queueJobs.length;
|
const stats = _computeQueueStats();
|
||||||
const done = queueJobs.filter(j => j.status === 'done').length;
|
const remaining = stats.total - stats.done - stats.errors;
|
||||||
const inProgress = queueJobs.filter(j => ['uploading', 'getting-server', 'retrying'].includes(j.status)).length;
|
|
||||||
const errors = queueJobs.filter(j => j.status === 'error').length;
|
|
||||||
const remaining = total - done - errors;
|
|
||||||
|
|
||||||
const totalSize = queueJobs.reduce((s, j) => s + (j.bytesTotal || 0), 0);
|
|
||||||
const remainingSize = queueJobs.filter(j => !['done', 'error', 'skipped'].includes(j.status))
|
|
||||||
.reduce((s, j) => s + ((j.bytesTotal || 0) - (j.bytesUploaded || 0)), 0);
|
|
||||||
|
|
||||||
const el = (id) => document.getElementById(id);
|
const el = (id) => document.getElementById(id);
|
||||||
if (el('statQueueTotal')) el('statQueueTotal').textContent = total;
|
if (el('statQueueTotal')) el('statQueueTotal').textContent = stats.total;
|
||||||
if (el('statQueueDone')) el('statQueueDone').textContent = done;
|
if (el('statQueueDone')) el('statQueueDone').textContent = stats.done;
|
||||||
if (el('statQueueRemaining')) el('statQueueRemaining').textContent = remaining;
|
if (el('statQueueRemaining')) el('statQueueRemaining').textContent = remaining;
|
||||||
if (el('statQueueInProgress')) el('statQueueInProgress').textContent = inProgress;
|
if (el('statQueueInProgress')) el('statQueueInProgress').textContent = stats.inProgress;
|
||||||
if (el('statQueueError')) el('statQueueError').textContent = errors;
|
if (el('statQueueError')) el('statQueueError').textContent = stats.errors;
|
||||||
if (el('statSizeTotal')) el('statSizeTotal').textContent = formatBytes(totalSize);
|
if (el('statSizeTotal')) el('statSizeTotal').textContent = formatBytes(stats.totalSize);
|
||||||
if (el('statSizeRemaining')) el('statSizeRemaining').textContent = formatBytes(remainingSize);
|
if (el('statSizeRemaining')) el('statSizeRemaining').textContent = formatBytes(stats.remainingSize);
|
||||||
|
|
||||||
const speed = lastUploadStats.globalSpeedKbs || 0;
|
const speed = lastUploadStats.globalSpeedKbs || 0;
|
||||||
if (el('statSpeed')) el('statSpeed').textContent = speed > 0 ? formatBytes(speed * 1024) + '/s' : '0 B/s';
|
if (el('statSpeed')) el('statSpeed').textContent = speed > 0 ? formatBytes(speed * 1024) + '/s' : '0 B/s';
|
||||||
if (el('statEta')) {
|
if (el('statEta')) {
|
||||||
if (speed > 0 && remainingSize > 0) {
|
if (speed > 0 && stats.remainingSize > 0) {
|
||||||
el('statEta').textContent = formatDuration(Math.round(remainingSize / (speed * 1024)));
|
el('statEta').textContent = formatDuration(Math.round(stats.remainingSize / (speed * 1024)));
|
||||||
} else {
|
} else {
|
||||||
el('statEta').textContent = '--:--';
|
el('statEta').textContent = '--:--';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,6 @@
|
|||||||
<div id="upload-view" class="view active">
|
<div id="upload-view" class="view active">
|
||||||
<div class="upload-toolbar">
|
<div class="upload-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<button class="btn btn-xs btn-secondary" id="chooseHostersBtn">Ziele auswählen</button>
|
|
||||||
<span class="hoster-summary" id="hosterSummary" style="display:none"></span>
|
<span class="hoster-summary" id="hosterSummary" style="display:none"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
|
|||||||
@ -329,8 +329,8 @@ body {
|
|||||||
}
|
}
|
||||||
.progress-bar-fill {
|
.progress-bar-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transition: width 0.3s ease;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
will-change: width;
|
||||||
}
|
}
|
||||||
.progress-bar-fill.status-uploading { background: linear-gradient(90deg, #4a90d9, #5dabf7); }
|
.progress-bar-fill.status-uploading { background: linear-gradient(90deg, #4a90d9, #5dabf7); }
|
||||||
.progress-bar-fill.status-getting-server { background: var(--accent); }
|
.progress-bar-fill.status-getting-server { background: var(--accent); }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user