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",
|
"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": {
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user