fix: preserve Twitch display name casing in queue details
Windows CI / verify (push) Canceled after 0s
Windows CI / verify (push) Canceled after 0s
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Show the Twitch display name with its original capitalization in queue details and refresh it when profile names load.
|
||||||
- Put queue detail URLs on a separate single line; click or use the keyboard to copy the complete URL with confirmation.
|
- 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.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Aktueller veröffentlichter Stand: **1.0.20**, Tag `v1.0.20`, veröffentlicht am
|
|||||||
|
|
||||||
## Letzte Änderungen
|
## Letzte Änderungen
|
||||||
|
|
||||||
|
- Streamer in Queue-Details verwendet jetzt den aufgelösten Twitch-Anzeigenamen mit originaler Groß-/Kleinschreibung, etwa `xRohat` statt `xrohat`. Gespeicherter Login bleibt unverändert; noch unbekannte Anzeigenamen fallen auf den Login zurück. Nachgeladene Profilnamen aktualisieren vorhandene Queue-Cards sofort und werden bei der Render-Erkennung berücksichtigt. Build, 43 gezielte Tests, Queue-UI-Test einschließlich nachgeladener Groß-/Kleinschreibung und Lint erfolgreich (15 bestehende Warnungen). Noch nicht veröffentlicht.
|
||||||
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -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, url: 'https://www.twitch.tv/videos/2863358704' },
|
{ 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', streamer: 'xrohat' },
|
||||||
{ 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.' },
|
||||||
@@ -44,6 +44,10 @@ async function main() {
|
|||||||
queueSyncTimer = null;
|
queueSyncTimer = null;
|
||||||
});
|
});
|
||||||
assert.equal(await win.locator('#queueList .queue-item').count(), fixtures.length, 'Queue fixtures survive background synchronization');
|
assert.equal(await win.locator('#queueList .queue-item').count(), fixtures.length, 'Queue fixtures survive background synchronization');
|
||||||
|
assert.equal(await win.locator('[data-id="pending"] .queue-streamer-name').textContent(), 'xrohat', 'Unknown display names fall back to the login');
|
||||||
|
await win.evaluate(() => rememberStreamerDisplayName('XROHAT', 'xRohat'));
|
||||||
|
assert.equal(await win.locator('[data-id="pending"] .queue-streamer-name').textContent(), 'xRohat', 'Loaded display casing refreshes existing queue cards');
|
||||||
|
assert.equal(await win.evaluate(() => queue.find((item) => item.id === 'pending').streamer), 'xrohat', 'Display casing preserves the stored login');
|
||||||
for (const theme of ['twitch', 'light']) {
|
for (const theme of ['twitch', 'light']) {
|
||||||
for (const language of ['de', 'en']) {
|
for (const language of ['de', 'en']) {
|
||||||
await win.setViewportSize({ width: 1280, height: 900 });
|
await win.setViewportSize({ width: 1280, height: 900 });
|
||||||
|
|||||||
@@ -503,7 +503,7 @@ describe('renderer queue production paths', () => {
|
|||||||
const fingerprints = evaluate(
|
const fingerprints = evaluate(
|
||||||
fragment('function getQueueRenderFingerprint', 'function hasActiveQueueDuplicate'),
|
fragment('function getQueueRenderFingerprint', 'function hasActiveQueueDuplicate'),
|
||||||
'getQueueRenderFingerprint',
|
'getQueueRenderFingerprint',
|
||||||
{ currentLanguage: 'en', selectedQueueIds: [], expandedQueueIds: new Set<string>() }
|
{ currentLanguage: 'en', selectedQueueIds: [], expandedQueueIds: new Set<string>(), getStreamerDisplayName: (login: string) => login }
|
||||||
);
|
);
|
||||||
const base = { id: 'live-1', status: 'downloading', progress: 1, isLive: true };
|
const base = { id: 'live-1', status: 'downloading', progress: 1, isLive: true };
|
||||||
const unknown = fingerprints.exposed.getQueueRenderFingerprint([{ ...base, recordingHealth: 'unknown' }]) as string;
|
const unknown = fingerprints.exposed.getQueueRenderFingerprint([{ ...base, recordingHealth: 'unknown' }]) as string;
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ function getQueueRenderFingerprint(items: QueueItem[]): string {
|
|||||||
const lang = typeof currentLanguage === 'string' ? currentLanguage : 'en';
|
const lang = typeof currentLanguage === 'string' ? currentLanguage : 'en';
|
||||||
const pieces = items.map((item) => [
|
const pieces = items.map((item) => [
|
||||||
item.id,
|
item.id,
|
||||||
|
getStreamerDisplayName(item.streamer),
|
||||||
item.status,
|
item.status,
|
||||||
Math.round((Number(item.progress) || 0) * 10),
|
Math.round((Number(item.progress) || 0) * 10),
|
||||||
item.currentPart || 0,
|
item.currentPart || 0,
|
||||||
@@ -732,7 +733,7 @@ function renderQueue(): void {
|
|||||||
<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 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 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> <span class="queue-streamer-name">${escapeHtml(getStreamerDisplayName(item.streamer))}</span></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>
|
||||||
${renderQueueItemFileActions(item)}
|
${renderQueueItemFileActions(item)}
|
||||||
|
|||||||
@@ -417,6 +417,7 @@ describe('renderer streamer and VOD state regressions', () => {
|
|||||||
config: { streamers: ['nightbot'], streamer_display_names: { nightbot: 'NightBot' } },
|
config: { streamers: ['nightbot'], streamer_display_names: { nightbot: 'NightBot' } },
|
||||||
currentStreamer: 'nightbot',
|
currentStreamer: 'nightbot',
|
||||||
renderStreamers: vi.fn(),
|
renderStreamers: vi.fn(),
|
||||||
|
renderQueue: vi.fn(),
|
||||||
api: { getStreamerDisplayNames: () => new Promise<Record<string, string>>((resolve) => { resolveNames = resolve; }) },
|
api: { getStreamerDisplayNames: () => new Promise<Record<string, string>>((resolve) => { resolveNames = resolve; }) },
|
||||||
byId: (id: string) => elements.get(id) ?? element(),
|
byId: (id: string) => elements.get(id) ?? element(),
|
||||||
queryAll: () => [],
|
queryAll: () => [],
|
||||||
@@ -440,6 +441,7 @@ describe('renderer streamer and VOD state regressions', () => {
|
|||||||
expect(displayRuntime.exposed.getStreamerDisplayName('nightbot')).toBe('NightBot');
|
expect(displayRuntime.exposed.getStreamerDisplayName('nightbot')).toBe('NightBot');
|
||||||
expect((displayRuntime.window as Record<string, unknown>).getStreamerDisplayName).toBe(displayRuntime.exposed.getStreamerDisplayName);
|
expect((displayRuntime.window as Record<string, unknown>).getStreamerDisplayName).toBe(displayRuntime.exposed.getStreamerDisplayName);
|
||||||
expect(displayRuntime.renderStreamers).toHaveBeenCalledOnce();
|
expect(displayRuntime.renderStreamers).toHaveBeenCalledOnce();
|
||||||
|
expect(displayRuntime.renderQueue).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
const tabRuntime = evaluate(
|
const tabRuntime = evaluate(
|
||||||
fragment(rendererSource, 'function showTab', 'function parseDurationToSeconds'),
|
fragment(rendererSource, 'function showTab', 'function parseDurationToSeconds'),
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ function rememberStreamerDisplayName(login: string, displayName: string): void {
|
|||||||
const setTitle = (window as unknown as { setPageTitle?: (text: string) => void }).setPageTitle;
|
const setTitle = (window as unknown as { setPageTitle?: (text: string) => void }).setPageTitle;
|
||||||
if (typeof setTitle === 'function') setTitle(normalizedDisplayName);
|
if (typeof setTitle === 'function') setTitle(normalizedDisplayName);
|
||||||
}
|
}
|
||||||
if (changed) renderStreamers();
|
if (changed) {
|
||||||
|
renderStreamers();
|
||||||
|
renderQueue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(window as unknown as { rememberStreamerDisplayName: typeof rememberStreamerDisplayName }).rememberStreamerDisplayName = rememberStreamerDisplayName;
|
(window as unknown as { rememberStreamerDisplayName: typeof rememberStreamerDisplayName }).rememberStreamerDisplayName = rememberStreamerDisplayName;
|
||||||
@@ -56,6 +59,7 @@ function renderHydratedStreamerDisplayNames(): void {
|
|||||||
if (typeof setTitle === 'function') setTitle(getStreamerDisplayName(currentStreamer));
|
if (typeof setTitle === 'function') setTitle(getStreamerDisplayName(currentStreamer));
|
||||||
}
|
}
|
||||||
renderStreamers();
|
renderStreamers();
|
||||||
|
renderQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hydrateStreamerDisplayNames(): Promise<void> {
|
async function hydrateStreamerDisplayNames(): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user