mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(epg): keep the import panel free of horizontal scrolling (#1599)
This commit is contained in:
1 parent
17b8aa309d
commit
d9c8ba4d0b
26 files changed
+280
-40
No files matched your search
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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 محدث، لا حاجة للمزامنة",
|
||||
|
||||
@@ -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 محدث، ما خاصش مزامنة",
|
||||
|
||||
@@ -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 актуальны, сінхранізацыя не патрэбна",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 είναι ενημερωμένο, δεν απαιτείται συγχρονισμός",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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は最新です。同期は不要です",
|
||||
|
||||
@@ -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가 최신 상태이며 동기화가 필요하지 않습니다",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 актуален, синхронизация не требуется",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 已为最新,无需同步",
|
||||
|
||||
@@ -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 已是最新,無需同步",
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,16 @@
|
||||
{{ 'EPG.IMPORT_PROGRESS' | translate }}
|
||||
} @else {
|
||||
<span class="minimized-summary">
|
||||
{{ loadingCount() }} loading
|
||||
{{
|
||||
'EPG.IMPORT_SUMMARY_LOADING'
|
||||
| translate: { count: loadingCount() }
|
||||
}}
|
||||
@if (queuedCount() > 0) {
|
||||
, {{ queuedCount() }} queued
|
||||
,
|
||||
{{
|
||||
'EPG.IMPORT_SUMMARY_QUEUED'
|
||||
| translate: { count: queuedCount() }
|
||||
}}
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@@ -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
|
||||
"
|
||||
>
|
||||
<mat-icon>
|
||||
{{ minimized() ? 'unfold_more' : 'unfold_less' }}
|
||||
@@ -99,7 +117,7 @@
|
||||
}}"
|
||||
>
|
||||
<mat-icon>tv</mat-icon>
|
||||
{{ item.stats.totalChannels }}
|
||||
{{ formatCount(item.stats.totalChannels) }}
|
||||
</span>
|
||||
<span
|
||||
class="stat-badge"
|
||||
@@ -108,7 +126,7 @@
|
||||
}}"
|
||||
>
|
||||
<mat-icon>event</mat-icon>
|
||||
{{ item.stats.totalPrograms }}
|
||||
{{ formatCount(item.stats.totalPrograms) }}
|
||||
</span>
|
||||
} @else if (item.error) {
|
||||
<span class="error-text">{{ item.error }}</span>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user