From 35845561148c7acc300db4a81f6848bb21e33e92 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:35:11 +0200 Subject: [PATCH] feat: copy queue URLs from single-line detail links --- CHANGELOG.md | 1 + PROJECT_MEMORY.md | 1 + scripts/smoke-test-queue-cards.js | 30 +++++++++++++++++++++-- src/renderer-queue.ts | 5 +++- src/style-modules.production-path.test.ts | 2 +- src/styles-workflows.css | 28 +++++++++++++++++++++ 6 files changed, 63 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98eb817..3b8507b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Put queue detail URLs on a separate single line; click or use the keyboard to copy the complete URL with confirmation. - Keep queue-card titles and content aligned when expanding or collapsing details by reserving scrollbar space. - Move the queue-card remove button slightly toward the top-right corner while retaining its 32-pixel click target. - Show a yellow left border on waiting queue cards, matching their status dot. diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md index a573cc7..d44bb51 100644 --- a/PROJECT_MEMORY.md +++ b/PROJECT_MEMORY.md @@ -20,6 +20,7 @@ Aktueller veröffentlichter Stand: **1.0.20**, Tag `v1.0.20`, veröffentlicht am ## Letzte Änderungen +- Queue-Details zeigen `URL:` separat und darunter einen einzeiligen, anklickbaren Link in 11 px. Klick, Enter oder Leertaste kopieren die vollständige URL über die vorhandene Kopierfunktion mit lokalisierter Erfolgs-/Fehlermeldung. Standard-VOD-URL einschließlich zehnstelliger ID vollständig sichtbar; überlange URLs werden nur in der Anzeige mit Ellipse gekürzt, bleiben vollständig kopierbar und im Tooltip lesbar. Build, 19 gezielte Tests, Queue-UI-Prüfung einschließlich Layout und Kopieraktionen sowie Lint erfolgreich (15 bestehende Warnungen); Hell/Dunkel visuell geprüft. Noch nicht veröffentlicht. - Titelsprung beim Auf-/Zuklappen der Queue-Cards behoben: `scrollbar-gutter: stable` reserviert den 8-px-Platz der Scrollleiste, damit sich Kartenbreite und Titelumbruch nicht ändern. In Tab- und Split-Ansicht nachgemessen. Neuer Queue-UI-Regressionscheck überschreitet beim Öffnen gezielt die Scrollgrenze und prüft die Titelgeometrie während beider Animationen in Hell/Dunkel und Deutsch/Englisch; Hintergrund-Synchronisierung wird nach dem Fixture-Abgleich für diese deterministische Messung angehalten. Vor dem Fix fehlgeschlagen, danach erfolgreich. Build, 19 gezielte Tests und Lint erfolgreich (15 bestehende Warnungen). Noch nicht veröffentlicht. - Löschen-Button in Queue-Cards um jeweils 3 px nach oben und rechts verschoben; Klickfläche bleibt 32 × 32 px. Build, fünf Stylesheet-Tests und isolierter Queue-UI-Test erfolgreich; Hell/Dunkel visuell geprüft. Noch nicht veröffentlicht. - Wartende Queue-Cards haben jetzt eine gelbe linke Statuslinie passend zum Wartepunkt. Der Wartet-Text bleibt ohne Umrandung oder gelblichen Hintergrund. Build, fünf Stylesheet-Tests und isolierter Queue-UI-Test erfolgreich; Linie in Hell/Dunkel visuell geprüft. Noch nicht veröffentlicht. diff --git a/scripts/smoke-test-queue-cards.js b/scripts/smoke-test-queue-cards.js index 49acdda..3d964be 100644 --- a/scripts/smoke-test-queue-cards.js +++ b/scripts/smoke-test-queue-cards.js @@ -24,7 +24,7 @@ async function main() { panel.dataset.vodsLayout = 'tabs'; panel.dataset.vodsWorkspace = 'queue'; queue = [ - { id: 'pending', status: 'pending', title: 'Ein langer Streamtitel mit gut lesbaren Download-Details und einer zweiten Zeile', progress: 0 }, + { id: 'pending', status: 'pending', title: 'Ein langer Streamtitel mit gut lesbaren Download-Details und einer zweiten Zeile', progress: 0, url: 'https://www.twitch.tv/videos/2863358704' }, { id: 'running', status: 'downloading', title: 'Sommerstream am See – gemeinsam unterwegs', progress: 42, progressStatus: 'Video wird heruntergeladen', speed: '12.5 MB/s', eta: '08:24' }, { id: 'paused', status: 'paused', title: 'Community-Abend mit Freunden', progress: 23 }, { id: 'error', status: 'error', title: 'Ein weiterer langer Streamtitel mit einer Fehlermeldung', progress: 10, last_error: 'Die Verbindung wurde unterbrochen. Bitte erneut versuchen.' }, @@ -102,8 +102,15 @@ async function main() { return measure(); }; const expanded = await animate(); + const url = item.querySelector('.queue-url-copy'); + const urlLabel = item.querySelector('.queue-url-row .queue-detail-label'); + const urlLayout = { + belowLabel: url.getBoundingClientRect().top >= urlLabel.getBoundingClientRect().bottom, + singleLine: getComputedStyle(url).whiteSpace === 'nowrap', + fullyVisible: url.scrollWidth <= url.clientWidth, + }; const collapsed = await animate(); - return { before, expanded, collapsed, samples }; + return { before, expanded, collapsed, samples, urlLayout }; } finally { [list, ...items].forEach((element, index) => { if (originalStyles[index] === null) element.removeAttribute('style'); @@ -112,6 +119,7 @@ async function main() { } }); assert(!expansionLayout.before.overflowing && expansionLayout.expanded.overflowing && !expansionLayout.collapsed.overflowing, `Expansion fixture crosses the scrollbar threshold (${theme}/${language}): ${JSON.stringify({ before: expansionLayout.before, expanded: expansionLayout.expanded, collapsed: expansionLayout.collapsed })}`); + assert(Object.values(expansionLayout.urlLayout).every(Boolean), `URL stays fully visible below its label (${theme}/${language}): ${JSON.stringify(expansionLayout.urlLayout)}`); for (const sample of [...expansionLayout.samples, expansionLayout.expanded, expansionLayout.collapsed]) { for (const dimension of ['left', 'width', 'height']) { assert(Math.abs(sample[dimension] - expansionLayout.before[dimension]) <= 0.5, `Title ${dimension} shifts during expansion (${theme}/${language})`); @@ -179,6 +187,24 @@ async function main() { }); assert.equal(await toggle.getAttribute('aria-expanded'), 'true', 'Expansion survives queue rerender'); assert.equal(await win.locator('[data-id="running"] .queue-progress-wrap').getAttribute('aria-valuenow'), '61'); + await win.evaluate(() => { + window.queueCopiedUrls = []; + Object.defineProperty(navigator.clipboard, 'writeText', { + configurable: true, + value: async (url) => { window.queueCopiedUrls.push(url); }, + }); + }); + const urlButton = card.locator('.queue-url-copy'); + await urlButton.click(); + await urlButton.press('Enter'); + await urlButton.press('Space'); + assert.deepEqual(await win.evaluate(() => window.queueCopiedUrls), Array(3).fill(fixtures[0].url)); + assert.equal(await toggle.getAttribute('aria-expanded'), 'true', 'Copying the URL keeps details open'); + assert(await win.evaluate(() => document.body.innerText.includes(UI_TEXT.queue.ctxCopiedUrl)), 'Copy confirmation is visible'); + for (const theme of ['twitch', 'light']) { + await win.evaluate((theme) => { document.body.className = `theme-${theme}`; }, theme); + await card.screenshot({ path: path.join(artifacts, `queue-url-${theme}.png`) }); + } await app.evaluate(({ ipcMain }) => { ipcMain.removeHandler('remove-from-queue'); ipcMain.handle('remove-from-queue', async (_event, id) => { diff --git a/src/renderer-queue.ts b/src/renderer-queue.ts index e4af696..798c2a2 100644 --- a/src/renderer-queue.ts +++ b/src/renderer-queue.ts @@ -94,6 +94,9 @@ async function invokeQueueItemAction(action: string, id: string): Promise await removeFromQueue(id); } else if (action === 'retry') { await retryQueueItem(id); + } else if (action === 'copy-url') { + const item = queue.find((candidate) => candidate.id === id); + if (item) await copyQueueUrl(item.url); } } @@ -728,7 +731,7 @@ function renderQueue(): void {
-
URL: ${escapeHtml(item.url)}
+
URL:
${escapeHtml(UI_TEXT.queue.detailStreamer)} ${escapeHtml(item.streamer)}
${escapeHtml(UI_TEXT.queue.detailDuration)} ${escapeHtml(item.duration_str)}
${escapeHtml(UI_TEXT.queue.detailDate)} ${escapeHtml(formatUiDateTime(item.date))}
diff --git a/src/style-modules.production-path.test.ts b/src/style-modules.production-path.test.ts index 6ebacbb..4705f08 100644 --- a/src/style-modules.production-path.test.ts +++ b/src/style-modules.production-path.test.ts @@ -20,7 +20,7 @@ describe('production style modules', () => { .replace(/\r\n/g, '\n')); const digest = createHash('sha256').update(content).digest('hex'); - expect(digest).toBe('a2626a94632641b7e6039d5a50165e34224a75a1d286db90ae9e22bdae72064e'); + expect(digest).toBe('89aee0dd2015cbf2890d9acfb4e5661f8520777f43fe48a86a89abc821982f4d'); }); test('derives the Windows hot-development executable version from package metadata', () => { diff --git a/src/styles-workflows.css b/src/styles-workflows.css index bd59ef2..32f6926 100644 --- a/src/styles-workflows.css +++ b/src/styles-workflows.css @@ -409,6 +409,34 @@ margin-bottom: 2px; } +.queue-url-copy { + display: block; + width: 100%; + min-width: 0; + min-height: 24px; + padding: 0; + border: 0; + background: transparent; + color: var(--text-secondary); + font: inherit; + font-size: 11px; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: copy; +} + +.queue-url-copy:hover { + color: var(--text); + text-decoration: underline; +} + +.queue-url-copy:focus-visible { + outline: 2px solid var(--accent); + outline-offset: -2px; +} + .queue-selection-order { position: absolute; z-index: 2;