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)
|
// 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,20 +807,29 @@ 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');
|
||||||
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();
|
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));
|
queueJobs.forEach(j => selectedJobIds.add(j.id));
|
||||||
renderQueueTable();
|
renderQueueTable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Delete — remove selected queue jobs
|
}
|
||||||
if (e.key === 'Delete' && !e.target.closest('input, textarea, select')) {
|
// Delete
|
||||||
const activeView = document.querySelector('.view.active');
|
if (e.key === 'Delete') {
|
||||||
if (activeView && activeView.id === 'upload-view' && selectedJobIds.size > 0 && !uploading) {
|
if (activeView && activeView.id === 'upload-view') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (selectedRecentIds.size > 0) {
|
||||||
|
deleteSelectedRecentFiles();
|
||||||
|
} else if (selectedJobIds.size > 0 && !uploading) {
|
||||||
queueJobs = queueJobs.filter(j => {
|
queueJobs = queueJobs.filter(j => {
|
||||||
if (selectedJobIds.has(j.id)) { removeJobFromIndex(j); return false; }
|
if (selectedJobIds.has(j.id)) { removeJobFromIndex(j); return false; }
|
||||||
return true;
|
return true;
|
||||||
@ -816,6 +842,7 @@ document.addEventListener('keydown', (e) => {
|
|||||||
persistQueueStateSoon();
|
persistQueueStateSoon();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('contextMenu').addEventListener('click', (e) => {
|
document.getElementById('contextMenu').addEventListener('click', (e) => {
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user