diff --git a/electron-config.json b/electron-config.json index d20e210..50421d1 100644 --- a/electron-config.json +++ b/electron-config.json @@ -59,10 +59,12 @@ "alwaysOnTop": false, "shutdownAfterFinish": "nothing", "logFilePath": "", + "sessionLog": false, "resumeQueueOnLaunch": true, "parallelUploadCount": 0, "scaleParallelUploads": true, "removeFromQueueOnDone": false, + "globalMaxSpeedKbs": 0, "pendingQueue": { "selectedUploadHosters": [ "doodstream.com", @@ -79,7 +81,7 @@ ], "queueJobs": [ { - "id": "preview-1773193741983-c1qa7p", + "id": "preview-1773271047205-k8l83r", "file": "C:\\Users\\ploet\\Downloads\\Einfach mal die Fresse halten!!!.mp4", "fileName": "Einfach mal die Fresse halten!!!.mp4", "hoster": "doodstream.com", @@ -90,7 +92,7 @@ "maxAttempts": 0 }, { - "id": "preview-1773193741983-bvlsvn", + "id": "preview-1773271047206-npnpph", "file": "C:\\Users\\ploet\\Downloads\\Einfach mal die Fresse halten!!!.mp4", "fileName": "Einfach mal die Fresse halten!!!.mp4", "hoster": "voe.sx", @@ -101,7 +103,7 @@ "maxAttempts": 0 }, { - "id": "preview-1773193741983-a7aixs", + "id": "preview-1773271047206-q2skl1", "file": "C:\\Users\\ploet\\Downloads\\Einfach mal die Fresse halten!!!.mp4", "fileName": "Einfach mal die Fresse halten!!!.mp4", "hoster": "vidmoly.me", @@ -112,7 +114,7 @@ "maxAttempts": 0 }, { - "id": "preview-1773193741983-39jnfg", + "id": "preview-1773271047206-cek27b", "file": "C:\\Users\\ploet\\Downloads\\Einfach mal die Fresse halten!!!.mp4", "fileName": "Einfach mal die Fresse halten!!!.mp4", "hoster": "byse.sx", diff --git a/lib/upload-manager.js b/lib/upload-manager.js index 203d5ab..8bde882 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -324,6 +324,9 @@ class UploadManager extends EventEmitter { 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 now = Date.now(); const elapsed = Math.round((now - jobStart) / 1000); @@ -335,12 +338,16 @@ class UploadManager extends EventEmitter { 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 ? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024)) : 0; - this.activeJobs.set(uploadId, { jobId, speedKbs: currentSpeedKbs, bytesUploaded }); - this._emitProgress(uploadId, fileName, task.hoster, { jobId, status: 'uploading', diff --git a/main.js b/main.js index c1f5349..c987420 100644 --- a/main.js +++ b/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 fs = require('fs'); const ConfigStore = require('./lib/config-store'); diff --git a/package.json b/package.json index 9884c88..90b177e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multi-hoster-uploader", - "version": "1.8.4", + "version": "1.8.5", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "main": "main.js", "scripts": { diff --git a/renderer/app.js b/renderer/app.js index b6a3f0e..c837473 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -363,7 +363,7 @@ function persistQueueStateSoon(immediate) { return; } // Use longer debounce during uploads to reduce disk I/O - const delay = uploading ? 3000 : 500; + const delay = uploading ? 10000 : 500; queuePersistTimer = setTimeout(() => { persistQueueStateNow().catch(() => {}); }, delay); @@ -570,12 +570,27 @@ const VIRTUAL_OVERSCAN = 10; let _lastVisibleRange = { start: -1, end: -1 }; 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() { if (_renderQueued) return; _renderQueued = true; requestAnimationFrame(() => { _renderQueued = false; renderQueueTable(); }); } +function scheduleThrottledUIUpdate() { + if (_uiUpdateTimer) return; + _uiUpdateTimer = setTimeout(() => { + _uiUpdateTimer = null; + scheduleQueueRender(); + updateQueueActionButtons(); + updateStatusBar(); + updateStatsPanel(); + }, UI_UPDATE_INTERVAL); +} + function buildRowHtml(job) { const statusClass = `status-${job.status}`; const rowClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`; @@ -608,6 +623,42 @@ function buildRowHtml(job) { `; } +// 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() { const tbody = document.getElementById('queueBody'); if (!tbody) return; @@ -615,10 +666,27 @@ function renderQueueTable() { _sortedJobsCache = sortQueueJobs(queueJobs); const totalRows = _sortedJobsCache.length; - // For small queues (<200 rows), use simple rendering if (totalRows < 200) { - tbody.innerHTML = _sortedJobsCache.map(buildRowHtml).join(''); - _lastVisibleRange = { start: -1, end: -1 }; + // 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(''); + _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 { // Virtual scrolling for large queues — force re-render _lastVisibleRange = { start: -1, end: -1 }; @@ -1185,10 +1253,15 @@ function handleProgress(data) { queueJobs = queueJobs.filter(j => j !== job); } - scheduleQueueRender(); - updateQueueActionButtons(); - updateStatusBar(); - updateStatsPanel(); + // Status changes (done/error/etc) get immediate render; ongoing progress is throttled + if (data.status === 'uploading') { + scheduleThrottledUIUpdate(); + } else { + scheduleQueueRender(); + updateQueueActionButtons(); + updateStatusBar(); + updateStatsPanel(); + } persistQueueStateSoon(); } @@ -1442,19 +1515,45 @@ function applySummaryResults(summary) { } } -function updateStatusBar() { - const counts = { - total: queueJobs.length, - remaining: queueJobs.filter((job) => ['preview', 'queued', 'getting-server', 'uploading', 'retrying'].includes(job.status)).length, - inProgress: queueJobs.filter((job) => ['getting-server', 'uploading', 'retrying'].includes(job.status)).length, - error: queueJobs.filter((job) => job.status === 'error').length - }; +// 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; + const total = queueJobs.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 - ? Math.round(bytesRemaining / (lastUploadStats.globalSpeedKbs * 1024)) + ? Math.round(stats.bytesRemaining / (lastUploadStats.globalSpeedKbs * 1024)) : 0; const stateText = lastUploadStats.state === 'uploading' @@ -1467,14 +1566,13 @@ function updateStatusBar() { document.getElementById('sbState').textContent = stateText; 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(queueTotalBytes)}`; + document.getElementById('sbTotal').textContent = `${formatSize(lastUploadStats.totalBytes || 0)} / ${formatSize(stats.totalSize)}`; document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`; document.getElementById('sbConnections').textContent = `Aktive Verbindungen ${lastUploadStats.activeJobs || 0}`; - document.getElementById('sbQueueCount').textContent = `Gesamt ${counts.total}`; - document.getElementById('sbRemainingCount').textContent = `Remaining ${counts.remaining}`; - document.getElementById('sbInProgressCount').textContent = `In Progress ${counts.inProgress}`; - document.getElementById('sbErrorCount').textContent = `Error ${counts.error}`; + document.getElementById('sbQueueCount').textContent = `Gesamt ${stats.total}`; + document.getElementById('sbRemainingCount').textContent = `Remaining ${stats.remaining}`; + document.getElementById('sbInProgressCount').textContent = `In Progress ${stats.inProgress}`; + document.getElementById('sbErrorCount').textContent = `Error ${stats.errors}`; } // --- Health Check --- @@ -2102,6 +2200,8 @@ function updateRecentSortHeaders() { }); } +let _recentListenersBound = false; + function renderRecentUploadsPanel() { const tbody = document.getElementById('recentFilesBody'); if (!tbody) return; @@ -2121,14 +2221,18 @@ function renderRecentUploadsPanel() { `).join(''); - tbody.querySelectorAll('.recent-file-row').forEach(tr => { - tr.addEventListener('click', (e) => { + // Event delegation – bind once, not per-row + 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); if (e.ctrlKey || e.metaKey) { if (selectedRecentIds.has(id)) selectedRecentIds.delete(id); else selectedRecentIds.add(id); } 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 curIdx = sortedOrders.indexOf(id); if (lastIdx >= 0 && curIdx >= 0) { @@ -2143,12 +2247,13 @@ function renderRecentUploadsPanel() { renderRecentUploadsPanel(); }); - tr.addEventListener('dblclick', () => { - if (tr.classList.contains('error')) return; + tbody.addEventListener('dblclick', (e) => { + const tr = e.target.closest('.recent-file-row'); + if (!tr || tr.classList.contains('error')) return; const link = tr.dataset.link; if (link) { window.api.copyToClipboard(link); showCopyToast('Link kopiert'); } }); - }); + } updateRecentSortHeaders(); } @@ -2222,7 +2327,6 @@ window.addEventListener('beforeunload', () => { // --- Setup Listeners --- function setupListeners() { document.getElementById('addFilesBtn').addEventListener('click', pickFiles); - document.getElementById('chooseHostersBtn').addEventListener('click', openHosterModal); document.getElementById('startUploadBtn').addEventListener('click', startUpload); document.getElementById('startSelectedBtn').addEventListener('click', startSelectedUpload); @@ -2598,30 +2702,23 @@ function formatDuration(seconds) { } function updateStatsPanel() { - const total = queueJobs.length; - const done = queueJobs.filter(j => j.status === 'done').length; - 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 stats = _computeQueueStats(); + const remaining = stats.total - stats.done - stats.errors; const el = (id) => document.getElementById(id); - if (el('statQueueTotal')) el('statQueueTotal').textContent = total; - if (el('statQueueDone')) el('statQueueDone').textContent = done; + if (el('statQueueTotal')) el('statQueueTotal').textContent = stats.total; + if (el('statQueueDone')) el('statQueueDone').textContent = stats.done; if (el('statQueueRemaining')) el('statQueueRemaining').textContent = remaining; - if (el('statQueueInProgress')) el('statQueueInProgress').textContent = inProgress; - if (el('statQueueError')) el('statQueueError').textContent = errors; - if (el('statSizeTotal')) el('statSizeTotal').textContent = formatBytes(totalSize); - if (el('statSizeRemaining')) el('statSizeRemaining').textContent = formatBytes(remainingSize); + if (el('statQueueInProgress')) el('statQueueInProgress').textContent = stats.inProgress; + if (el('statQueueError')) el('statQueueError').textContent = stats.errors; + if (el('statSizeTotal')) el('statSizeTotal').textContent = formatBytes(stats.totalSize); + if (el('statSizeRemaining')) el('statSizeRemaining').textContent = formatBytes(stats.remainingSize); const speed = lastUploadStats.globalSpeedKbs || 0; if (el('statSpeed')) el('statSpeed').textContent = speed > 0 ? formatBytes(speed * 1024) + '/s' : '0 B/s'; if (el('statEta')) { - if (speed > 0 && remainingSize > 0) { - el('statEta').textContent = formatDuration(Math.round(remainingSize / (speed * 1024))); + if (speed > 0 && stats.remainingSize > 0) { + el('statEta').textContent = formatDuration(Math.round(stats.remainingSize / (speed * 1024))); } else { el('statEta').textContent = '--:--'; } diff --git a/renderer/index.html b/renderer/index.html index 9f0e83d..0162dfe 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -24,7 +24,6 @@
-
diff --git a/renderer/styles.css b/renderer/styles.css index a23a5fd..269acd2 100644 --- a/renderer/styles.css +++ b/renderer/styles.css @@ -329,8 +329,8 @@ body { } .progress-bar-fill { height: 100%; - transition: width 0.3s ease; border-radius: 2px; + will-change: width; } .progress-bar-fill.status-uploading { background: linear-gradient(90deg, #4a90d9, #5dabf7); } .progress-bar-fill.status-getting-server { background: var(--accent); }