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 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-11 03:51:05 +01:00
parent 14490d947a
commit 2e09a3d9d7
3 changed files with 106 additions and 29 deletions

View File

@ -28,7 +28,8 @@ let historySortState = { key: 'date', direction: 'desc' };
// Session-specific files for the "Files" panel (resets each session) // Session-specific files for the "Files" panel (resets each session)
let sessionFilesData = []; let sessionFilesData = [];
let recentSortState = { key: 'date', direction: 'desc' }; // null key = default (date desc) let recentSortState = { key: 'date', direction: 'desc' };
let selectedRecentIds = new Set();
// --- Init --- // --- Init ---
async function init() { async function init() {
@ -780,6 +781,22 @@ function showContextMenu(x, y) {
function hideContextMenu() { function hideContextMenu() {
document.getElementById('contextMenu').style.display = 'none'; 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) => { document.addEventListener('click', (e) => {
@ -790,30 +807,40 @@ document.addEventListener('keydown', (e) => {
hideContextMenu(); hideContextMenu();
cancelHosterModal(); cancelHosterModal();
} }
// Ctrl+A — select all queue jobs (only when not in an input/textarea) if (e.target.closest('input, textarea, select')) return;
if ((e.ctrlKey || e.metaKey) && e.key === 'a' && !e.target.closest('input, textarea, select')) { const activeView = document.querySelector('.view.active');
const activeView = document.querySelector('.view.active'); // Ctrl+A
if (activeView && activeView.id === 'upload-view' && queueJobs.length > 0) { if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
if (activeView && activeView.id === 'upload-view') {
e.preventDefault(); e.preventDefault();
queueJobs.forEach(j => selectedJobIds.add(j.id)); // If recent files panel is focused / has selection, select all recent files
renderQueueTable(); 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 // Delete
if (e.key === 'Delete' && !e.target.closest('input, textarea, select')) { if (e.key === 'Delete') {
const activeView = document.querySelector('.view.active'); if (activeView && activeView.id === 'upload-view') {
if (activeView && activeView.id === 'upload-view' && selectedJobIds.size > 0 && !uploading) {
e.preventDefault(); e.preventDefault();
queueJobs = queueJobs.filter(j => { if (selectedRecentIds.size > 0) {
if (selectedJobIds.has(j.id)) { removeJobFromIndex(j); return false; } deleteSelectedRecentFiles();
return true; } else if (selectedJobIds.size > 0 && !uploading) {
}); queueJobs = queueJobs.filter(j => {
selectedJobIds.clear(); if (selectedJobIds.has(j.id)) { removeJobFromIndex(j); return false; }
syncSelectedFilesFromQueue(); return true;
renderQueueTable(); });
if (queueJobs.length === 0) { selectedFiles = []; updateUploadView(); } selectedJobIds.clear();
updateStatusBar(); syncSelectedFilesFromQueue();
persistQueueStateSoon(); renderQueueTable();
if (queueJobs.length === 0) { selectedFiles = []; updateUploadView(); }
updateStatusBar();
persistQueueStateSoon();
}
} }
} }
}); });
@ -1976,7 +2003,7 @@ function renderRecentUploadsPanel() {
const rows = sortRecentFiles(sessionFilesData); const rows = sortRecentFiles(sessionFilesData);
tbody.innerHTML = rows.map(row => ` tbody.innerHTML = rows.map(row => `
<tr class="recent-file-row${row.isError ? ' error' : ''}" data-link="${escapeAttr(row.link)}"> <tr class="recent-file-row${row.isError ? ' error' : ''}${selectedRecentIds.has(row.order) ? ' selected' : ''}" data-order="${row.order}" data-link="${escapeAttr(row.link)}">
<td>${escapeHtml(row.date)}</td> <td>${escapeHtml(row.date)}</td>
<td title="${escapeAttr(row.filename)}">${escapeHtml(row.filename)}</td> <td title="${escapeAttr(row.filename)}">${escapeHtml(row.filename)}</td>
<td>${escapeHtml(row.host)}</td> <td>${escapeHtml(row.host)}</td>
@ -1984,14 +2011,32 @@ function renderRecentUploadsPanel() {
</tr> </tr>
`).join(''); `).join('');
tbody.querySelectorAll('.recent-file-row').forEach(row => { tbody.querySelectorAll('.recent-file-row').forEach(tr => {
row.addEventListener('click', () => { tr.addEventListener('click', (e) => {
if (row.classList.contains('error')) return; const id = parseInt(tr.dataset.order, 10);
const link = row.dataset.link; if (e.ctrlKey || e.metaKey) {
if (link) { if (selectedRecentIds.has(id)) selectedRecentIds.delete(id);
window.api.copyToClipboard(link); else selectedRecentIds.add(id);
showCopyToast('Link kopiert'); } 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(); 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('reuploadSelectedBtn').addEventListener('click', retrySelectedJobs);
document.getElementById('abortSelectedBtn').addEventListener('click', abortSelectedJobs); document.getElementById('abortSelectedBtn').addEventListener('click', abortSelectedJobs);
document.getElementById('finishStopBtn').addEventListener('click', finishUploadsInProgress); document.getElementById('finishStopBtn').addEventListener('click', finishUploadsInProgress);

View File

@ -241,6 +241,11 @@
<div class="ctx-item" data-action="copy-all-links">Alle Links kopieren</div> <div class="ctx-item" data-action="copy-all-links">Alle Links kopieren</div>
</div> </div>
<div class="context-menu" id="recentContextMenu" style="display:none">
<div class="ctx-item" data-action="recent-copy-links">Links kopieren</div>
<div class="ctx-item" data-action="recent-delete">Entfernen</div>
</div>
<div class="statusbar" id="statusbar"> <div class="statusbar" id="statusbar">
<span class="sb-state" id="sbState">Bereit</span> <span class="sb-state" id="sbState">Bereit</span>
<span class="sb-separator">|</span> <span class="sb-separator">|</span>

View File

@ -473,6 +473,7 @@ body {
.recent-file-row:hover { .recent-file-row:hover {
background: rgba(255, 255, 255, 0.03); background: rgba(255, 255, 255, 0.03);
} }
.recent-file-row.selected { background: rgba(102, 126, 234, 0.12) !important; }
.recent-file-row.error { .recent-file-row.error {
color: var(--danger); color: var(--danger);
opacity: 0.75; opacity: 0.75;