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:
parent
14490d947a
commit
2e09a3d9d7
103
renderer/app.js
103
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,20 +807,29 @@ 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')) {
|
||||
if (e.target.closest('input, textarea, select')) return;
|
||||
const activeView = document.querySelector('.view.active');
|
||||
if (activeView && activeView.id === 'upload-view' && queueJobs.length > 0) {
|
||||
// Ctrl+A
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
|
||||
if (activeView && activeView.id === 'upload-view') {
|
||||
e.preventDefault();
|
||||
// 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();
|
||||
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;
|
||||
@ -816,6 +842,7 @@ document.addEventListener('keydown', (e) => {
|
||||
persistQueueStateSoon();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('contextMenu').addEventListener('click', (e) => {
|
||||
@ -1976,7 +2003,7 @@ function renderRecentUploadsPanel() {
|
||||
const rows = sortRecentFiles(sessionFilesData);
|
||||
|
||||
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 title="${escapeAttr(row.filename)}">${escapeHtml(row.filename)}</td>
|
||||
<td>${escapeHtml(row.host)}</td>
|
||||
@ -1984,14 +2011,32 @@ function renderRecentUploadsPanel() {
|
||||
</tr>
|
||||
`).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);
|
||||
|
||||
@ -241,6 +241,11 @@
|
||||
<div class="ctx-item" data-action="copy-all-links">Alle Links kopieren</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">
|
||||
<span class="sb-state" id="sbState">Bereit</span>
|
||||
<span class="sb-separator">|</span>
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user