From cc5ee47fb8f1c4e3a512a90e552785d41546bac3 Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 11 Mar 2026 02:03:10 +0100 Subject: [PATCH] fix: drag&drop many files, layout split, virtual scrolling, keyboard selection - Use webUtils.getPathForFile (Electron 33+) for reliable file paths - Use Set for O(1) dedup on large file drops - Fix flex layout so Files panel stays visible with many queue items - Fix virtual scrolling viewport height and range cache reset - Add Ctrl+A (select all), Delete (remove selected) keyboard shortcuts - Fix Shift+Click range selection to work with virtual scrolling Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- preload.js | 4 ++- renderer/app.js | 61 ++++++++++++++++++++++++++++++++++++--------- renderer/styles.css | 11 ++++---- 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 3c1eca7..423f35c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multi-hoster-uploader", - "version": "1.5.2", + "version": "1.5.3", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "main": "main.js", "scripts": { diff --git a/preload.js b/preload.js index d209018..e7b10de 100644 --- a/preload.js +++ b/preload.js @@ -1,4 +1,4 @@ -const { contextBridge, ipcRenderer } = require('electron'); +const { contextBridge, ipcRenderer, webUtils } = require('electron'); contextBridge.exposeInMainWorld('api', { // Config @@ -65,6 +65,8 @@ contextBridge.exposeInMainWorld('api', { onShutdownCountdown: (callback) => { ipcRenderer.on('shutdown-countdown', (_event, data) => callback(data)); }, + // File path from drag & drop (Electron 33+ compatible) + getPathForFile: (file) => webUtils.getPathForFile(file), removeAllListeners: () => { ipcRenderer.removeAllListeners('upload-progress'); ipcRenderer.removeAllListeners('upload-batch-done'); diff --git a/renderer/app.js b/renderer/app.js index 3f7f7c1..0428c40 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -354,10 +354,19 @@ let _pendingFiles = []; // Files waiting for hoster modal confirmation function addDroppedFiles(fileList) { const files = Array.from(fileList); + const existingPaths = new Set([ + ...selectedFiles.map(f => f.path), + ..._pendingFiles.map(f => f.path) + ]); const newFiles = []; for (const file of files) { - if (!selectedFiles.find(f => f.path === file.path) && !_pendingFiles.find(f => f.path === file.path)) { - newFiles.push({ path: file.path, name: file.name, size: file.size }); + // Use webUtils.getPathForFile (Electron 33+) with fallback to file.path + let filePath = ''; + try { filePath = window.api.getPathForFile(file); } catch { filePath = file.path || ''; } + const fileName = file.name || ''; + if (filePath && !existingPaths.has(filePath)) { + newFiles.push({ path: filePath, name: fileName, size: file.size }); + existingPaths.add(filePath); } } if (newFiles.length > 0) { @@ -527,7 +536,8 @@ function renderQueueTable() { tbody.innerHTML = _sortedJobsCache.map(buildRowHtml).join(''); _lastVisibleRange = { start: -1, end: -1 }; } else { - // Virtual scrolling for large queues + // Virtual scrolling for large queues — force re-render + _lastVisibleRange = { start: -1, end: -1 }; _renderVirtualRows(tbody); } @@ -554,9 +564,9 @@ function _renderVirtualRows(tbody) { if (!scrollContainer) return; const totalRows = _sortedJobsCache.length; - const totalHeight = totalRows * VIRTUAL_ROW_HEIGHT; const scrollTop = scrollContainer.scrollTop; - const viewportHeight = scrollContainer.clientHeight; + // Use a minimum viewport height to avoid rendering nothing when container is being laid out + const viewportHeight = Math.max(scrollContainer.clientHeight, 200); const startIdx = Math.max(0, Math.floor(scrollTop / VIRTUAL_ROW_HEIGHT) - VIRTUAL_OVERSCAN); const endIdx = Math.min(totalRows, Math.ceil((scrollTop + viewportHeight) / VIRTUAL_ROW_HEIGHT) + VIRTUAL_OVERSCAN); @@ -566,7 +576,7 @@ function _renderVirtualRows(tbody) { _lastVisibleRange = { start: startIdx, end: endIdx }; const topPad = startIdx * VIRTUAL_ROW_HEIGHT; - const bottomPad = (totalRows - endIdx) * VIRTUAL_ROW_HEIGHT; + const bottomPad = Math.max(0, (totalRows - endIdx) * VIRTUAL_ROW_HEIGHT); let html = ''; if (topPad > 0) html += ``; @@ -627,12 +637,15 @@ function handleRowClick(e, row) { if (selectedJobIds.has(jobId)) selectedJobIds.delete(jobId); else selectedJobIds.add(jobId); } else if (e.shiftKey && selectedJobIds.size > 0) { - const allRows = Array.from(document.querySelectorAll('.queue-row')); - const lastIdx = allRows.findIndex(r => selectedJobIds.has(r.dataset.jobId)); - const curIdx = allRows.indexOf(row); - const from = Math.min(lastIdx, curIdx); - const to = Math.max(lastIdx, curIdx); - for (let i = from; i <= to; i++) selectedJobIds.add(allRows[i].dataset.jobId); + // Use sorted jobs cache for correct shift-click with virtual scrolling + const sortedIds = _sortedJobsCache.map(j => j.id); + const lastIdx = sortedIds.findIndex(id => selectedJobIds.has(id)); + const curIdx = sortedIds.indexOf(jobId); + if (lastIdx >= 0 && curIdx >= 0) { + const from = Math.min(lastIdx, curIdx); + const to = Math.max(lastIdx, curIdx); + for (let i = from; i <= to; i++) selectedJobIds.add(sortedIds[i]); + } } else { selectedJobIds.clear(); selectedJobIds.add(jobId); @@ -692,6 +705,30 @@ document.addEventListener('keydown', (e) => { hideContextMenu(); cancelHosterModal(); } + // Ctrl+A — select all queue jobs (only when not in an input/textarea) + if ((e.ctrlKey || e.metaKey) && e.key === 'a' && !e.target.closest('input, textarea, select')) { + const activeView = document.querySelector('.view.active'); + if (activeView && activeView.id === 'upload-view' && queueJobs.length > 0) { + e.preventDefault(); + queueJobs.forEach(j => selectedJobIds.add(j.id)); + renderQueueTable(); + } + } + // Delete — remove selected queue jobs + if (e.key === 'Delete' && !e.target.closest('input, textarea, select')) { + const activeView = document.querySelector('.view.active'); + if (activeView && activeView.id === 'upload-view' && selectedJobIds.size > 0 && !uploading) { + e.preventDefault(); + queueJobs = queueJobs.filter(j => { + if (selectedJobIds.has(j.id)) { removeJobFromIndex(j); return false; } + return true; + }); + selectedJobIds.clear(); + renderQueueTable(); + if (queueJobs.length === 0) { selectedFiles = []; updateUploadView(); } + persistQueueStateSoon(); + } + } }); document.getElementById('contextMenu').addEventListener('click', (e) => { diff --git a/renderer/styles.css b/renderer/styles.css index 8dbfb32..0ebd057 100644 --- a/renderer/styles.css +++ b/renderer/styles.css @@ -169,15 +169,15 @@ body { /* Queue Container */ .queue-shell { - flex: 1; + flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; + overflow: hidden; } .queue-container { - flex: 1 1 50%; - min-height: 150px; - max-height: 50%; + flex: 1 1 0; + min-height: 0; overflow: auto; padding: 0; background: rgba(255, 255, 255, 0.02); @@ -291,8 +291,7 @@ body { } .recent-files-panel { - flex: 1 1 auto; - min-height: 150px; + flex: 0 0 180px; display: flex; flex-direction: column; border-top: 1px solid var(--border);