feat: improve queue card readability and detail controls
Windows CI / verify (push) Canceled after 0s

This commit is contained in:
Sucukdeluxe
2026-09-06 12:41:30 +02:00
parent c21520ee3d
commit d935e622d2
17 changed files with 285 additions and 61 deletions
+2
View File
@@ -378,6 +378,8 @@ const UI_TEXT_DE = {
statusRunning: 'Läuft',
statusPaused: 'Pausiert',
statusWaiting: 'Wartet',
toggleDetails: 'Download-Details ein-/ausklappen',
removeItem: 'Aus der Queue entfernen',
progressError: 'Fehler',
progressReady: 'Bereit',
progressLoading: 'Lade...',
+2
View File
@@ -378,6 +378,8 @@ const UI_TEXT_EN = {
statusRunning: 'Running',
statusPaused: 'Paused',
statusWaiting: 'Waiting',
toggleDetails: 'Expand or collapse download details',
removeItem: 'Remove from queue',
progressError: 'Error',
progressReady: 'Ready',
progressLoading: 'Loading...',
+1 -1
View File
@@ -408,7 +408,7 @@ describe('renderer queue production paths', () => {
expect(metrics({ status: 'downloading', progress: 12.34, speed: '4 MB/s', eta: '2m' })).toBe('12.3% | 4 MB/s | 2m');
expect(metrics({ status: 'pending', progress: 12.34, speed: '4 MB/s', eta: '2m' })).toBe('');
expect(metrics({ status: 'paused', progress: 12.34, speed: '4 MB/s', eta: '2m' })).toBe('');
expect(metrics({ status: 'paused', progress: 12.34, speed: '4 MB/s', eta: '2m' })).toBe('12.3%');
expect(metrics({ status: 'error', progress: 12.34, speed: '4 MB/s', eta: '2m' })).toBe('');
expect(metrics({ status: 'completed', progress: 100, speed: '4 MB/s', eta: '2m' })).toBe('100%');
});
+29 -12
View File
@@ -135,8 +135,17 @@ function initQueueActions(): void {
list.addEventListener('click', (event: MouseEvent) => {
const control = resolveQueueControl(event.target);
if (!control || !list.contains(control)) return;
if (control.dataset.queueAction === 'details' && event.detail > 1) return;
void activateQueueControl(control);
});
list.addEventListener('dblclick', (event: MouseEvent) => {
if (!(event.target instanceof Element)) return;
if (event.target.closest('button, a, input, select, textarea, [role="button"], [data-queue-action], [data-queue-file-action]')) return;
const item = event.target.closest<HTMLElement>('.queue-item');
if (!item?.dataset.id || !list.contains(item)) return;
event.preventDefault();
toggleQueueDetails(item.dataset.id);
});
list.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key !== 'Enter' && event.key !== ' ') return;
const control = resolveQueueControl(event.target);
@@ -457,7 +466,7 @@ function getQueueProgressStatusText(item: QueueItem): string {
function getQueueProgressMetricsText(item: QueueItem): string {
const parts: string[] = [];
if (item.status === 'completed') parts.push('100%');
if (item.status === 'downloading' && item.progress > 0) {
if ((item.status === 'downloading' || item.status === 'paused') && item.progress > 0) {
parts.push(`${Math.max(0, Math.min(100, item.progress)).toFixed(1)}%`);
}
if (item.status === 'downloading' && item.speed) parts.push(item.speed);
@@ -564,7 +573,10 @@ function toggleQueueDetails(id: string): void {
} else {
expandedQueueIds.add(id);
}
renderQueue();
const item = Array.from(byId<HTMLElement>('queueList').querySelectorAll<HTMLElement>('.queue-item'))
.find((candidate) => candidate.dataset.id === id);
item?.querySelector('.queue-details')?.classList.toggle('expanded', expandedQueueIds.has(id));
item?.querySelector('[data-queue-action="details"]')?.setAttribute('aria-expanded', String(expandedQueueIds.has(id)));
}
function initQueueDragDrop(): void {
@@ -627,7 +639,7 @@ function renderQueue(): void {
const list = byId('queueList');
initQueueActions();
byId('queueCount').textContent = String(queue.length);
byId('queueCount').textContent = formatUiNumber(queue.length);
const retryBtn = byId<HTMLButtonElement>('btnRetryFailed');
const clearBtn = byId<HTMLButtonElement>('btnClear');
const hasFailed = queue.some((item) => item.status === 'error' && !item.mergeRecoveryBlocked);
@@ -683,26 +695,32 @@ function renderQueue(): void {
? renderRecordingHealthBadge(item.recordingHealth)
: '';
const mergeMetaExtra = isMergeGroup
? ` (${UI_TEXT.mergeGroup.metaLabel.replace('{count}', String(item.mergeGroup!.items.length))})`
? escapeHtml(UI_TEXT.mergeGroup.metaLabel.replace('{count}', formatUiNumber(item.mergeGroup!.items.length)))
: '';
const detailsId = `queue-details-${itemIndex}`;
return `
<div class="queue-item${isMergeGroup ? ' merge-group' : ''}${isSelected ? ' merge-selected' : ''}" draggable="${item.status === 'pending' ? 'true' : 'false'}" data-id="${escapeHtml(item.id)}">
${isSelected ? `<span class="queue-selection-order" title="${selectionTitle}" aria-label="${selectionTitle}">${selectionPosition}</span>` : ''}
<div class="status ${item.status}"></div>
<div class="queue-main">
<div class="queue-title-row">
<div class="title" title="${safeTitle}" role="button" tabindex="0" aria-expanded="${expandedQueueIds.has(item.id) ? 'true' : 'false'}" aria-controls="${detailsId}" data-queue-action="details">${liveBadge}${healthBadge}${mergeIcon}${isClip}${safeTitle}</div>
<div class="queue-status-label">${safeStatusLabel}</div>
<span class="remove" role="button" tabindex="0" aria-label="${escapeHtml(UI_TEXT.streamers.removeAria)}" data-queue-action="remove">x</span>
<div class="title" title="${safeTitle}">${liveBadge}${healthBadge}${mergeIcon}${isClip}${safeTitle}</div>
${item.status === 'error' && !item.mergeRecoveryBlocked ? `<button class="queue-retry-btn" type="button" title="${escapeHtml(UI_TEXT.queue.retryItem)}" aria-label="${escapeHtml(UI_TEXT.queue.retryItem)}" data-queue-action="retry">&#x21bb;</button>` : ''}
<button class="remove" type="button" title="${escapeHtml(UI_TEXT.queue.removeItem)}" aria-label="${escapeHtml(UI_TEXT.queue.removeItem)}" data-queue-action="remove"><svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13M10 11v5M14 11v5"/></svg></button>
</div>
<div class="queue-meta"><span class="queue-date">${safeDate}</span>${mergeMetaExtra}</div>
${mergeMetaExtra ? `<div class="queue-meta">${mergeMetaExtra}</div>` : ''}
<div class="queue-progress-wrap" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${Math.round(progressValue)}" aria-label="${escapeHtml(safeStatusLabel)}">
<div class="queue-progress-bar" style="width: ${progressValue}%;"></div>
</div>
<div class="queue-progress-info">
<span class="queue-progress-status${progressStatusClass}">${safeProgressStatus}</span>
<div class="queue-footer">
<div class="queue-summary">
<button class="queue-details-toggle" type="button" aria-label="${escapeHtml(UI_TEXT.queue.toggleDetails)}" title="${escapeHtml(UI_TEXT.queue.toggleDetails)}" aria-expanded="${expandedQueueIds.has(item.id) ? 'true' : 'false'}" aria-controls="${detailsId}" data-queue-action="details"><svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m7 10 5 5 5-5"/></svg></button>
<span class="queue-status-badge" title="${safeProgressStatus}"><span class="status ${item.status}" aria-hidden="true"></span><span class="queue-status-label">${safeStatusLabel}</span></span>
</div>
<span class="queue-date">${safeDate}</span>
</div>
<div class="queue-progress-info${item.status === 'pending' || item.status === 'completed' ? ' is-hidden' : ''}">
<span class="queue-progress-status${progressStatusClass}${item.status === 'paused' ? ' is-hidden' : ''}">${safeProgressStatus}</span>
<span class="queue-progress-metrics">${safeProgressMetrics}</span>
</div>
<div class="queue-details${expandedQueueIds.has(item.id) ? ' expanded' : ''}" id="${detailsId}">
@@ -713,7 +731,6 @@ function renderQueue(): void {
${renderQueueItemFileActions(item)}
</div>
</div>
${item.status === 'error' && !item.mergeRecoveryBlocked ? `<button class="queue-retry-btn" type="button" title="${escapeHtml(UI_TEXT.queue.retryItem)}" aria-label="${escapeHtml(UI_TEXT.queue.retryItem)}" data-queue-action="retry">&#x21bb;</button>` : ''}
</div>
`;
}).join('');
+2 -2
View File
@@ -20,7 +20,7 @@ describe('production style modules', () => {
.replace(/\r\n/g, '\n'));
const digest = createHash('sha256').update(content).digest('hex');
expect(digest).toBe('143fb0cc3e6c2ca3f04ded7e3b83175db424b34b6ca9ed4ef32e337b4a60a898');
expect(digest).toBe('2ed5d8cce6dca293fba4f83a0347950ee887c01cafb9febfb345842a6c569cd5');
});
test('derives the Windows hot-development executable version from package metadata', () => {
@@ -57,7 +57,7 @@ describe('production style modules', () => {
test('animates queue progress only while downloading and visibly marks paused items', () => {
const styles = readFileSync(join(__dirname, 'styles-workflows.css'), 'utf8');
const baseShimmer = styles.match(/\.queue-progress-bar::after\s*\{([\s\S]*?)\}/)?.[1] ?? '';
const activeShimmer = styles.match(/\.status\.downloading\s*~\s*\.queue-main\s+\.queue-progress-bar::after\s*\{([\s\S]*?)\}/)?.[1] ?? '';
const activeShimmer = styles.match(/\.queue-item:has\(\.status\.downloading\)\s+\.queue-progress-bar::after\s*\{([\s\S]*?)\}/)?.[1] ?? '';
const pausedStatus = styles.match(/\.queue-item\s+\.status\.paused\s*\{([\s\S]*?)\}/)?.[1] ?? '';
expect(baseShimmer).toMatch(/display:\s*none/);
+108 -38
View File
@@ -93,7 +93,6 @@
.queue-item:has(.status.completed) {
border-left-color: var(--success);
opacity: 0.85;
}
.queue-item .status {
@@ -102,7 +101,7 @@
border-radius: 50%;
background: var(--text-secondary);
flex-shrink: 0;
margin-top: 4px;
margin: 0;
}
.queue-item .status.pending { background: var(--warning); box-shadow: 0 0 6px rgba(255, 167, 38, 0.5); }
@@ -118,10 +117,15 @@
.queue-item .title {
flex: 1;
min-width: 0;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
overflow-wrap: anywhere;
font-size: 13px;
font-weight: 600;
line-height: 19px;
}
.queue-detail-label {
@@ -167,19 +171,21 @@
.queue-title-row {
display: flex;
align-items: center;
gap: 8px;
min-height: 16px;
gap: 6px;
min-height: 32px;
margin-bottom: 9px;
}
.queue-status-label {
flex-shrink: 0;
font-size: 10px;
color: var(--text-secondary);
line-height: 16px;
font-size: 11px;
font-weight: 600;
color: var(--text);
line-height: 18px;
}
.queue-meta {
font-size: 10px;
font-size: 12px;
color: var(--text-secondary);
margin-top: 2px;
margin-bottom: 4px;
@@ -188,11 +194,50 @@
text-overflow: ellipsis;
}
.queue-footer,
.queue-summary {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.queue-footer {
justify-content: space-between;
margin-top: 6px;
}
.queue-date {
flex-shrink: 0;
margin-left: auto;
color: var(--text-secondary);
font-size: 12px;
line-height: 18px;
font-variant-numeric: tabular-nums;
text-align: right;
}
.queue-status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 7px;
border: 1px solid var(--border-soft);
border-radius: 6px;
background: color-mix(in srgb, var(--text-secondary) 8%, transparent);
white-space: nowrap;
}
.queue-status-badge:has(.status.pending) {
background: color-mix(in srgb, var(--warning) 10%, transparent);
border-color: color-mix(in srgb, var(--warning) 25%, transparent);
}
.queue-progress-wrap {
height: 5px;
border-radius: 999px;
overflow: hidden;
background: rgba(255,255,255,0.10);
background: color-mix(in srgb, var(--text-secondary) 18%, transparent);
position: relative;
}
@@ -215,7 +260,7 @@
animation: none;
}
.status.downloading ~ .queue-main .queue-progress-bar::after {
.queue-item:has(.status.downloading) .queue-progress-bar::after {
display: block;
animation: queue-progress-shimmer 1.8s ease-in-out infinite;
}
@@ -227,21 +272,20 @@
.queue-progress-info {
display: flex;
align-items: center;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
margin-top: 3px;
font-size: 10px;
flex-wrap: wrap;
font-size: 12px;
color: var(--text-secondary);
line-height: 14px;
line-height: 18px;
}
.queue-progress-status,
.queue-progress-metrics {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow-wrap: anywhere;
}
.queue-progress-status.is-starting {
@@ -259,22 +303,51 @@
font-variant-numeric: tabular-nums;
}
.queue-item .remove {
.queue-item .remove,
.queue-item .queue-retry-btn,
.queue-details-toggle {
display: inline-flex;
width: 16px;
height: 16px;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
flex: 0 0 16px;
flex: 0 0 32px;
padding: 0;
cursor: pointer;
color: var(--error);
opacity: 0.7;
font-size: 11px;
line-height: 16px;
color: var(--text-secondary);
background: transparent;
border: 1px solid var(--border-soft);
border-radius: 7px;
font-size: 18px;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.queue-item .remove:hover {
opacity: 1;
color: var(--error);
background: color-mix(in srgb, var(--error) 12%, transparent);
border-color: color-mix(in srgb, var(--error) 45%, transparent);
}
.queue-details-toggle {
width: 28px;
height: 28px;
flex-basis: 28px;
border-color: transparent;
}
.queue-details-toggle:hover,
.queue-item .queue-retry-btn:hover {
color: var(--text);
background: color-mix(in srgb, var(--text-secondary) 12%, transparent);
}
.queue-details-toggle[aria-expanded="true"] svg {
transform: rotate(180deg);
}
.queue-details-toggle:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.queue-item[draggable="true"] {
@@ -291,10 +364,13 @@
.queue-details {
display: none;
font-size: 10px;
font-size: 12px;
line-height: 18px;
color: var(--text-secondary);
padding: 4px 0;
word-break: break-all;
margin-top: 8px;
padding: 8px 0 2px;
border-top: 1px solid var(--border-soft);
overflow-wrap: anywhere;
}
.queue-details.expanded {
@@ -330,12 +406,6 @@
box-shadow: inset 0 0 0 1px rgba(0, 200, 83, 0.45);
}
.queue-item .title:focus-visible {
outline: none;
border-radius: 3px;
box-shadow: 0 0 0 2px rgba(145, 70, 255, 0.45);
}
.queue-item.merge-group {
border-left: 3px solid var(--accent);
}
@@ -780,7 +850,7 @@ input[type="checkbox"].vod-select-checkbox {
display: inline-block;
background: #ff4444;
color: white;
font-size: 9px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.5px;
padding: 1px 5px;
+2 -2
View File
@@ -956,8 +956,8 @@ textarea:disabled,
}
.context-sidebar .queue-item {
margin: 3px 0;
padding: 7px;
margin: 0 0 8px;
padding: 10px;
background: var(--workspace-panel-raised);
border: 1px solid var(--workspace-border);
border-radius: var(--workspace-radius-small);