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 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-11 02:03:10 +01:00
parent f7e8f9a56c
commit cc5ee47fb8
4 changed files with 58 additions and 20 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "1.5.2", "version": "1.5.3",
"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": {

View File

@ -1,4 +1,4 @@
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer, webUtils } = require('electron');
contextBridge.exposeInMainWorld('api', { contextBridge.exposeInMainWorld('api', {
// Config // Config
@ -65,6 +65,8 @@ contextBridge.exposeInMainWorld('api', {
onShutdownCountdown: (callback) => { onShutdownCountdown: (callback) => {
ipcRenderer.on('shutdown-countdown', (_event, data) => callback(data)); ipcRenderer.on('shutdown-countdown', (_event, data) => callback(data));
}, },
// File path from drag & drop (Electron 33+ compatible)
getPathForFile: (file) => webUtils.getPathForFile(file),
removeAllListeners: () => { removeAllListeners: () => {
ipcRenderer.removeAllListeners('upload-progress'); ipcRenderer.removeAllListeners('upload-progress');
ipcRenderer.removeAllListeners('upload-batch-done'); ipcRenderer.removeAllListeners('upload-batch-done');

View File

@ -354,10 +354,19 @@ let _pendingFiles = []; // Files waiting for hoster modal confirmation
function addDroppedFiles(fileList) { function addDroppedFiles(fileList) {
const files = Array.from(fileList); const files = Array.from(fileList);
const existingPaths = new Set([
...selectedFiles.map(f => f.path),
..._pendingFiles.map(f => f.path)
]);
const newFiles = []; const newFiles = [];
for (const file of files) { for (const file of files) {
if (!selectedFiles.find(f => f.path === file.path) && !_pendingFiles.find(f => f.path === file.path)) { // Use webUtils.getPathForFile (Electron 33+) with fallback to file.path
newFiles.push({ path: file.path, name: file.name, size: file.size }); 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) { if (newFiles.length > 0) {
@ -527,7 +536,8 @@ function renderQueueTable() {
tbody.innerHTML = _sortedJobsCache.map(buildRowHtml).join(''); tbody.innerHTML = _sortedJobsCache.map(buildRowHtml).join('');
_lastVisibleRange = { start: -1, end: -1 }; _lastVisibleRange = { start: -1, end: -1 };
} else { } else {
// Virtual scrolling for large queues // Virtual scrolling for large queues — force re-render
_lastVisibleRange = { start: -1, end: -1 };
_renderVirtualRows(tbody); _renderVirtualRows(tbody);
} }
@ -554,9 +564,9 @@ function _renderVirtualRows(tbody) {
if (!scrollContainer) return; if (!scrollContainer) return;
const totalRows = _sortedJobsCache.length; const totalRows = _sortedJobsCache.length;
const totalHeight = totalRows * VIRTUAL_ROW_HEIGHT;
const scrollTop = scrollContainer.scrollTop; 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 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); 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 }; _lastVisibleRange = { start: startIdx, end: endIdx };
const topPad = startIdx * VIRTUAL_ROW_HEIGHT; 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 = ''; let html = '';
if (topPad > 0) html += `<tr class="virtual-spacer" style="height:${topPad}px"><td colspan="8"></td></tr>`; if (topPad > 0) html += `<tr class="virtual-spacer" style="height:${topPad}px"><td colspan="8"></td></tr>`;
@ -627,12 +637,15 @@ function handleRowClick(e, row) {
if (selectedJobIds.has(jobId)) selectedJobIds.delete(jobId); if (selectedJobIds.has(jobId)) selectedJobIds.delete(jobId);
else selectedJobIds.add(jobId); else selectedJobIds.add(jobId);
} else if (e.shiftKey && selectedJobIds.size > 0) { } else if (e.shiftKey && selectedJobIds.size > 0) {
const allRows = Array.from(document.querySelectorAll('.queue-row')); // Use sorted jobs cache for correct shift-click with virtual scrolling
const lastIdx = allRows.findIndex(r => selectedJobIds.has(r.dataset.jobId)); const sortedIds = _sortedJobsCache.map(j => j.id);
const curIdx = allRows.indexOf(row); const lastIdx = sortedIds.findIndex(id => selectedJobIds.has(id));
const from = Math.min(lastIdx, curIdx); const curIdx = sortedIds.indexOf(jobId);
const to = Math.max(lastIdx, curIdx); if (lastIdx >= 0 && curIdx >= 0) {
for (let i = from; i <= to; i++) selectedJobIds.add(allRows[i].dataset.jobId); const from = Math.min(lastIdx, curIdx);
const to = Math.max(lastIdx, curIdx);
for (let i = from; i <= to; i++) selectedJobIds.add(sortedIds[i]);
}
} else { } else {
selectedJobIds.clear(); selectedJobIds.clear();
selectedJobIds.add(jobId); selectedJobIds.add(jobId);
@ -692,6 +705,30 @@ document.addEventListener('keydown', (e) => {
hideContextMenu(); hideContextMenu();
cancelHosterModal(); 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) => { document.getElementById('contextMenu').addEventListener('click', (e) => {

View File

@ -169,15 +169,15 @@ body {
/* Queue Container */ /* Queue Container */
.queue-shell { .queue-shell {
flex: 1; flex: 1 1 0;
min-height: 0; min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
} }
.queue-container { .queue-container {
flex: 1 1 50%; flex: 1 1 0;
min-height: 150px; min-height: 0;
max-height: 50%;
overflow: auto; overflow: auto;
padding: 0; padding: 0;
background: rgba(255, 255, 255, 0.02); background: rgba(255, 255, 255, 0.02);
@ -291,8 +291,7 @@ body {
} }
.recent-files-panel { .recent-files-panel {
flex: 1 1 auto; flex: 0 0 180px;
min-height: 150px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);