feat: improve queue card readability and detail controls
Windows CI / verify (push) Canceled after 0s
Windows CI / verify (push) Canceled after 0s
This commit is contained in:
@@ -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...',
|
||||
|
||||
@@ -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...',
|
||||
|
||||
@@ -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
@@ -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">↻</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">↻</button>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user