style(settings): add EPG sources handling and UI enhancements

Entire-Checkpoint: c6e522b4276c
This commit is contained in:
4gray committed 2026-04-25 22:39:55 +02:00
1 parent 4616df5d16
commit e53a25f441
21 files changed
+265 -82

No files matched your search

+103 -65
View File
@@ -232,7 +232,7 @@
formControlName="player"
data-test-id="select-video-player"
>
@for (player of players; track player) {
@for (player of players(); track player.id) {
<mat-option
[value]="player.id"
[attr.data-test-id]="player.id"
@@ -419,70 +419,96 @@
<div class="setting-item setting-item--full-control">
<div class="setting-item__control">
<ng-container formArrayName="epgUrl">
<div class="epg-sources">
@for (
_ of epgUrl.controls;
track _;
let i = $index
) {
<div class="epg-source-row">
<mat-form-field
appearance="outline"
subscriptSizing="dynamic"
class="w-full"
>
<mat-label>{{
'SETTINGS.EPG_URL_PLACEHOLDER'
| translate
}}</mat-label>
<input
matInput
type="url"
[formControlName]="i"
#epgField
/>
<mat-error>{{
'SETTINGS.EPG_URL_ERROR'
| translate
}}</mat-error>
</mat-form-field>
@if (epgField.value) {
<app-epg-source-status
[url]="epgField.value"
/>
}
<button
mat-icon-button
color="accent"
[matTooltip]="
'SETTINGS.REFRESH_EPG'
| translate
"
type="button"
[disabled]="
epgField.value === ''
"
(click)="
refreshEpg(epgUrl.value[i])
"
>
<mat-icon>refresh</mat-icon>
</button>
<button
mat-icon-button
color="accent"
[matTooltip]="
'SETTINGS.REMOVE_EPG_SOURCE'
| translate
"
type="button"
(click)="removeEpgSource(i)"
>
<mat-icon>remove</mat-icon>
</button>
</div>
}
</div>
@if (epgUrl.length === 0) {
<div
class="epg-sources-empty"
data-test-id="epg-sources-empty"
>
<mat-icon
class="epg-sources-empty__icon"
>event_busy</mat-icon
>
<h4 class="epg-sources-empty__title">
{{
'SETTINGS.EPG_EMPTY_TITLE'
| translate
}}
</h4>
<p class="epg-sources-empty__hint">
{{
'SETTINGS.EPG_EMPTY_HINT'
| translate
}}
</p>
</div>
} @else {
<div class="epg-sources">
@for (
_ of epgUrl.controls;
track _;
let i = $index
) {
<div class="epg-source-row">
<mat-form-field
appearance="outline"
subscriptSizing="dynamic"
class="w-full"
>
<mat-label>{{
'SETTINGS.EPG_URL_PLACEHOLDER'
| translate
}}</mat-label>
<input
matInput
type="url"
[formControlName]="i"
#epgField
/>
<mat-error>{{
'SETTINGS.EPG_URL_ERROR'
| translate
}}</mat-error>
</mat-form-field>
@if (epgField.value) {
<app-epg-source-status
[url]="epgField.value"
/>
}
<button
mat-icon-button
color="accent"
[matTooltip]="
'SETTINGS.REFRESH_EPG'
| translate
"
type="button"
[disabled]="
epgField.value === ''
"
(click)="
refreshEpg(
epgUrl.value[i]
)
"
>
<mat-icon>refresh</mat-icon>
</button>
<button
mat-icon-button
color="accent"
[matTooltip]="
'SETTINGS.REMOVE_EPG_SOURCE'
| translate
"
type="button"
(click)="removeEpgSource(i)"
>
<mat-icon>remove</mat-icon>
</button>
</div>
}
</div>
}
</ng-container>
</div>
</div>
@@ -909,6 +935,18 @@
</div>
</div>
@if (isDesktop) {
<div class="settings-note">
<strong>{{
'SETTINGS.EMBEDDED_MPV_NOTICE_TITLE' | translate
}}</strong>
<span>{{
'SETTINGS.EMBEDDED_MPV_NOTICE_DESCRIPTION'
| translate
}}</span>
</div>
}
<div class="setting-item setting-item--full-control">
<div class="setting-item__meta">
<h4>{{ 'SETTINGS.COMMUNITY' | translate }}</h4>
@@ -297,6 +297,40 @@
gap: 8px;
}
.epg-sources-empty {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 8px;
padding: 24px 16px 16px;
color: var(--mat-sys-on-surface-variant);
.epg-sources-empty__icon {
font-size: 40px;
width: 40px;
height: 40px;
opacity: 0.55;
margin-bottom: 4px;
}
.epg-sources-empty__title {
margin: 0;
font-size: 0.95rem;
font-weight: 600;
color: var(--mat-sys-on-surface);
}
.epg-sources-empty__hint {
margin: 0;
font-size: 0.825rem;
line-height: 1.5;
max-width: 360px;
opacity: 0.75;
}
}
.epg-source-row {
display: flex;
align-items: center;
@@ -30,6 +30,7 @@ import {
PlaylistsService,
} from 'services';
import {
EmbeddedMpvSupport,
Language,
PlaylistMeta,
StartupBehavior,
@@ -400,6 +401,80 @@ describe('SettingsComponent', () => {
...settings,
});
});
it('hides the embedded mpv option when the desktop support probe reports unsupported', async () => {
window.electron = {
...window.electron,
getEmbeddedMpvSupport: jest.fn().mockResolvedValue({
supported: false,
platform: 'darwin',
reason: 'hidden harness',
}),
} as unknown as typeof window.electron;
await component.ngOnInit();
await fixture.whenStable();
expect(
component.players().some(
(player) => player.id === VideoPlayer.EmbeddedMpv
)
).toBe(false);
});
it('shows the embedded mpv option when the desktop support probe reports supported', async () => {
window.electron = {
...window.electron,
getEmbeddedMpvSupport: jest.fn().mockResolvedValue({
supported: true,
platform: 'darwin',
}),
} as unknown as typeof window.electron;
await component.ngOnInit();
await fixture.whenStable();
expect(
component.players().some(
(player) => player.id === VideoPlayer.EmbeddedMpv
)
).toBe(true);
});
it('does not block settings initialization while embedded mpv support is pending', async () => {
let resolveSupport: (value: EmbeddedMpvSupport) => void;
window.electron = {
...window.electron,
getEmbeddedMpvSupport: jest.fn(
() =>
new Promise((resolve) => {
resolveSupport = resolve;
})
),
} as unknown as typeof window.electron;
await expect(component.ngOnInit()).resolves.toBeUndefined();
expect(component.settingsForm.value).toEqual(DEFAULT_SETTINGS);
expect(window.electron.getEmbeddedMpvSupport).toHaveBeenCalled();
expect(
component.players().some(
(player) => player.id === VideoPlayer.EmbeddedMpv
)
).toBe(false);
resolveSupport!({
supported: true,
platform: 'darwin',
});
await fixture.whenStable();
expect(
component.players().some(
(player) => player.id === VideoPlayer.EmbeddedMpv
)
).toBe(true);
});
});
describe('Version check', () => {
+3 -1
View File
@@ -209,7 +209,9 @@
"REMOTE_CONTROL_PORT_DESCRIPTION": "المنفذ المستخدم للتحكم عن بعد (الافتراضي: 8765).",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "السمة الداكنة",
+3 -1
View File
@@ -201,7 +201,9 @@
"REMOTE_CONTROL_PORT_DESCRIPTION": "المنفذ اللي كيتستعمل للتحكم عن بعد للتطبيق. المنفذ 3000 كيتستعمل افتراضياً.",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "المظهر الداكن",
+3 -1
View File
@@ -203,7 +203,9 @@
"VLC_PLAYER_PATH_DESCRIPTION": "Усталюйце шлях да прайгравальніка VLC у вашай сістэме. \nЁн будзе выкарыстоўваць прадастаўлены шлях да бінарнага файла VLC замест шляху па змаўчанні.",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "Цёмная тэма",
+3 -1
View File
@@ -376,7 +376,9 @@
"PORT_PATTERN": "Nur Ziffern sind erlaubt",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "Dunkel",
+3 -1
View File
@@ -254,7 +254,9 @@
"PORT_PATTERN": "Επιτρέπονται μόνο αριθμητικά ψηφία",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "Σκούρο θέμα",
+2
View File
@@ -200,6 +200,8 @@
"PLAYBACK": "Playback",
"EPG_SOURCES": "EPG sources",
"EPG_SOURCES_DESCRIPTION": "Add one or more URLs as EPG sources",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels.",
"DATA": "Data",
"ABOUT": "About",
"NAV_GENERAL": "General",
+3 -1
View File
@@ -231,7 +231,9 @@
"STREAM_FORMAT_DESCRIPTION": "Selecciona el formato de transmision predeterminado (Xtream)",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "Tema oscuro",
+3 -1
View File
@@ -222,7 +222,9 @@
"REMOTE_CONTROL_PORT_DESCRIPTION": "Port utilisé pour la télécommande de l'application. Le port 8765 est utilisé par défaut.",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "Thème foncé",
+3 -1
View File
@@ -243,7 +243,9 @@
"PORT_PATTERN": "Sono consentite solo cifre",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "Tema scuro",
+3 -1
View File
@@ -189,7 +189,9 @@
"REMOTE_CONTROL_PORT_DESCRIPTION": "アプリケーションのリモートコントロールに使用されるポート。デフォルトではポート3000が使用されます。",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "ダークテーマ",
+3 -1
View File
@@ -160,7 +160,9 @@
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "어두운 테마",
+3 -1
View File
@@ -201,7 +201,9 @@
"REMOTE_CONTROL_PORT_DESCRIPTION": "Poort voor de afstandsbediening van de applicatie. Standaard wordt poort 3000 gebruikt.",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "Donker thema",
+3 -1
View File
@@ -201,7 +201,9 @@
"REMOTE_CONTROL_PORT_DESCRIPTION": "Port używany do zdalnego sterowania aplikacją. Domyślnie używany jest port 3000.",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "Ciemny motyw",
+3 -1
View File
@@ -220,7 +220,9 @@
"REMOTE_CONTROL_PORT_DESCRIPTION": "Porta usada para controle remoto do aplicativo. A porta 8765 é usada por padrão.",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "Tema escuro",
+3 -1
View File
@@ -299,7 +299,9 @@
"PORT_PATTERN": "Разрешены только цифры",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "Тёмная тема",
+3 -1
View File
@@ -255,7 +255,9 @@
"PORT_PATTERN": "Sadece rakamlara izin verilir",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "Koyu tema",
+3 -1
View File
@@ -160,7 +160,9 @@
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "暗黑模式",
+3 -1
View File
@@ -206,7 +206,9 @@
"REMOTE_CONTROL_PORT_DESCRIPTION": "用於遠端控制應用程式的連接埠。預設使用連接埠 8765。",
"REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…",
"REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}",
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again.",
"EPG_EMPTY_TITLE": "No EPG sources yet",
"EPG_EMPTY_HINT": "Add a URL to fetch program data for your channels."
},
"THEMES": {
"DARK_THEME": "深色主題",