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);
}
}