From 2e09a3d9d7247783ab898b5e89faaf4a7bf83eff Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 11 Mar 2026 03:51:05 +0100 Subject: [PATCH] feat: selectable recent files with context menu - Click/Ctrl+Click/Shift+Click to select rows in Files panel - Ctrl+A to select all, Delete to remove selected - Right-click context menu with "Copy links" and "Remove" - Double-click to copy single link Co-Authored-By: Claude Opus 4.6 --- renderer/app.js | 129 ++++++++++++++++++++++++++++++++++---------- renderer/index.html | 5 ++ renderer/styles.css | 1 + 3 files changed, 106 insertions(+), 29 deletions(-) diff --git a/renderer/app.js b/renderer/app.js index 2232c20..a78028c 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -28,7 +28,8 @@ let historySortState = { key: 'date', direction: 'desc' }; // Session-specific files for the "Files" panel (resets each session) let sessionFilesData = []; -let recentSortState = { key: 'date', direction: 'desc' }; // null key = default (date desc) +let recentSortState = { key: 'date', direction: 'desc' }; +let selectedRecentIds = new Set(); // --- Init --- async function init() { @@ -780,6 +781,22 @@ function showContextMenu(x, y) { function hideContextMenu() { document.getElementById('contextMenu').style.display = 'none'; + document.getElementById('recentContextMenu').style.display = 'none'; +} + +function deleteSelectedRecentFiles() { + if (selectedRecentIds.size === 0) return; + sessionFilesData = sessionFilesData.filter(r => !selectedRecentIds.has(r.order)); + selectedRecentIds.clear(); + renderRecentUploadsPanel(); +} + +function copySelectedRecentLinks() { + const links = sessionFilesData + .filter(r => selectedRecentIds.has(r.order) && !r.isError) + .map(r => r.link) + .filter(Boolean); + if (links.length) { window.api.copyToClipboard(links.join('\n')); showCopyToast(`${links.length} Links kopiert`); } } document.addEventListener('click', (e) => { @@ -790,30 +807,40 @@ 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) { + if (e.target.closest('input, textarea, select')) return; + const activeView = document.querySelector('.view.active'); + // Ctrl+A + if ((e.ctrlKey || e.metaKey) && e.key === 'a') { + if (activeView && activeView.id === 'upload-view') { e.preventDefault(); - queueJobs.forEach(j => selectedJobIds.add(j.id)); - renderQueueTable(); + // If recent files panel is focused / has selection, select all recent files + if (selectedRecentIds.size > 0 || document.activeElement?.closest('.recent-files-panel')) { + sessionFilesData.forEach(r => selectedRecentIds.add(r.order)); + renderRecentUploadsPanel(); + } else if (queueJobs.length > 0) { + 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) { + // Delete + if (e.key === 'Delete') { + if (activeView && activeView.id === 'upload-view') { e.preventDefault(); - queueJobs = queueJobs.filter(j => { - if (selectedJobIds.has(j.id)) { removeJobFromIndex(j); return false; } - return true; - }); - selectedJobIds.clear(); - syncSelectedFilesFromQueue(); - renderQueueTable(); - if (queueJobs.length === 0) { selectedFiles = []; updateUploadView(); } - updateStatusBar(); - persistQueueStateSoon(); + if (selectedRecentIds.size > 0) { + deleteSelectedRecentFiles(); + } else if (selectedJobIds.size > 0 && !uploading) { + queueJobs = queueJobs.filter(j => { + if (selectedJobIds.has(j.id)) { removeJobFromIndex(j); return false; } + return true; + }); + selectedJobIds.clear(); + syncSelectedFilesFromQueue(); + renderQueueTable(); + if (queueJobs.length === 0) { selectedFiles = []; updateUploadView(); } + updateStatusBar(); + persistQueueStateSoon(); + } } } }); @@ -1976,7 +2003,7 @@ function renderRecentUploadsPanel() { const rows = sortRecentFiles(sessionFilesData); tbody.innerHTML = rows.map(row => ` - + ${escapeHtml(row.date)} ${escapeHtml(row.filename)} ${escapeHtml(row.host)} @@ -1984,14 +2011,32 @@ function renderRecentUploadsPanel() { `).join(''); - tbody.querySelectorAll('.recent-file-row').forEach(row => { - row.addEventListener('click', () => { - if (row.classList.contains('error')) return; - const link = row.dataset.link; - if (link) { - window.api.copyToClipboard(link); - showCopyToast('Link kopiert'); + tbody.querySelectorAll('.recent-file-row').forEach(tr => { + tr.addEventListener('click', (e) => { + const id = parseInt(tr.dataset.order, 10); + if (e.ctrlKey || e.metaKey) { + if (selectedRecentIds.has(id)) selectedRecentIds.delete(id); + else selectedRecentIds.add(id); + } else if (e.shiftKey && selectedRecentIds.size > 0) { + const sortedOrders = rows.map(r => r.order); + const lastIdx = sortedOrders.findIndex(o => selectedRecentIds.has(o)); + const curIdx = sortedOrders.indexOf(id); + if (lastIdx >= 0 && curIdx >= 0) { + const from = Math.min(lastIdx, curIdx); + const to = Math.max(lastIdx, curIdx); + for (let i = from; i <= to; i++) selectedRecentIds.add(sortedOrders[i]); + } + } else { + selectedRecentIds.clear(); + selectedRecentIds.add(id); } + renderRecentUploadsPanel(); + }); + + tr.addEventListener('dblclick', () => { + if (tr.classList.contains('error')) return; + const link = tr.dataset.link; + if (link) { window.api.copyToClipboard(link); showCopyToast('Link kopiert'); } }); }); @@ -2084,6 +2129,32 @@ function setupListeners() { } renderRecentUploadsPanel(); }); + + // Recent files context menu + document.getElementById('recentFilesBody').addEventListener('contextmenu', (e) => { + const tr = e.target.closest('.recent-file-row'); + if (!tr) return; + e.preventDefault(); + const id = parseInt(tr.dataset.order, 10); + if (!selectedRecentIds.has(id)) { + selectedRecentIds.clear(); + selectedRecentIds.add(id); + renderRecentUploadsPanel(); + } + const menu = document.getElementById('recentContextMenu'); + menu.style.display = 'block'; + menu.style.left = Math.min(e.clientX, window.innerWidth - 180) + 'px'; + menu.style.top = Math.min(e.clientY, window.innerHeight - 80) + 'px'; + }); + + document.getElementById('recentContextMenu').addEventListener('click', (e) => { + const item = e.target.closest('.ctx-item'); + if (!item) return; + hideContextMenu(); + const action = item.dataset.action; + if (action === 'recent-copy-links') copySelectedRecentLinks(); + else if (action === 'recent-delete') deleteSelectedRecentFiles(); + }); document.getElementById('reuploadSelectedBtn').addEventListener('click', retrySelectedJobs); document.getElementById('abortSelectedBtn').addEventListener('click', abortSelectedJobs); document.getElementById('finishStopBtn').addEventListener('click', finishUploadsInProgress); diff --git a/renderer/index.html b/renderer/index.html index 5defc92..1c59ee8 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -241,6 +241,11 @@
Alle Links kopieren
+ +
Bereit | diff --git a/renderer/styles.css b/renderer/styles.css index 764f5ca..4627b57 100644 --- a/renderer/styles.css +++ b/renderer/styles.css @@ -473,6 +473,7 @@ body { .recent-file-row:hover { background: rgba(255, 255, 255, 0.03); } +.recent-file-row.selected { background: rgba(102, 126, 234, 0.12) !important; } .recent-file-row.error { color: var(--danger); opacity: 0.75;