feat: sticky tab bar, improved context menu, instant retry

- Sticky tab bar: stays fixed at top when scrolling settings/history
- Context menu improvements:
  - Click on empty queue area deselects all selected jobs
  - Dynamic labels with selection count (e.g. "Links kopieren (3)")
  - Singular/plural for single selection ("Link kopieren" vs "Links kopieren")
  - "Alle entfernen" to clear entire queue
  - Reorganized menu items into logical groups with separators
- Instant retry: "Erneut versuchen" now immediately starts uploading
  the selected files instead of just resetting status to preview

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-11 19:52:24 +01:00
parent 6f939103b9
commit bb30b58037
3 changed files with 51 additions and 4 deletions

View File

@ -768,6 +768,17 @@ function showContextMenu(x, y) {
// Update "Always on top" text // Update "Always on top" text
const aotItem = menu.querySelector('[data-action="always-on-top"]'); const aotItem = menu.querySelector('[data-action="always-on-top"]');
if (aotItem) aotItem.textContent = alwaysOnTopState ? 'Immer im Vordergrund ✓' : 'Immer im Vordergrund'; if (aotItem) aotItem.textContent = alwaysOnTopState ? 'Immer im Vordergrund ✓' : 'Immer im Vordergrund';
// Update labels with selection count
const n = selectedJobIds.size;
const delItem = menu.querySelector('[data-action="delete-selected"]');
if (delItem) delItem.textContent = n > 1 ? `Entfernen (${n})` : 'Entfernen';
const copyItem = menu.querySelector('[data-action="copy-links"]');
if (copyItem) copyItem.textContent = n > 1 ? `Links kopieren (${n})` : 'Link kopieren';
menu.querySelectorAll('[data-action="retry-selected"]').forEach(el => {
el.textContent = n > 1 ? `Erneut versuchen (${n})` : 'Erneut versuchen';
});
const startItem = menu.querySelector('[data-action="start-selected"]');
if (startItem) startItem.textContent = n > 1 ? `Ausgewählte starten (${n})` : 'Ausgewählte starten';
menu.style.display = 'block'; menu.style.display = 'block';
const menuX = Math.min(x, window.innerWidth - menu.offsetWidth - 5); const menuX = Math.min(x, window.innerWidth - menu.offsetWidth - 5);
@ -953,6 +964,16 @@ async function handleContextAction(action) {
persistQueueStateSoon(); persistQueueStateSoon();
} else if (action === 'copy-all-links') { } else if (action === 'copy-all-links') {
copyAllLinks(); copyAllLinks();
} else if (action === 'delete-all') {
queueJobs.forEach(j => removeJobFromIndex(j));
queueJobs = [];
selectedJobIds.clear();
selectedFiles = [];
syncSelectedFilesFromQueue();
renderQueueTable();
updateUploadView();
updateStatusBar();
persistQueueStateSoon();
} else if (action === 'always-on-top') { } else if (action === 'always-on-top') {
alwaysOnTopState = !alwaysOnTopState; alwaysOnTopState = !alwaysOnTopState;
await window.api.setAlwaysOnTop(alwaysOnTopState); await window.api.setAlwaysOnTop(alwaysOnTopState);
@ -1237,7 +1258,8 @@ function handleStats(data) {
} }
// --- Retry --- // --- Retry ---
function retrySelectedJobs() { async function retrySelectedJobs() {
const retryJobs = [];
queueJobs.forEach(j => { queueJobs.forEach(j => {
if (selectedJobIds.has(j.id) && ['error', 'done', 'aborted', 'skipped'].includes(j.status)) { if (selectedJobIds.has(j.id) && ['error', 'done', 'aborted', 'skipped'].includes(j.status)) {
j.status = 'preview'; j.status = 'preview';
@ -1249,16 +1271,22 @@ function retrySelectedJobs() {
j.remaining = 0; j.remaining = 0;
j.progress = 0; j.progress = 0;
j.uploadId = null; j.uploadId = null;
retryJobs.push(j);
if (!selectedFiles.find(f => f.path === j.file || f.name === j.fileName)) { if (!selectedFiles.find(f => f.path === j.file || f.name === j.fileName)) {
selectedFiles.push({ path: j.file, name: j.fileName, size: j.bytesTotal }); selectedFiles.push({ path: j.file, name: j.fileName, size: j.bytesTotal });
} }
} }
}); });
if (retryJobs.length === 0) return;
// Select the retry jobs and start them immediately
selectedJobIds.clear(); selectedJobIds.clear();
retryJobs.forEach(j => selectedJobIds.add(j.id));
renderQueueTable(); renderQueueTable();
updateQueueActionButtons(); updateQueueActionButtons();
updateStatusBar(); updateStatusBar();
persistQueueStateSoon(); persistQueueStateSoon();
await startSelectedUpload();
} }
async function abortSelectedJobs() { async function abortSelectedJobs() {
@ -2314,6 +2342,17 @@ function setupListeners() {
document.getElementById('shutdownOverlay').style.display = 'none'; document.getElementById('shutdownOverlay').style.display = 'none';
}); });
// Click on empty area in queue → deselect all
document.getElementById('upload-view').addEventListener('click', (e) => {
if (!e.target.closest('.queue-row') && !e.target.closest('.btn') && !e.target.closest('.context-menu') && !e.target.closest('.recent-files-panel')) {
if (selectedJobIds.size > 0) {
selectedJobIds.clear();
renderQueueTable();
updateQueueActionButtons();
}
}
});
// Right-click on upload view background // Right-click on upload view background
document.getElementById('upload-view').addEventListener('contextmenu', (e) => { document.getElementById('upload-view').addEventListener('contextmenu', (e) => {
if (e.target.closest('.queue-row')) return; // handled per row if (e.target.closest('.queue-row')) return; // handled per row

View File

@ -266,11 +266,15 @@
<div class="context-menu" id="contextMenu" style="display:none"> <div class="context-menu" id="contextMenu" style="display:none">
<div class="ctx-item" data-action="start-selected">Ausgewählte starten</div> <div class="ctx-item" data-action="start-selected">Ausgewählte starten</div>
<div class="ctx-item" data-action="copy-links">Links kopieren</div>
<div class="ctx-item" data-action="retry-selected">Erneut versuchen</div> <div class="ctx-item" data-action="retry-selected">Erneut versuchen</div>
<div class="ctx-item" data-action="delete-selected">Entfernen</div>
<div class="ctx-separator"></div> <div class="ctx-separator"></div>
<div class="ctx-item" data-action="copy-links">Links kopieren</div>
<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 class="ctx-separator"></div>
<div class="ctx-item" data-action="delete-selected">Entfernen</div>
<div class="ctx-item" data-action="delete-all">Alle entfernen</div>
<div class="ctx-separator"></div>
<div class="ctx-item" data-action="retry-selected">Erneut versuchen</div>
</div> </div>
<div class="context-menu" id="recentContextMenu" style="display:none"> <div class="context-menu" id="recentContextMenu" style="display:none">

View File

@ -26,7 +26,8 @@ body {
background: background:
radial-gradient(circle at top right, rgba(62, 167, 255, 0.08), transparent 28%), radial-gradient(circle at top right, rgba(62, 167, 255, 0.08), transparent 28%),
linear-gradient(180deg, #14171b 0%, #191d23 100%); linear-gradient(180deg, #14171b 0%, #191d23 100%);
min-height: 100vh; height: 100vh;
overflow: hidden;
color: var(--text); color: var(--text);
user-select: none; user-select: none;
display: flex; display: flex;
@ -41,6 +42,9 @@ body {
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(0, 0, 0, 0.08)); background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(0, 0, 0, 0.08));
flex-shrink: 0; flex-shrink: 0;
position: sticky;
top: 0;
z-index: 100;
} }
.tab { .tab {