diff --git a/lib/upload-manager.js b/lib/upload-manager.js index ee2b367..158f23b 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -430,6 +430,7 @@ class UploadManager extends EventEmitter { succeeded, failed: total - succeeded - skipped, skipped, + durationSec: Math.max(0, Math.round((Date.now() - this.startTime) / 1000)), files }; diff --git a/main.js b/main.js index f168069..3377674 100644 --- a/main.js +++ b/main.js @@ -2082,6 +2082,7 @@ ipcMain.handle('start-upload', async (_event, payload) => { succeeded: 0, failed: 0, skipped: 0, + durationSec: 0, files: [] }, skippedJobs); try { await configStore.appendHistory(skippedSummary); } catch (error) { @@ -2347,6 +2348,8 @@ ipcMain.handle('start-upload', async (_event, payload) => { total: tasks.length, succeeded: 0, failed: tasks.length, + skipped: 0, + durationSec: 0, files: [], error: err ? err.message : 'Unbekannter Fehler' }; diff --git a/renderer/app.js b/renderer/app.js index 5449469..5870740 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -368,6 +368,7 @@ let _updateDownloadCancelable = false; let _updateCancelBusy = false; let _updateDialogReturnFocus = null; let _updateDialogInertState = []; +let _batchCompletionTimer = null; let _startupAutoResumeController = null; let _startupAutoResumeCanceled = false; @@ -3405,9 +3406,62 @@ function handleBatchDone(summary) { lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: lastUploadStats.totalBytes, elapsed: lastUploadStats.elapsed, activeJobs: 0 }; updateStatusBar(); _refreshSessionFailedSnapshot(); + showBatchCompletionSummary(summary); _scheduleAutoRetryIfNeeded(); } +function _formatBatchCompletionDuration(seconds, english) { + const total = Math.max(0, Math.round(Number(seconds) || 0)); + if (total < 60) return english ? `${total} sec` : `${total} Sek.`; + const minutes = Math.round(total / 60); + if (minutes < 60) return english ? `${minutes} min` : `${minutes} Min.`; + const hours = Math.floor(minutes / 60); + const remainder = minutes % 60; + return english ? `${hours} hr ${remainder} min` : `${hours} Std. ${remainder} Min.`; +} + +function hideBatchCompletionSummary() { + const toast = document.getElementById('batchCompletionToast'); + if (!toast) return; + if (_batchCompletionTimer) clearTimeout(_batchCompletionTimer); + _batchCompletionTimer = null; + toast.classList.remove('show'); + setTimeout(() => { + if (!toast.classList.contains('show')) toast.hidden = true; + }, 240); +} + +function showBatchCompletionSummary(summary) { + const toast = document.getElementById('batchCompletionToast'); + const text = document.getElementById('batchCompletionText'); + const showErrors = document.getElementById('batchCompletionShowErrors'); + if (!toast || !text || !showErrors || !summary) return; + const total = Math.max(0, Number(summary.total) || 0); + if (total <= 0) return; + const succeeded = Math.max(0, Number(summary.succeeded) || 0); + const failed = Math.max(0, Number(summary.failed) || 0); + const skipped = Math.max(0, Number(summary.skipped) || 0); + const english = getUiLocale() === 'en-US'; + const format = value => value.toLocaleString(getUiLocale()); + const duration = _formatBatchCompletionDuration(summary.durationSec, english); + text.textContent = english + ? `${format(succeeded)} successful · ${format(failed)} failed · ${format(skipped)} skipped · ${duration}` + : `${format(succeeded)} erfolgreich · ${format(failed)} fehlgeschlagen · ${format(skipped)} übersprungen · ${duration}`; + const canShowErrors = failed > 0 && queueJobs.some(job => job.status === 'error'); + showErrors.hidden = !canShowErrors; + toast.classList.toggle('has-failures', failed > 0); + toast.hidden = false; + requestAnimationFrame(() => toast.classList.add('show')); + if (_batchCompletionTimer) clearTimeout(_batchCompletionTimer); + _batchCompletionTimer = setTimeout(hideBatchCompletionSummary, 15000); +} + +function showBatchCompletionErrors() { + document.getElementById('upload-tab')?.click(); + setUploadSidebarFilter('error'); + hideBatchCompletionSummary(); +} + let _sessionFailedKeys = new Set(); const _autoRetryState = { round: 0, timer: null }; @@ -7285,6 +7339,8 @@ function setupListeners() { document.getElementById('queueStatusFilter').addEventListener('change', applyQueueDetailFilters); document.getElementById('queueFilterResetBtn').addEventListener('click', resetQueueFilters); syncQueueFilterResetAction(); + document.getElementById('batchCompletionShowErrors').addEventListener('click', showBatchCompletionErrors); + document.getElementById('batchCompletionClose').addEventListener('click', hideBatchCompletionSummary); const historyRetentionPicker = document.getElementById('historyRetentionPicker'); const historyRetentionTrigger = document.getElementById('historyRetentionTrigger'); diff --git a/renderer/i18n.js b/renderer/i18n.js index d1d4473..e859eb7 100644 --- a/renderer/i18n.js +++ b/renderer/i18n.js @@ -109,6 +109,7 @@ ['Abbrechen', 'Cancel'], ['Filter zurücksetzen', 'Reset filters'], ['Filter', 'Filters'], + ['Fehler anzeigen', 'Show failures'], ['Jetzt installieren', 'Install now'], ['Jetzt updaten', 'Install now'], ['Alle Dateien', 'All files'], diff --git a/renderer/index.html b/renderer/index.html index 7b5f5d7..1251271 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -656,6 +656,11 @@
+