This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Unreleased
|
## 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.
|
- 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.
|
- 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.
|
- Show a yellow left border on waiting queue cards, matching their status dot.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Aktueller veröffentlichter Stand: **1.0.20**, Tag `v1.0.20`, veröffentlicht am
|
|||||||
|
|
||||||
## Letzte Änderungen
|
## 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ async function main() {
|
|||||||
panel.dataset.vodsLayout = 'tabs';
|
panel.dataset.vodsLayout = 'tabs';
|
||||||
panel.dataset.vodsWorkspace = 'queue';
|
panel.dataset.vodsWorkspace = 'queue';
|
||||||
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: '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: '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.' },
|
{ 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();
|
return measure();
|
||||||
};
|
};
|
||||||
const expanded = await animate();
|
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();
|
const collapsed = await animate();
|
||||||
return { before, expanded, collapsed, samples };
|
return { before, expanded, collapsed, samples, urlLayout };
|
||||||
} finally {
|
} finally {
|
||||||
[list, ...items].forEach((element, index) => {
|
[list, ...items].forEach((element, index) => {
|
||||||
if (originalStyles[index] === null) element.removeAttribute('style');
|
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(!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 sample of [...expansionLayout.samples, expansionLayout.expanded, expansionLayout.collapsed]) {
|
||||||
for (const dimension of ['left', 'width', 'height']) {
|
for (const dimension of ['left', 'width', 'height']) {
|
||||||
assert(Math.abs(sample[dimension] - expansionLayout.before[dimension]) <= 0.5, `Title ${dimension} shifts during expansion (${theme}/${language})`);
|
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 toggle.getAttribute('aria-expanded'), 'true', 'Expansion survives queue rerender');
|
||||||
assert.equal(await win.locator('[data-id="running"] .queue-progress-wrap').getAttribute('aria-valuenow'), '61');
|
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 }) => {
|
await app.evaluate(({ ipcMain }) => {
|
||||||
ipcMain.removeHandler('remove-from-queue');
|
ipcMain.removeHandler('remove-from-queue');
|
||||||
ipcMain.handle('remove-from-queue', async (_event, id) => {
|
ipcMain.handle('remove-from-queue', async (_event, id) => {
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ async function invokeQueueItemAction(action: string, id: string): Promise<void>
|
|||||||
await removeFromQueue(id);
|
await removeFromQueue(id);
|
||||||
} else if (action === 'retry') {
|
} else if (action === 'retry') {
|
||||||
await retryQueueItem(id);
|
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 {
|
|||||||
</div>
|
</div>
|
||||||
<div class="queue-details${expandedQueueIds.has(item.id) ? ' expanded' : ''}" id="${detailsId}"${expandedQueueIds.has(item.id) ? '' : ' inert'}>
|
<div class="queue-details${expandedQueueIds.has(item.id) ? ' expanded' : ''}" id="${detailsId}"${expandedQueueIds.has(item.id) ? '' : ' inert'}>
|
||||||
<div class="queue-details-clip"><div class="queue-details-content">
|
<div class="queue-details-clip"><div class="queue-details-content">
|
||||||
<div><span class="queue-detail-label">URL:</span> ${escapeHtml(item.url)}</div>
|
<div class="queue-url-row"><span class="queue-detail-label">URL:</span><button class="queue-url-copy" type="button" data-queue-action="copy-url" aria-label="${escapeHtml(UI_TEXT.queue.ctxCopyUrl)}" title="${escapeHtml(UI_TEXT.queue.ctxCopyUrl)}: ${escapeHtml(item.url)}">${escapeHtml(item.url)}</button></div>
|
||||||
<div><span class="queue-detail-label">${escapeHtml(UI_TEXT.queue.detailStreamer)}</span> ${escapeHtml(item.streamer)}</div>
|
<div><span class="queue-detail-label">${escapeHtml(UI_TEXT.queue.detailStreamer)}</span> ${escapeHtml(item.streamer)}</div>
|
||||||
<div><span class="queue-detail-label">${escapeHtml(UI_TEXT.queue.detailDuration)}</span> ${escapeHtml(item.duration_str)}</div>
|
<div><span class="queue-detail-label">${escapeHtml(UI_TEXT.queue.detailDuration)}</span> ${escapeHtml(item.duration_str)}</div>
|
||||||
<div><span class="queue-detail-label">${escapeHtml(UI_TEXT.queue.detailDate)}</span> ${escapeHtml(formatUiDateTime(item.date))}</div>
|
<div><span class="queue-detail-label">${escapeHtml(UI_TEXT.queue.detailDate)}</span> ${escapeHtml(formatUiDateTime(item.date))}</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe('production style modules', () => {
|
|||||||
.replace(/\r\n/g, '\n'));
|
.replace(/\r\n/g, '\n'));
|
||||||
const digest = createHash('sha256').update(content).digest('hex');
|
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', () => {
|
test('derives the Windows hot-development executable version from package metadata', () => {
|
||||||
|
|||||||
@@ -409,6 +409,34 @@
|
|||||||
margin-bottom: 2px;
|
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 {
|
.queue-selection-order {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|||||||
Reference in New Issue
Block a user