From ec172a8762eea45aa4ba00b70c7934192d0fd7f1 Mon Sep 17 00:00:00 2001 From: Zain Date: Fri, 20 Mar 2026 17:38:22 +0800 Subject: [PATCH] feat: surface conflict and error info --- .../components/virtual-preview-list.ts | 54 +++++++++-- tests/unit/virtual-preview-list.test.ts | 89 +++++++++++++++++++ 2 files changed, 135 insertions(+), 8 deletions(-) diff --git a/src/content/components/virtual-preview-list.ts b/src/content/components/virtual-preview-list.ts index 53f41e6..ce987ec 100644 --- a/src/content/components/virtual-preview-list.ts +++ b/src/content/components/virtual-preview-list.ts @@ -74,6 +74,15 @@ export class VirtualPreviewList extends LitElement { * Render a single preview item * @private */ + private resolveErrorText(error?: string): string | undefined { + if (!error) return undefined; + + const translationKey = `error_${error}`; + const translated = I18nService.t(translationKey); + + return translated === translationKey ? error : translated; + } + private renderPreviewItem(item: PreviewItem): any { const statusClass = item.conflict ? 'conflict' @@ -95,19 +104,28 @@ export class VirtualPreviewList extends LitElement { ? html`${I18nService.t('status_pending')}` : ''; + const errorText = this.resolveErrorText(item.error); + const errorTitle = item.error ?? errorText ?? ''; + const conflictHint = item.conflict + ? html`${I18nService.t('error_api_conflict')}` + : ''; + return html`
-
- ${item.file.name} -
-
- ${item.newName} +
+
+ ${item.file.name} +
${statusBadge}
+
+ ${item.newName} + ${conflictHint} +
${ - item.error - ? html`
${item.error}
` + errorText + ? html`
${errorText}
` : '' }
@@ -232,10 +250,16 @@ export class VirtualPreviewList extends LitElement { min-width: 0; } + .old-name-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + } + .old-name { font-size: 12px; color: var(--cdr-text-secondary, #595959); - margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -253,6 +277,14 @@ export class VirtualPreviewList extends LitElement { gap: 8px; } + .new-name-text { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .new-name.conflict { color: var(--cdr-warning-text, #fa8c16); } @@ -269,6 +301,12 @@ export class VirtualPreviewList extends LitElement { color: var(--cdr-text-secondary, #595959); } + .conflict-hint { + font-size: 12px; + color: var(--cdr-warning-text, #fa8c16); + flex-shrink: 0; + } + .error-message { margin-top: 4px; font-size: 12px; diff --git a/tests/unit/virtual-preview-list.test.ts b/tests/unit/virtual-preview-list.test.ts index 0d08077..a89c337 100644 --- a/tests/unit/virtual-preview-list.test.ts +++ b/tests/unit/virtual-preview-list.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { VirtualPreviewList } from '../../src/content/components/virtual-preview-list'; +import { I18nService } from '../../src/utils/i18n'; describe('VirtualPreviewList pending preview mapping', () => { it('renders original name and new name for a pending preview item', async () => { @@ -29,4 +30,92 @@ describe('VirtualPreviewList pending preview mapping', () => { element.remove(); }); + + it('renders conflict items with status badge and helper text', async () => { + const element = new VirtualPreviewList(); + element.items = [ + { + file: { + id: 'file-conflict', + name: 'old-conflict.mkv', + ext: 'mkv', + size: 1, + mtime: Date.now(), + isDir: false, + }, + newName: 'new-conflict.mkv', + conflict: true, + done: false, + }, + ]; + + document.body.appendChild(element); + await element.updateComplete; + + const textContent = element.shadowRoot?.textContent ?? ''; + expect(textContent).toContain('old-conflict.mkv'); + expect(textContent).toContain('new-conflict.mkv'); + expect(textContent).toContain(I18nService.t('preview_badge_conflict')); + expect(textContent).toContain(I18nService.t('error_api_conflict')); + + const badge = element.shadowRoot?.querySelector('.old-name-row .status-badge.conflict'); + expect(badge).toBeTruthy(); + + element.remove(); + }); + + it('shows localized extract_episode_not_found message instead of raw error key', async () => { + const element = new VirtualPreviewList(); + element.items = [ + { + file: { + id: 'file-error', + name: 'old-error.mkv', + ext: 'mkv', + size: 1, + mtime: Date.now(), + isDir: false, + }, + newName: 'new-error.mkv', + conflict: false, + error: 'extract_episode_not_found', + }, + ]; + + document.body.appendChild(element); + await element.updateComplete; + + const textContent = element.shadowRoot?.textContent ?? ''; + expect(textContent).toContain(I18nService.t('error_extract_episode_not_found')); + + element.remove(); + }); + + it('keeps pending badge next to old name row when showStatus is true', async () => { + const element = new VirtualPreviewList(); + element.showStatus = true; + element.items = [ + { + file: { + id: 'file-pending', + name: 'old-pending.mkv', + ext: 'mkv', + size: 1, + mtime: Date.now(), + isDir: false, + }, + newName: 'new-pending.mkv', + conflict: false, + done: false, + }, + ]; + + document.body.appendChild(element); + await element.updateComplete; + + const badge = element.shadowRoot?.querySelector('.old-name-row .status-badge.pending'); + expect(badge).toBeTruthy(); + + element.remove(); + }); });