From d9c8ba4d0bd442d0056c611da9b37dbfafaa03b0 Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:25:53 +0200 Subject: [PATCH] fix(epg): keep the import panel free of horizontal scrolling (#1599) --- .changes/epg-import-panel-polish.md | 6 ++ apps/electron-backend-e2e/src/epg.e2e.ts | 19 +++++ apps/web/src/assets/i18n/ar.json | 4 + apps/web/src/assets/i18n/ary.json | 4 + apps/web/src/assets/i18n/by.json | 4 + apps/web/src/assets/i18n/de.json | 4 + apps/web/src/assets/i18n/el.json | 4 + apps/web/src/assets/i18n/en.json | 4 + apps/web/src/assets/i18n/es.json | 4 + apps/web/src/assets/i18n/fr.json | 4 + apps/web/src/assets/i18n/hu.json | 4 + apps/web/src/assets/i18n/it.json | 4 + apps/web/src/assets/i18n/ja.json | 4 + apps/web/src/assets/i18n/ko.json | 4 + apps/web/src/assets/i18n/nl.json | 4 + apps/web/src/assets/i18n/pl.json | 4 + apps/web/src/assets/i18n/pt.json | 4 + apps/web/src/assets/i18n/ru.json | 4 + apps/web/src/assets/i18n/tr.json | 4 + apps/web/src/assets/i18n/zh.json | 4 + apps/web/src/assets/i18n/zhtw.json | 4 + .../epg-import-display-url.spec.ts | 53 +++++++++++++ .../epg-import-display-url.ts | 48 ++++++++++++ .../epg-progress-panel.component.html | 28 +++++-- .../epg-progress-panel.component.scss | 74 +++++++++++-------- .../epg-progress-panel.component.ts | 16 +++- 26 files changed, 280 insertions(+), 40 deletions(-) create mode 100644 .changes/epg-import-panel-polish.md create mode 100644 libs/ui/epg/src/lib/epg-progress-panel/epg-import-display-url.spec.ts create mode 100644 libs/ui/epg/src/lib/epg-progress-panel/epg-import-display-url.ts diff --git a/.changes/epg-import-panel-polish.md b/.changes/epg-import-panel-polish.md new file mode 100644 index 000000000..7e102563d --- /dev/null +++ b/.changes/epg-import-panel-polish.md @@ -0,0 +1,6 @@ +--- +type: fix +area: epg +--- + +The EPG import panel no longer shows a horizontal scrollbar while sources download. Source names now read as host/file instead of running the two together, channel and programme counts use your locale's digit grouping, and the minimize button and summary are translated. diff --git a/apps/electron-backend-e2e/src/epg.e2e.ts b/apps/electron-backend-e2e/src/epg.e2e.ts index 6fa53db09..aacfd7ccb 100644 --- a/apps/electron-backend-e2e/src/epg.e2e.ts +++ b/apps/electron-backend-e2e/src/epg.e2e.ts @@ -334,6 +334,25 @@ test.describe('Electron EPG', () => { .first() ).toBeVisible(); + // The row must fit the 300px panel: Material icon buttons carry a + // 48px touch target that used to poke past the row padding and + // turn into a horizontal scrollbar. + const importsList = app.mainWindow.locator( + '.epg-progress-panel .imports-list' + ); + await expect + .poll(() => + importsList.evaluate( + (element) => element.scrollWidth - element.clientWidth + ) + ) + .toBe(0); + await expect( + app.mainWindow.locator('.epg-progress-panel .item-url') + ).toHaveText( + `${new URL(epgServer.resourceUrl).hostname}/guide.xml` + ); + await saveSettings(app.mainWindow); await app.mainWindow diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index c6c699add..bfce7ab2c 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -857,6 +857,10 @@ "PROGRAMS": "برامج", "LOADING": "جارٍ التحميل", "QUEUED": "في قائمة الانتظار", + "IMPORT_PANEL_MINIMIZE": "تصغير لوحة استيراد دليل البرامج", + "IMPORT_PANEL_EXPAND": "توسيع لوحة استيراد دليل البرامج", + "IMPORT_SUMMARY_LOADING": "{{count}} قيد التحميل", + "IMPORT_SUMMARY_QUEUED": "{{count}} في قائمة الانتظار", "TOTAL_CHANNELS": "إجمالي القنوات", "TOTAL_PROGRAMS": "إجمالي البرامج", "UP_TO_DATE": "EPG محدث، لا حاجة للمزامنة", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index 3ea8f4604..034be9e4c 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -857,6 +857,10 @@ "PROGRAMS": "برامج", "LOADING": "جاري التشريج", "QUEUED": "في الطابور", + "IMPORT_PANEL_MINIMIZE": "صغّر لوحة استيراد دليل البرامج", + "IMPORT_PANEL_EXPAND": "كبّر لوحة استيراد دليل البرامج", + "IMPORT_SUMMARY_LOADING": "{{count}} كيتحمّل", + "IMPORT_SUMMARY_QUEUED": "{{count}} فالصف", "TOTAL_CHANNELS": "مجموع القنوات", "TOTAL_PROGRAMS": "مجموع البرامج", "UP_TO_DATE": "EPG محدث، ما خاصش مزامنة", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index c15c7d871..fd7656417 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -857,6 +857,10 @@ "PROGRAMS": "перадач", "LOADING": "Загрузка", "QUEUED": "У чарзе", + "IMPORT_PANEL_MINIMIZE": "Згарнуць панэль імпарту EPG", + "IMPORT_PANEL_EXPAND": "Разгарнуць панэль імпарту EPG", + "IMPORT_SUMMARY_LOADING": "Загружаецца: {{count}}", + "IMPORT_SUMMARY_QUEUED": "у чарзе: {{count}}", "TOTAL_CHANNELS": "Усяго каналаў", "TOTAL_PROGRAMS": "Усяго перадач", "UP_TO_DATE": "EPG актуальны, сінхранізацыя не патрэбна", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index 418a73e69..5c196d68e 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -857,6 +857,10 @@ "PROGRAMS": "Programme", "LOADING": "Wird geladen", "QUEUED": "In Warteschlange", + "IMPORT_PANEL_MINIMIZE": "EPG-Importpanel minimieren", + "IMPORT_PANEL_EXPAND": "EPG-Importpanel ausklappen", + "IMPORT_SUMMARY_LOADING": "{{count}} wird geladen", + "IMPORT_SUMMARY_QUEUED": "{{count}} in Warteschlange", "TOTAL_CHANNELS": "Sender insgesamt", "TOTAL_PROGRAMS": "Programme insgesamt", "UP_TO_DATE": "EPG ist aktuell, keine Synchronisation nötig", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index ad8d6501d..5e88b86d2 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -857,6 +857,10 @@ "PROGRAMS": "προγράμματα", "LOADING": "Φόρτωση", "QUEUED": "Σε ουρά", + "IMPORT_PANEL_MINIMIZE": "Ελαχιστοποίηση πίνακα εισαγωγής EPG", + "IMPORT_PANEL_EXPAND": "Ανάπτυξη πίνακα εισαγωγής EPG", + "IMPORT_SUMMARY_LOADING": "{{count}} σε φόρτωση", + "IMPORT_SUMMARY_QUEUED": "{{count}} σε αναμονή", "TOTAL_CHANNELS": "Σύνολο καναλιών", "TOTAL_PROGRAMS": "Σύνολο προγραμμάτων", "UP_TO_DATE": "Το EPG είναι ενημερωμένο, δεν απαιτείται συγχρονισμός", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index e95bb7c70..fd207686b 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -857,6 +857,10 @@ "PROGRAMS": "programs", "LOADING": "Loading", "QUEUED": "Queued", + "IMPORT_PANEL_MINIMIZE": "Minimize EPG import panel", + "IMPORT_PANEL_EXPAND": "Expand EPG import panel", + "IMPORT_SUMMARY_LOADING": "{{count}} loading", + "IMPORT_SUMMARY_QUEUED": "{{count}} queued", "TOTAL_CHANNELS": "Total channels", "TOTAL_PROGRAMS": "Total programs", "UP_TO_DATE": "EPG is up-to-date, no sync needed", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index 028e86667..307268b04 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -857,6 +857,10 @@ "PROGRAMS": "programas", "LOADING": "Cargando", "QUEUED": "En cola", + "IMPORT_PANEL_MINIMIZE": "Minimizar el panel de importación de EPG", + "IMPORT_PANEL_EXPAND": "Expandir el panel de importación de EPG", + "IMPORT_SUMMARY_LOADING": "{{count}} cargando", + "IMPORT_SUMMARY_QUEUED": "{{count}} en cola", "TOTAL_CHANNELS": "Total de canales", "TOTAL_PROGRAMS": "Total de programas", "UP_TO_DATE": "EPG actualizada, no se necesita sincronización", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index e9975c103..2732734d4 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -857,6 +857,10 @@ "PROGRAMS": "programmes", "LOADING": "Chargement", "QUEUED": "En file d'attente", + "IMPORT_PANEL_MINIMIZE": "Réduire le panneau d’import EPG", + "IMPORT_PANEL_EXPAND": "Agrandir le panneau d’import EPG", + "IMPORT_SUMMARY_LOADING": "{{count}} en cours", + "IMPORT_SUMMARY_QUEUED": "{{count}} en attente", "TOTAL_CHANNELS": "Total des chaînes", "TOTAL_PROGRAMS": "Total des programmes", "UP_TO_DATE": "L'EPG est à jour, aucune synchronisation nécessaire", diff --git a/apps/web/src/assets/i18n/hu.json b/apps/web/src/assets/i18n/hu.json index 9b76312fe..a85807d0a 100644 --- a/apps/web/src/assets/i18n/hu.json +++ b/apps/web/src/assets/i18n/hu.json @@ -857,6 +857,10 @@ "PROGRAMS": "műsor", "LOADING": "Betöltés", "QUEUED": "Várólistán", + "IMPORT_PANEL_MINIMIZE": "EPG-importálási panel összecsukása", + "IMPORT_PANEL_EXPAND": "EPG-importálási panel kibontása", + "IMPORT_SUMMARY_LOADING": "{{count}} betöltés alatt", + "IMPORT_SUMMARY_QUEUED": "{{count}} várakozik", "TOTAL_CHANNELS": "Csatornák összesen", "TOTAL_PROGRAMS": "Műsorok összesen", "UP_TO_DATE": "Az EPG naprakész, nincs szükség szinkronizálásra", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index 543494654..fc277a738 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -857,6 +857,10 @@ "PROGRAMS": "programmi", "LOADING": "Caricamento", "QUEUED": "In coda", + "IMPORT_PANEL_MINIMIZE": "Riduci il pannello di importazione EPG", + "IMPORT_PANEL_EXPAND": "Espandi il pannello di importazione EPG", + "IMPORT_SUMMARY_LOADING": "{{count}} in caricamento", + "IMPORT_SUMMARY_QUEUED": "{{count}} in coda", "TOTAL_CHANNELS": "Totale canali", "TOTAL_PROGRAMS": "Totale programmi", "UP_TO_DATE": "L'EPG è aggiornato, nessuna sincronizzazione necessaria", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index 42d98c2f9..b25ea4953 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -857,6 +857,10 @@ "PROGRAMS": "番組", "LOADING": "読み込み中", "QUEUED": "待機中", + "IMPORT_PANEL_MINIMIZE": "EPG インポートパネルを最小化", + "IMPORT_PANEL_EXPAND": "EPG インポートパネルを展開", + "IMPORT_SUMMARY_LOADING": "{{count}} 件読み込み中", + "IMPORT_SUMMARY_QUEUED": "{{count}} 件待機中", "TOTAL_CHANNELS": "総チャンネル数", "TOTAL_PROGRAMS": "総番組数", "UP_TO_DATE": "EPGは最新です。同期は不要です", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index d3d472070..fd330bd2c 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -857,6 +857,10 @@ "PROGRAMS": "프로그램", "LOADING": "불러오는 중", "QUEUED": "대기 중", + "IMPORT_PANEL_MINIMIZE": "EPG 가져오기 패널 최소화", + "IMPORT_PANEL_EXPAND": "EPG 가져오기 패널 펼치기", + "IMPORT_SUMMARY_LOADING": "{{count}}개 불러오는 중", + "IMPORT_SUMMARY_QUEUED": "{{count}}개 대기 중", "TOTAL_CHANNELS": "전체 채널", "TOTAL_PROGRAMS": "전체 프로그램", "UP_TO_DATE": "EPG가 최신 상태이며 동기화가 필요하지 않습니다", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index 11b4d7ef7..3f59c18d2 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -857,6 +857,10 @@ "PROGRAMS": "programma's", "LOADING": "Laden", "QUEUED": "In wachtrij", + "IMPORT_PANEL_MINIMIZE": "EPG-importpaneel minimaliseren", + "IMPORT_PANEL_EXPAND": "EPG-importpaneel uitvouwen", + "IMPORT_SUMMARY_LOADING": "{{count}} wordt geladen", + "IMPORT_SUMMARY_QUEUED": "{{count}} in de wachtrij", "TOTAL_CHANNELS": "Totaal aantal kanalen", "TOTAL_PROGRAMS": "Totaal aantal programma's", "UP_TO_DATE": "EPG is up-to-date, synchronisatie niet nodig", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index 213a834e9..79d769a46 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -857,6 +857,10 @@ "PROGRAMS": "programów", "LOADING": "Ładowanie", "QUEUED": "W kolejce", + "IMPORT_PANEL_MINIMIZE": "Zwiń panel importu EPG", + "IMPORT_PANEL_EXPAND": "Rozwiń panel importu EPG", + "IMPORT_SUMMARY_LOADING": "Wczytywanie: {{count}}", + "IMPORT_SUMMARY_QUEUED": "w kolejce: {{count}}", "TOTAL_CHANNELS": "Wszystkie kanały", "TOTAL_PROGRAMS": "Wszystkie programy", "UP_TO_DATE": "EPG jest aktualne, synchronizacja nie jest potrzebna", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index 6b3702806..89e2c286f 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -857,6 +857,10 @@ "PROGRAMS": "programas", "LOADING": "Carregando", "QUEUED": "Na fila", + "IMPORT_PANEL_MINIMIZE": "Minimizar o painel de importação do EPG", + "IMPORT_PANEL_EXPAND": "Expandir o painel de importação do EPG", + "IMPORT_SUMMARY_LOADING": "{{count}} a carregar", + "IMPORT_SUMMARY_QUEUED": "{{count}} na fila", "TOTAL_CHANNELS": "Total de canais", "TOTAL_PROGRAMS": "Total de programas", "UP_TO_DATE": "EPG está atualizado, sincronização não é necessária", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index 71956f8a8..668bdd0ce 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -857,6 +857,10 @@ "PROGRAMS": "программ", "LOADING": "Загрузка", "QUEUED": "В очереди", + "IMPORT_PANEL_MINIMIZE": "Свернуть панель импорта EPG", + "IMPORT_PANEL_EXPAND": "Развернуть панель импорта EPG", + "IMPORT_SUMMARY_LOADING": "Загружается: {{count}}", + "IMPORT_SUMMARY_QUEUED": "в очереди: {{count}}", "TOTAL_CHANNELS": "Всего каналов", "TOTAL_PROGRAMS": "Всего программ", "UP_TO_DATE": "EPG актуален, синхронизация не требуется", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index 2e2e4a90c..6ceb0db40 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -857,6 +857,10 @@ "PROGRAMS": "programlar", "LOADING": "Yükleniyor", "QUEUED": "Sırada", + "IMPORT_PANEL_MINIMIZE": "EPG içe aktarma panelini küçült", + "IMPORT_PANEL_EXPAND": "EPG içe aktarma panelini genişlet", + "IMPORT_SUMMARY_LOADING": "{{count}} yükleniyor", + "IMPORT_SUMMARY_QUEUED": "{{count}} sırada", "TOTAL_CHANNELS": "Toplam kanal", "TOTAL_PROGRAMS": "Toplam program", "UP_TO_DATE": "EPG güncel, senkronizasyona gerek yok", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index f3dc37731..241da46f5 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -857,6 +857,10 @@ "PROGRAMS": "节目", "LOADING": "加载中", "QUEUED": "排队中", + "IMPORT_PANEL_MINIMIZE": "最小化 EPG 导入面板", + "IMPORT_PANEL_EXPAND": "展开 EPG 导入面板", + "IMPORT_SUMMARY_LOADING": "{{count}} 个加载中", + "IMPORT_SUMMARY_QUEUED": "{{count}} 个排队中", "TOTAL_CHANNELS": "频道总数", "TOTAL_PROGRAMS": "节目总数", "UP_TO_DATE": "EPG 已为最新,无需同步", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 2c2ea6e9a..267d161ba 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -857,6 +857,10 @@ "PROGRAMS": "節目", "LOADING": "載入中", "QUEUED": "佇列中", + "IMPORT_PANEL_MINIMIZE": "最小化 EPG 匯入面板", + "IMPORT_PANEL_EXPAND": "展開 EPG 匯入面板", + "IMPORT_SUMMARY_LOADING": "{{count}} 個載入中", + "IMPORT_SUMMARY_QUEUED": "{{count}} 個排隊中", "TOTAL_CHANNELS": "頻道總數", "TOTAL_PROGRAMS": "節目總數", "UP_TO_DATE": "EPG 已是最新,無需同步", diff --git a/libs/ui/epg/src/lib/epg-progress-panel/epg-import-display-url.spec.ts b/libs/ui/epg/src/lib/epg-progress-panel/epg-import-display-url.spec.ts new file mode 100644 index 000000000..465ece4f6 --- /dev/null +++ b/libs/ui/epg/src/lib/epg-progress-panel/epg-import-display-url.spec.ts @@ -0,0 +1,53 @@ +import { formatEpgImportDisplayUrl } from './epg-import-display-url'; + +describe('formatEpgImportDisplayUrl', () => { + it('separates the host from the file name', () => { + expect( + formatEpgImportDisplayUrl('http://epg.it999.ru/edem.xml.gz') + ).toBe('epg.it999.ru/edem.xml.gz'); + }); + + it('collapses intermediate path segments', () => { + expect( + formatEpgImportDisplayUrl( + 'https://iptvx.one/static/epg/epg_lite.xml.gz?token=secret' + ) + ).toBe('iptvx.one/…/epg_lite.xml.gz'); + }); + + it('decodes a percent-encoded remote file name', () => { + expect( + formatEpgImportDisplayUrl('https://example.org/guide%20v2.xml') + ).toBe('example.org/guide v2.xml'); + expect( + formatEpgImportDisplayUrl('https://example.org/epg/guide%ZZ.xml') + ).toBe('example.org/…/guide%ZZ.xml'); + }); + + it('falls back to the host when the path has no file name', () => { + expect(formatEpgImportDisplayUrl('https://example.org/')).toBe( + 'example.org' + ); + expect(formatEpgImportDisplayUrl('https://example.org')).toBe( + 'example.org' + ); + }); + + it('shows the file name of a local file URL or path', () => { + expect( + formatEpgImportDisplayUrl('file:///home/user/epg/guide%20v2.xml.gz') + ).toBe('…/guide v2.xml.gz'); + expect(formatEpgImportDisplayUrl('/home/user/epg/guide.xml')).toBe( + '…/guide.xml' + ); + expect(formatEpgImportDisplayUrl('C:\\epg\\guide.xml.gz')).toBe( + '…/guide.xml.gz' + ); + }); + + it('truncates unparsable values instead of throwing', () => { + const long = 'x'.repeat(60); + expect(formatEpgImportDisplayUrl(long)).toBe(`${'x'.repeat(40)}…`); + expect(formatEpgImportDisplayUrl('short')).toBe('short'); + }); +}); diff --git a/libs/ui/epg/src/lib/epg-progress-panel/epg-import-display-url.ts b/libs/ui/epg/src/lib/epg-progress-panel/epg-import-display-url.ts new file mode 100644 index 000000000..fb55a3f84 --- /dev/null +++ b/libs/ui/epg/src/lib/epg-progress-panel/epg-import-display-url.ts @@ -0,0 +1,48 @@ +const MAX_PLAIN_LENGTH = 40; + +/** + * Compact single-line label for an EPG source: the host, an ellipsis for + * any intermediate path, and the file name — `host/…/file.xml.gz`. A local + * file (a `file:` URL or a filesystem path) shows `…/file.xml.gz`, since the + * host slot would be empty. Query strings and credentials are never + * rendered; the full value belongs in the tooltip only. + */ +export function formatEpgImportDisplayUrl(url: string): string { + let parsed: URL | null = null; + try { + parsed = new URL(url); + } catch { + parsed = null; + } + + if (parsed && parsed.protocol !== 'file:' && parsed.hostname) { + const segments = parsed.pathname.split('/').filter(Boolean); + const file = segments[segments.length - 1]; + if (!file) { + return parsed.hostname; + } + const name = safeDecode(file); + return segments.length > 1 + ? `${parsed.hostname}/…/${name}` + : `${parsed.hostname}/${name}`; + } + + const localPath = parsed?.protocol === 'file:' ? parsed.pathname : url; + const segments = localPath.split(/[\\/]/).filter(Boolean); + const file = segments[segments.length - 1]; + if (file && segments.length > 1) { + return `…/${safeDecode(file)}`; + } + + return url.length > MAX_PLAIN_LENGTH + ? `${url.substring(0, MAX_PLAIN_LENGTH)}…` + : url; +} + +function safeDecode(value: string): string { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} diff --git a/libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.html b/libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.html index a767ffc1d..5f8e2f8d0 100644 --- a/libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.html +++ b/libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.html @@ -10,9 +10,16 @@ {{ 'EPG.IMPORT_PROGRESS' | translate }} } @else { - {{ loadingCount() }} loading + {{ + 'EPG.IMPORT_SUMMARY_LOADING' + | translate: { count: loadingCount() } + }} @if (queuedCount() > 0) { - , {{ queuedCount() }} queued + , + {{ + 'EPG.IMPORT_SUMMARY_QUEUED' + | translate: { count: queuedCount() } + }} } } @@ -22,7 +29,18 @@ mat-icon-button class="minimize-btn" (click)="toggleMinimize()" - [attr.aria-label]="minimized() ? 'Expand' : 'Minimize'" + [attr.aria-label]=" + (minimized() + ? 'EPG.IMPORT_PANEL_EXPAND' + : 'EPG.IMPORT_PANEL_MINIMIZE' + ) | translate + " + [matTooltip]=" + (minimized() + ? 'EPG.IMPORT_PANEL_EXPAND' + : 'EPG.IMPORT_PANEL_MINIMIZE' + ) | translate + " > {{ minimized() ? 'unfold_more' : 'unfold_less' }} @@ -99,7 +117,7 @@ }}" > tv - {{ item.stats.totalChannels }} + {{ formatCount(item.stats.totalChannels) }} event - {{ item.stats.totalPrograms }} + {{ formatCount(item.stats.totalPrograms) }} } @else if (item.error) { {{ item.error }} diff --git a/libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.scss b/libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.scss index 61914a45c..66c053a5f 100644 --- a/libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.scss +++ b/libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.scss @@ -20,6 +20,34 @@ --epg-queued: #a78bfa; } +// Material icon buttons keep a 48px absolutely positioned touch target and +// a 48px state layer regardless of the width/height they are given. Inside +// the scrollable list that 48px box pokes past the row padding and turns +// into a horizontal scrollbar. Compact buttons shrink the state layer to the +// visible size and keep a touch target that is still larger than the button +// but fits inside the row padding (6px on each side, the tightest row has +// 8px), so touch and motor-impaired users keep a forgiving hit area. +@mixin compact-icon-button($size, $icon-size) { + --mdc-icon-button-state-layer-size: #{$size}; + --mdc-icon-button-icon-size: #{$icon-size}; + width: $size; + height: $size; + padding: 0; + line-height: $size; + flex-shrink: 0; + + ::ng-deep .mat-mdc-button-touch-target { + width: $size + 12px; + height: $size + 12px; + } + + mat-icon { + font-size: $icon-size; + width: $icon-size; + height: $icon-size; + } +} + .epg-progress-panel { position: fixed; bottom: 20px; @@ -90,18 +118,9 @@ .minimize-btn, .dismiss-all-btn { - width: 28px; - height: 28px; - padding: 0; - line-height: 28px; + @include compact-icon-button(28px, 18px); border-radius: 8px; color: var(--epg-text-tertiary); - - mat-icon { - font-size: 18px; - width: 18px; - height: 18px; - } } } @@ -117,7 +136,9 @@ .imports-list { max-height: 320px; - overflow-y: auto; + // Never scroll sideways: every row truncates or wraps instead. + overflow: hidden auto; + overscroll-behavior: contain; padding: 4px 0; scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--app-muted-color) 55%, transparent) @@ -152,6 +173,7 @@ .item-url { flex: 1; + min-width: 0; font-size: 0.78rem; font-weight: 500; white-space: nowrap; @@ -162,22 +184,13 @@ .dismiss-btn, .retry-btn { - width: 24px; - height: 24px; - padding: 0; - line-height: 24px; + @include compact-icon-button(24px, 16px); border-radius: 6px; opacity: 0; transition: opacity 0.15s ease, color 0.15s ease; color: var(--epg-text-tertiary); - - mat-icon { - font-size: 16px; - width: 16px; - height: 16px; - } } // Retry is a deliberate action — keep it discoverable, not hover-only, @@ -207,7 +220,8 @@ } } - &:hover .dismiss-btn { + &:hover .dismiss-btn, + .dismiss-btn:focus-visible { opacity: 1; } @@ -260,8 +274,11 @@ } .error-text { + min-width: 0; font-size: 0.72rem; + line-height: 1.35; color: var(--epg-error); + overflow-wrap: anywhere; } } @@ -322,6 +339,7 @@ .queue-url { flex: 1; + min-width: 0; font-size: 0.72rem; white-space: nowrap; overflow: hidden; @@ -329,17 +347,9 @@ } .dismiss-btn { - width: 22px; - height: 22px; - padding: 0; - line-height: 22px; + @include compact-icon-button(22px, 14px); + border-radius: 6px; color: var(--epg-text-tertiary); - - mat-icon { - font-size: 14px; - width: 14px; - height: 14px; - } } } diff --git a/libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.ts b/libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.ts index b83619288..37c55b5ee 100644 --- a/libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.ts +++ b/libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.ts @@ -14,6 +14,8 @@ import { EpgProgressService, } from '@iptvnator/epg/data-access'; import { ELECTRON_BRIDGE_SECURITY_ERROR_CODES } from '@iptvnator/shared/interfaces'; +import { normalizeDateLocale } from '@iptvnator/pipes'; +import { formatEpgImportDisplayUrl } from './epg-import-display-url'; interface EpgTrustConfirmDialogData { confirmLabel: string; @@ -106,11 +108,19 @@ export class EpgProgressPanelComponent { } getDisplayUrl(url: string): string { + return formatEpgImportDisplayUrl(url); + } + + formatCount(value: number): string { + // App language codes are not all BCP 47 tags (`zhtw`, `ary`, `by`); + // the date pipes already map those aliases for Intl. + const locale = normalizeDateLocale( + this.translate.currentLang || this.translate.defaultLang + ); try { - const urlObject = new URL(url); - return urlObject.hostname + urlObject.pathname.split('/').pop(); + return new Intl.NumberFormat(locale).format(value); } catch { - return url.length > 40 ? `${url.substring(0, 40)}...` : url; + return String(value); } }