diff --git a/README.md b/README.md index 74f6485..dc1e5a6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Multi-Hoster-Upload is a Windows desktop application for sending file batches to Download the current Setup or Portable build from the [latest GitHub release](https://github.com/Sucukdeluxe/Multi-Hoster-Upload/releases/latest). -The latest public release is version 2.1.9. Use the release page for the executables and the full English changelog. +The latest public release is version 2.1.10. Use the release page for the executables and the full English changelog. ## Features diff --git a/package-lock.json b/package-lock.json index a942312..78472eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "multi-hoster-uploader", - "version": "2.1.9", + "version": "2.1.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "multi-hoster-uploader", - "version": "2.1.9", + "version": "2.1.10", "dependencies": { "chokidar": "^3.6.0", "undici": "^7.29.0", diff --git a/package.json b/package.json index 9a96ceb..f8be023 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multi-hoster-uploader", - "version": "2.1.9", + "version": "2.1.10", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "main": "main.js", "scripts": { diff --git a/renderer/app.js b/renderer/app.js index 7503922..9a10d33 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -1563,40 +1563,37 @@ function suppressPreviewKeysStillSelected(keys) { // Build preview jobs from selected files x selected hosters (before upload starts) function buildQueuePreview() { const hosters = getSelectedHosters(); - if (hosters.length === 0) { - queueJobs = queueJobs.filter(j => j.status !== 'preview'); - rebuildJobIndex(); - renderQueueTable(); - persistQueueStateSoon(); - return; - } - // Remove old preview jobs queueJobs = queueJobs.filter(j => j.status !== 'preview'); - // Build a Set for fast existence checks - const existingKeys = new Set(); - for (const j of queueJobs) { - existingKeys.add(`${j.file}|${j.hoster}`); - } + if (hosters.length > 0) { + const existingKeys = new Set(); + for (const j of queueJobs) { + existingKeys.add(`${j.file}|${j.hoster}`); + } - for (const file of selectedFiles) { - for (const hoster of hosters) { - const key = `${file.path}|${hoster}`; - if (!existingKeys.has(key) && !_completedUploadKeys.has(key) && !_suppressedPreviewKeys.has(key)) { - const job = { - id: `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - file: file.path, fileName: file.name, hoster, - status: 'preview', bytesUploaded: 0, bytesTotal: file.size || 0, - speedKbs: 0, elapsed: 0, remaining: 0, - error: null, result: null, attempt: 0, maxAttempts: 0, link: '' - }; - queueJobs.push(job); - existingKeys.add(key); + for (const file of selectedFiles) { + for (const hoster of hosters) { + const key = `${file.path}|${hoster}`; + if (!existingKeys.has(key) && !_completedUploadKeys.has(key) && !_suppressedPreviewKeys.has(key)) { + const job = { + id: `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + file: file.path, fileName: file.name, hoster, + status: 'preview', bytesUploaded: 0, bytesTotal: file.size || 0, + speedKbs: 0, elapsed: 0, remaining: 0, + error: null, result: null, attempt: 0, maxAttempts: 0, link: '' + }; + queueJobs.push(job); + existingKeys.add(key); + } } } } + rebuildJobIndex(); + _queueStatsCache = null; renderQueueTable(); + updateStatusBar(); + updateStatsPanel(); persistQueueStateSoon(); } diff --git a/renderer/styles.css b/renderer/styles.css index 643498a..5b18ba2 100644 --- a/renderer/styles.css +++ b/renderer/styles.css @@ -2518,13 +2518,18 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; } } .view-sidebar-badge { + height: 22px; min-width: 22px; - padding: 2px 6px; + display: inline-grid; + place-items: center; + padding: 0 7px; border-radius: 999px; - background: rgba(255, 255, 255, .055); - color: var(--text-dim); - font-size: 10px; + background: var(--accent); + color: var(--accent-ink); + font-size: 12px; + font-weight: 700; font-variant-numeric: tabular-nums; + line-height: 1; text-align: center; } @@ -3768,11 +3773,14 @@ input[type="checkbox"] { display: grid; } - #upload-view .view-sidebar-copy, - #upload-view .view-sidebar-badge { + #upload-view .view-sidebar-copy { display: block; } + #upload-view .view-sidebar-badge { + display: grid; + } + #upload-view .upload-sidebar-lower { min-width: 0; gap: 10px; diff --git a/tests/ui-smoke.js b/tests/ui-smoke.js index 445cb88..c724c56 100644 --- a/tests/ui-smoke.js +++ b/tests/ui-smoke.js @@ -313,7 +313,7 @@ setTimeout(async () => { await wc.debugger.sendCommand('Input.dispatchDragEvent', { type: 'drop', ...populatedDropPoint, data: dragData }); await waitUntil(() => wc.executeJavaScript('document.getElementById("hosterModal")?.style.display === "flex"')); populatedDropState = await wc.executeJavaScript('(() => ({ modal: document.getElementById("hosterModal")?.style.display, paths: _pendingFiles.map(file => file.path) }))()'); - await wc.executeJavaScript('cancelHosterModal(); selectedFiles = []; queueJobs = []; rebuildJobIndex(); updateUploadView(); renderQueueTable();'); + await wc.executeJavaScript('cancelHosterModal(); selectedFiles = []; queueJobs = []; rebuildJobIndex(); _queueStatsCache = null; updateUploadView(); renderQueueTable(); updateStatusBar();'); } finally { if (wc.debugger.isAttached()) wc.debugger.detach(); try { fs.unlinkSync(populatedDropFixture); } catch {} @@ -337,7 +337,7 @@ setTimeout(async () => { await wc.debugger.sendCommand('Input.dispatchDragEvent', { type: 'drop', ...duplicateDropPoint, data: dragData }); await new Promise(resolve => setTimeout(resolve, 100)); duplicateDropState = await wc.executeJavaScript('(() => ({ modal: document.getElementById("hosterModal")?.style.display, pending: _pendingFiles.length, toast: document.getElementById("copyToast")?.textContent, shown: document.getElementById("copyToast")?.classList.contains("show") }))()'); - await wc.executeJavaScript('selectedFiles = []; queueJobs = []; rebuildJobIndex(); updateUploadView(); renderQueueTable();'); + await wc.executeJavaScript('selectedFiles = []; queueJobs = []; rebuildJobIndex(); _queueStatsCache = null; updateUploadView(); renderQueueTable(); updateStatusBar();'); } finally { if (wc.debugger.isAttached()) wc.debugger.detach(); try { fs.unlinkSync(duplicateDropFixture); } catch {} @@ -374,6 +374,39 @@ setTimeout(async () => { const initialTelemetryValues = await wc.executeJavaScript('[...document.querySelectorAll("#uploadTelemetry .upload-telemetry-value")].map(el => el.getAttribute("aria-label") || el.textContent.trim()).join("|")'); check('Upload telemetry starts with stable empty values', initialTelemetryValues === '0|0|0|0|0|0|0 B/s|--:--'); + const previewQueueCounts = await wc.executeJavaScript(\`(() => { + queueJobs = [{ id: 'existing-done', file: 'C:/ui/existing-done.bin', fileName: 'existing-done.bin', hoster: 'doodstream.com', status: 'done', bytesUploaded: 1024, bytesTotal: 1024, speedKbs: 0, elapsed: 1, remaining: 0, progress: 1 }]; + selectedFiles = []; + selectedUploadHosters = ['doodstream.com']; + _sessionDoneCount = 1; + _sessionErrorCount = 0; + _queueStatsCache = null; + updateStatusBar(); + selectedFiles = [1, 2, 3].map(index => ({ path: 'C:/ui/new-' + index + '.bin', name: 'new-' + index + '.bin', size: 2048 })); + buildQueuePreview(); + const result = { + queue: queueJobs.map(job => job.status).join('|'), + sidebar: ['All', 'Active', 'Waiting', 'Done', 'Error'].map(key => document.getElementById('uploadSidebar' + key + 'Count')?.textContent).join('|'), + telemetry: ['Total', 'Remaining', 'Running'].map(key => document.getElementById('uploadTelemetry' + key)?.getAttribute('aria-label')).join('|') + }; + queueJobs = []; + selectedFiles = []; + selectedUploadHosters = []; + _sessionDoneCount = 0; + _queueStatsCache = null; + updateStatusBar(); + return result; + })()\`); + check('Adding preview files immediately refreshes upload counts before the batch starts', previewQueueCounts.queue === 'done|preview|preview|preview' && previewQueueCounts.sidebar === '4|0|3|1|0' && previewQueueCounts.telemetry === '4|3|0'); + + const sidebarBadgeStyle = await wc.executeJavaScript(\`(() => { + const badge = document.getElementById('uploadSidebarAllCount'); + const style = getComputedStyle(badge); + const rect = badge.getBoundingClientRect(); + return { background: style.backgroundColor, color: style.color, fontSize: parseFloat(style.fontSize), fontWeight: parseInt(style.fontWeight, 10), width: rect.width, height: rect.height }; + })()\`); + check('Sidebar count badges use a larger light-blue high-contrast pill', sidebarBadgeStyle.background === 'rgb(186, 208, 252)' && sidebarBadgeStyle.color === 'rgb(16, 23, 35)' && sidebarBadgeStyle.fontSize >= 12 && sidebarBadgeStyle.fontWeight >= 600 && sidebarBadgeStyle.width >= 22 && sidebarBadgeStyle.height >= 22); + const speedSparklineState = await wc.executeJavaScript('(() => { const widget = document.getElementById("uploadSpeedSparkline"); const canvas = document.getElementById("uploadSpeedCanvas"); const rect = canvas?.getBoundingClientRect(); return [Boolean(widget), widget?.classList.contains("is-hidden"), rect?.width > 0, rect?.height > 0, document.getElementById("uploadSpeedValue")?.textContent].join("|"); })()'); check('Upload header exposes the visible speed sparkline', speedSparklineState === 'true|false|true|true|0 B/s');