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:
parent
f7e8f9a56c
commit
cc5ee47fb8
@ -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": {
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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 += `<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);
|
||||
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);
|
||||
// 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(allRows[i].dataset.jobId);
|
||||
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) => {
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user