From 34b252831bb7b1bc9800e6ce19b70dba4df37593 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 15 Nov 2025 14:26:46 +0100 Subject: [PATCH 1/8] feat(account-info): add translations Add a complete Angular template for the Xtream Tauri account-info dialog. The new HTML lays out user and server information in a responsive two-column grid, including status, username, connection counts, creation/expiry dates, trial indicator, allowed output formats and server details (live/movies/series counts, URL, protocol, timezone, server time and ports). Translation pipes are used for all labels and conditional classes mark active/trial states. This provides a full UI for displaying parsed accountInfo data and prepares the dialog for binding with component logic and styles. --- .../account-info/account-info.component.html | 114 ++++++++++++++ .../account-info/account-info.component.ts | 148 ++---------------- apps/web/src/assets/i18n/de.json | 28 +++- apps/web/src/assets/i18n/en.json | 28 +++- apps/web/src/assets/i18n/ru.json | 28 +++- 5 files changed, 210 insertions(+), 136 deletions(-) create mode 100644 apps/web/src/app/xtream-tauri/account-info/account-info.component.html diff --git a/apps/web/src/app/xtream-tauri/account-info/account-info.component.html b/apps/web/src/app/xtream-tauri/account-info/account-info.component.html new file mode 100644 index 000000000..005c1ed2e --- /dev/null +++ b/apps/web/src/app/xtream-tauri/account-info/account-info.component.html @@ -0,0 +1,114 @@ +

{{ 'XTREAM.ACCOUNT_INFO.TITLE' | translate }}

+ + @if (accountInfo?.user_info?.message) { +
+ {{ accountInfo?.user_info?.message }} +
+ } +
+
+

{{ 'XTREAM.ACCOUNT_INFO.USER_INFO' | translate }}

+ + + {{ 'XTREAM.ACCOUNT_INFO.STATUS' | translate }}: + {{ + accountInfo?.user_info?.status + }} + + + {{ 'XTREAM.ACCOUNT_INFO.USERNAME' | translate }}: + {{ accountInfo?.user_info?.username }} + + + {{ 'XTREAM.ACCOUNT_INFO.ACTIVE_CONNECTIONS' | translate }}: + {{ accountInfo?.user_info?.active_cons }}/{{ + accountInfo?.user_info?.max_connections + }} + + + {{ 'XTREAM.ACCOUNT_INFO.CREATED' | translate }}: + {{ formattedCreatedDate }} + + + {{ 'XTREAM.ACCOUNT_INFO.EXPIRES' | translate }}: + {{ formattedExpDate }} + + + {{ 'XTREAM.ACCOUNT_INFO.TRIAL_ACCOUNT' | translate }}: + {{ + (isTrial + ? 'XTREAM.ACCOUNT_INFO.YES' + : 'XTREAM.ACCOUNT_INFO.NO' + ) | translate + }} + + + {{ 'XTREAM.ACCOUNT_INFO.ALLOWED_FORMATS' | translate }}: + + {{ + accountInfo?.user_info?.allowed_output_formats?.join( + ', ' + ) + }} + + + +
+ +
+

{{ 'XTREAM.ACCOUNT_INFO.SERVER_INFO' | translate }}

+ + + {{ 'XTREAM.ACCOUNT_INFO.LIVE_TV' | translate }}: + {{ liveStreamsCount }} + + + {{ 'XTREAM.ACCOUNT_INFO.MOVIES' | translate }}: + {{ vodStreamsCount }} + + + {{ 'XTREAM.ACCOUNT_INFO.TV_SERIES' | translate }}: + {{ seriesCount }} + + + {{ 'XTREAM.ACCOUNT_INFO.URL' | translate }}: + {{ accountInfo?.server_info?.url }} + + + {{ 'XTREAM.ACCOUNT_INFO.PROTOCOL' | translate }}: + {{ accountInfo?.server_info?.server_protocol }} + + + {{ 'XTREAM.ACCOUNT_INFO.TIMEZONE' | translate }}: + {{ accountInfo?.server_info?.timezone }} + + + {{ 'XTREAM.ACCOUNT_INFO.SERVER_TIME' | translate }}: + {{ accountInfo?.server_info?.time_now }} + + + {{ 'XTREAM.ACCOUNT_INFO.PORTS' | translate }}: +
+ {{ 'XTREAM.ACCOUNT_INFO.HTTP_PORT' | translate }}: + {{ accountInfo?.server_info?.port }} + {{ 'XTREAM.ACCOUNT_INFO.HTTPS_PORT' | translate }}: + {{ accountInfo?.server_info?.https_port }} + {{ 'XTREAM.ACCOUNT_INFO.RTMP_PORT' | translate }}: + {{ accountInfo?.server_info?.rtmp_port }} +
+
+
+
+
+
+ + + diff --git a/apps/web/src/app/xtream-tauri/account-info/account-info.component.ts b/apps/web/src/app/xtream-tauri/account-info/account-info.component.ts index feea9eede..2e1f168c3 100644 --- a/apps/web/src/app/xtream-tauri/account-info/account-info.component.ts +++ b/apps/web/src/app/xtream-tauri/account-info/account-info.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, inject } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { MatButton } from '@angular/material/button'; import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; import { MatListModule } from '@angular/material/list'; @@ -11,150 +11,32 @@ import { XtreamAccountInfo } from './account-info.interface'; @Component({ selector: 'app-account-info', imports: [MatButton, MatDialogModule, MatListModule, TranslatePipe], - template: ` -

Account Information

- - @if (accountInfo?.user_info?.message) { -
- {{ accountInfo?.user_info?.message }} -
- } -
-
-

User Information

- - - Status: - {{ - accountInfo?.user_info?.status - }} - - - Username: - {{ accountInfo?.user_info?.username }} - - - Active Connections: - {{ accountInfo?.user_info?.active_cons }}/{{ - accountInfo?.user_info?.max_connections - }} - - - Created: - {{ formattedCreatedDate }} - - - Expires: {{ formattedExpDate }} - - - Trial Account: - {{ - isTrial ? 'Yes' : 'No' - }} - - - Allowed Formats: - - {{ - accountInfo?.user_info?.allowed_output_formats?.join( - ', ' - ) - }} - - - -
- -
-

Server Information

- - - Live TV: - {{ liveStreamsCount }} - - - Movies: - {{ vodStreamsCount }} - - - TV Series: - {{ seriesCount }} - - - URL: - {{ accountInfo?.server_info?.url }} - - - Protocol: - {{ accountInfo?.server_info?.server_protocol }} - - - Timezone: - {{ accountInfo?.server_info?.timezone }} - - - Server Time: - {{ accountInfo?.server_info?.time_now }} - - - Ports: -
- HTTP: - {{ accountInfo?.server_info?.port }} - HTTPS: - {{ - accountInfo?.server_info?.https_port - }} - RTMP: - {{ - accountInfo?.server_info?.rtmp_port - }} -
-
-
-
-
-
- - - - `, + templateUrl: './account-info.component.html', styleUrl: './account-info.component.scss', }) export class AccountInfoComponent { - accountInfo: XtreamAccountInfo; - formattedExpDate: string; - formattedCreatedDate: string; - + readonly data = inject<{ + vodStreamsCount: number; + liveStreamsCount: number; + seriesCount: number; + }>(MAT_DIALOG_DATA); private readonly dataService = inject(DataService); private readonly store = inject(Store); + accountInfo: XtreamAccountInfo; + formattedExpDate: string; + formattedCreatedDate: string; vodStreamsCount: number; liveStreamsCount: number; seriesCount: number; readonly currentPlaylist = this.store.selectSignal(selectActivePlaylist); - constructor( - @Inject(MAT_DIALOG_DATA) - data: { - vodStreamsCount: number; - liveStreamsCount: number; - seriesCount: number; - } - ) { + constructor() { this.setAccountInfo(); - this.vodStreamsCount = data.vodStreamsCount; - this.liveStreamsCount = data.liveStreamsCount; - this.seriesCount = data.seriesCount; + this.vodStreamsCount = this.data.vodStreamsCount; + this.liveStreamsCount = this.data.liveStreamsCount; + this.seriesCount = this.data.seriesCount; } async setAccountInfo() { @@ -170,7 +52,7 @@ export class AccountInfoComponent { action: 'get_account_info', } ); - console.log(this.accountInfo); + if (this.accountInfo) { this.formattedExpDate = new Date( parseInt(this.accountInfo.user_info.exp_date) * 1000 diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index b8715234a..a97d8b5f7 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -219,6 +219,7 @@ "RELEASE_DATE": "Veröffentlichungsdatum", "ACTORS": "Schauspieler", "PLAY": "Abspielen", + "OPENING_PLAYER": "Player wird geöffnet...", "GENRE": "Genre", "DURATION": "Dauer", "COUNTRY": "Land", @@ -233,7 +234,32 @@ "DESCRIPTION": "Beschreibung", "KINOPOISK_RATING": "Bewertung Kinopoisk", "IMDB_RATING": "IMDB-Bewertung", - "EPISODE_RUN_TIME": "Laufzeit der Episode" + "EPISODE_RUN_TIME": "Laufzeit der Episode", + "ACCOUNT_INFO": { + "TITLE": "Kontoinformationen", + "USER_INFO": "Benutzerinformationen", + "STATUS": "Status", + "USERNAME": "Benutzername", + "ACTIVE_CONNECTIONS": "Aktive Verbindungen", + "CREATED": "Erstellt", + "EXPIRES": "Läuft ab", + "TRIAL_ACCOUNT": "Testkonto", + "YES": "Ja", + "NO": "Nein", + "ALLOWED_FORMATS": "Zulässige Formate", + "SERVER_INFO": "Serverinformationen", + "LIVE_TV": "Live-TV", + "MOVIES": "Filme", + "TV_SERIES": "Serien", + "URL": "URL", + "PROTOCOL": "Protokoll", + "TIMEZONE": "Zeitzone", + "SERVER_TIME": "Serverzeit", + "PORTS": "Ports", + "HTTP_PORT": "HTTP", + "HTTPS_PORT": "HTTPS", + "RTMP_PORT": "RTMP" + } }, "PORTALS": { "SIDEBAR": { diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index 2451434ef..658bf4f2a 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -264,6 +264,7 @@ "REFRESH": "Refresh", "XTREAM": { "PLAY": "Play", + "OPENING_PLAYER": "Opening player...", "NAME": "Name", "RELEASE_DATE": "Release date", "GENRE": "Genre", @@ -280,7 +281,32 @@ "YEAR": "Year", "AGE": "Age", "YOUTUBE_TRAILER": "YouTube Trailer", - "EPISODE": "Episode" + "EPISODE": "Episode", + "ACCOUNT_INFO": { + "TITLE": "Account Information", + "USER_INFO": "User Information", + "STATUS": "Status", + "USERNAME": "Username", + "ACTIVE_CONNECTIONS": "Active Connections", + "CREATED": "Created", + "EXPIRES": "Expires", + "TRIAL_ACCOUNT": "Trial Account", + "YES": "Yes", + "NO": "No", + "ALLOWED_FORMATS": "Allowed Formats", + "SERVER_INFO": "Server Information", + "LIVE_TV": "Live TV", + "MOVIES": "Movies", + "TV_SERIES": "TV Series", + "URL": "URL", + "PROTOCOL": "Protocol", + "TIMEZONE": "Timezone", + "SERVER_TIME": "Server Time", + "PORTS": "Ports", + "HTTP_PORT": "HTTP", + "HTTPS_PORT": "HTTPS", + "RTMP_PORT": "RTMP" + } }, "PORTALS": { "ALL_CATEGORIES": "All categories", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index 9dec7a7d4..f363a9341 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -230,6 +230,7 @@ "RELEASE_DATE": "Дата выпуска", "ACTORS": "Актеры", "PLAY": "Воспроизвести", + "OPENING_PLAYER": "Открытие плеера...", "GENRE": "Жанр", "DURATION": "Продолжительность", "COUNTRY": "Страна", @@ -243,7 +244,32 @@ "YOUTUBE_TRAILER": "YouTube-трейлер", "DESCRIPTION": "Описание", "KINOPOISK_RATING": "Рейтинг на Кинопоиск", - "IMDB_RATING": "Рейтинг на IMDB" + "IMDB_RATING": "Рейтинг на IMDB", + "ACCOUNT_INFO": { + "TITLE": "Информация об аккаунте", + "USER_INFO": "Информация о пользователе", + "STATUS": "Статус", + "USERNAME": "Имя пользователя", + "ACTIVE_CONNECTIONS": "Активные подключения", + "CREATED": "Создано", + "EXPIRES": "Истекает", + "TRIAL_ACCOUNT": "Пробный аккаунт", + "YES": "Да", + "NO": "Нет", + "ALLOWED_FORMATS": "Разрешенные форматы", + "SERVER_INFO": "Информация о сервере", + "LIVE_TV": "Прямой эфир", + "MOVIES": "Фильмы", + "TV_SERIES": "Сериалы", + "URL": "URL", + "PROTOCOL": "Протокол", + "TIMEZONE": "Часовой пояс", + "SERVER_TIME": "Время сервера", + "PORTS": "Порты", + "HTTP_PORT": "HTTP", + "HTTPS_PORT": "HTTPS", + "RTMP_PORT": "RTMP" + } }, "PORTALS": { "ALL_CATEGORIES": "Все категории", From 3c4af147a8faf7d2c48627be4f47635c9aacb52a Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 15 Nov 2025 14:27:08 +0100 Subject: [PATCH 2/8] refactor(player): remove unused PlayerDialog and SafePipe Remove the PlayerDialogComponent (TS, HTML, SCSS) and the standalone SafePipe from the xtream feature. These files were deleted because the dialog and pipe are no longer used by the application, reducing dead code and simplifying the web module surface. This change prevents maintaining UI code (player markup, styles, and clipboard/snackbar logic) and an unused DOM sanitizer pipe, improving project maintainability and build size. --- .../player-dialog.component.html | 25 --------- .../player-dialog.component.scss | 22 -------- .../player-dialog/player-dialog.component.ts | 54 ------------------- .../src/app/xtream/vod-details/safe.pipe.ts | 11 ---- 4 files changed, 112 deletions(-) delete mode 100644 apps/web/src/app/xtream/player-dialog/player-dialog.component.html delete mode 100644 apps/web/src/app/xtream/player-dialog/player-dialog.component.scss delete mode 100644 apps/web/src/app/xtream/player-dialog/player-dialog.component.ts delete mode 100644 apps/web/src/app/xtream/vod-details/safe.pipe.ts diff --git a/apps/web/src/app/xtream/player-dialog/player-dialog.component.html b/apps/web/src/app/xtream/player-dialog/player-dialog.component.html deleted file mode 100644 index 34bd66a04..000000000 --- a/apps/web/src/app/xtream/player-dialog/player-dialog.component.html +++ /dev/null @@ -1,25 +0,0 @@ -

{{ title }}

- - - - - - - - - - diff --git a/apps/web/src/app/xtream/player-dialog/player-dialog.component.scss b/apps/web/src/app/xtream/player-dialog/player-dialog.component.scss deleted file mode 100644 index d2371f35a..000000000 --- a/apps/web/src/app/xtream/player-dialog/player-dialog.component.scss +++ /dev/null @@ -1,22 +0,0 @@ -@use '@angular/material' as mat; - -.content { - overflow: hidden; - padding: 10px !important; -} - -.link-input { - width: 98%; - padding-top: 5px; - @include mat.form-field-density(-5); -} - -.align-actions { - justify-content: space-between; -} - -mat-dialog-content { - .video-js { - height: 500px !important; - } -} \ No newline at end of file diff --git a/apps/web/src/app/xtream/player-dialog/player-dialog.component.ts b/apps/web/src/app/xtream/player-dialog/player-dialog.component.ts deleted file mode 100644 index dc4c4d6c8..000000000 --- a/apps/web/src/app/xtream/player-dialog/player-dialog.component.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ClipboardModule } from '@angular/cdk/clipboard'; -import { Component, inject, ViewEncapsulation } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatSnackBar } from '@angular/material/snack-bar'; -import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { WebPlayerViewComponent } from 'shared-portals'; - -export interface PlayerDialogData { - streamUrl: string; - title: string; -} - -@Component({ - templateUrl: './player-dialog.component.html', - imports: [ - ClipboardModule, - MatButtonModule, - MatDialogModule, - MatFormFieldModule, - MatIconModule, - MatInputModule, - TranslateModule, - WebPlayerViewComponent, - ], - styleUrl: './player-dialog.component.scss', - encapsulation: ViewEncapsulation.None, -}) -export class PlayerDialogComponent { - readonly data = inject(MAT_DIALOG_DATA) as PlayerDialogData; - private snackBar = inject(MatSnackBar); - private translateService = inject(TranslateService); - - readonly title: string; - readonly streamUrl: string; - - constructor() { - this.streamUrl = this.data.streamUrl; - this.title = this.data.title; - } - - showCopyNotification() { - this.snackBar.open( - this.translateService.instant('PORTALS.STREAM_URL_COPIED'), - null, - { - duration: 2000, - } - ); - } -} diff --git a/apps/web/src/app/xtream/vod-details/safe.pipe.ts b/apps/web/src/app/xtream/vod-details/safe.pipe.ts deleted file mode 100644 index d91d2fcec..000000000 --- a/apps/web/src/app/xtream/vod-details/safe.pipe.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; - -@Pipe({ name: 'safe', standalone: true }) -export class SafePipe implements PipeTransform { - constructor(private sanitizer: DomSanitizer) {} - - transform(url: string) { - return this.sanitizer.bypassSecurityTrustResourceUrl(url); - } -} From 3455ff75f1933018b46f030d32776d73ee398867 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 15 Nov 2025 14:27:47 +0100 Subject: [PATCH 3/8] refactor: clean up layout, simplify markup, fix img alt Remove unused .link-input styles from player-dialog component and tidy vod-details template indentation and structure. Reformat container and image/detail sections to use consistent indentation, add an alt attribute to the movie poster img, replace several label elements with simpler divs for clearer semantics, and ensure placeholder-cover fallback remains. These changes improve readability, reduce unused CSS, and enhance HTML accessibility. --- apps/web/src/app/app.component.spec.ts | 66 +----- .../category-content-view.component.ts | 9 +- .../player-dialog.component.html | 9 +- .../player-dialog.component.scss | 6 - .../player-dialog/player-dialog.component.ts | 40 ++-- .../recently-viewed.component.ts | 18 +- .../vod-details/vod-details.component.html | 66 +++--- .../app/xtream/portal-card-item.interface.ts | 10 - .../vod-details/vod-details.component.html | 211 ++++++++---------- .../vod-details/vod-details.component.ts | 2 +- .../xtream/xtream-main-container.component.ts | 11 +- 11 files changed, 169 insertions(+), 279 deletions(-) delete mode 100644 apps/web/src/app/xtream/portal-card-item.interface.ts diff --git a/apps/web/src/app/app.component.spec.ts b/apps/web/src/app/app.component.spec.ts index 2061a1fb9..4ab97362d 100644 --- a/apps/web/src/app/app.component.spec.ts +++ b/apps/web/src/app/app.component.spec.ts @@ -12,20 +12,16 @@ import { provideMockStore } from '@ngrx/store/testing'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { MockComponent, MockModule, MockPipe, MockProviders } from 'ng-mocks'; import { NgxIndexedDBService } from 'ngx-indexed-db'; -import { NgxWhatsNewComponent } from 'ngx-whats-new'; import { of } from 'rxjs'; -import { DataService } from '../../../../libs/services/src/lib/data.service'; -import { PlaylistsService } from '../../../../libs/services/src/lib/playlists.service'; -import { Language } from '../../../../libs/shared/interfaces/src/lib/language.enum'; -import { STORE_KEY } from '../../../../libs/shared/interfaces/src/lib/store-keys.enum'; -import { Theme } from '../../../../libs/shared/interfaces/src/lib/theme.enum'; +import { DataService, PlaylistsService } from 'services'; +import { Language, STORE_KEY, Theme } from 'shared-interfaces'; import { AppComponent } from './app.component'; import { ElectronServiceStub } from './services/electron.service.stub'; import { SettingsService } from './services/settings.service'; -import { WhatsNewService } from './services/whats-new.service'; -import { WhatsNewServiceStub } from './services/whats-new.service.stub'; -jest.spyOn(global.console, 'error').mockImplementation(() => {}); +jest.spyOn(global.console, 'error').mockImplementation(() => { + // suppress console.error output during tests +}); describe('AppComponent', () => { let component: AppComponent; @@ -33,14 +29,12 @@ describe('AppComponent', () => { let fixture: ComponentFixture; let settingsService: SettingsService; let translateService: TranslateService; - let whatsNewService: WhatsNewService; const defaultLanguage = 'en'; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [AppComponent, MockPipe(TranslatePipe)], providers: [ - { provide: WhatsNewService, useClass: WhatsNewServiceStub }, MockProviders( TranslateService, PlaylistsService, @@ -68,7 +62,6 @@ describe('AppComponent', () => { fixture = TestBed.createComponent(AppComponent); settingsService = TestBed.inject(SettingsService); translateService = TestBed.inject(TranslateService); - whatsNewService = TestBed.inject(WhatsNewService); component = fixture.componentInstance; // TODO: investigate in detail @@ -128,14 +121,6 @@ describe('AppComponent', () => { expect(router.navigateByUrl).toHaveBeenCalledWith(route); } )); - - it('show show whats new dialog', () => { - jest.spyOn(whatsNewService, 'getModalsByVersion'); - jest.spyOn(component, 'setDialogVisibility'); - component.showWhatsNewDialog(); - expect(whatsNewService.getModalsByVersion).toHaveBeenCalledTimes(1); - expect(component.setDialogVisibility).toHaveBeenCalledWith(true); - }); }); describe('Test version handling', () => { @@ -144,17 +129,8 @@ describe('AppComponent', () => { const spyOnSettingsGet = jest .spyOn(settingsService, 'getValueFromLocalStorage') .mockReturnValue(of(currentAppVersion)); - jest.spyOn(whatsNewService, 'getModalsByVersion').mockReturnValue([ - {}, - ]); - jest.spyOn(whatsNewService, 'changeDialogVisibleState'); - component.handleWhatsNewDialog(); expect(spyOnSettingsGet).toHaveBeenCalled(); - expect(whatsNewService.getModalsByVersion).toHaveBeenCalled(); - expect( - whatsNewService.changeDialogVisibleState - ).toHaveBeenCalledWith(true); }); it('should get actual app version which is not outdated and do not shop updates dialog', () => { @@ -162,40 +138,8 @@ describe('AppComponent', () => { const spyOnSettingsGet = jest .spyOn(settingsService, 'getValueFromLocalStorage') .mockReturnValue(of(currentAppVersion)); - jest.spyOn(whatsNewService, 'getModalsByVersion').mockReturnValue([ - {}, - ]); - jest.spyOn(whatsNewService, 'changeDialogVisibleState'); - component.handleWhatsNewDialog(); expect(spyOnSettingsGet).toHaveBeenCalled(); - expect(whatsNewService.getModalsByVersion).toHaveBeenCalledTimes(0); - expect( - whatsNewService.changeDialogVisibleState - ).toHaveBeenCalledTimes(0); - }); - - it('should change the visibility of the whats new dialog', () => { - jest.spyOn(whatsNewService, 'changeDialogVisibleState'); - component.modals = [{}, {}]; - const visibilityFlag = true; - component.setDialogVisibility(true); - - expect( - whatsNewService.changeDialogVisibleState - ).toHaveBeenCalledWith(visibilityFlag); - expect( - whatsNewService.changeDialogVisibleState - ).toHaveBeenCalledTimes(1); - }); - - it('should not change the visibility of the whats new dialog', () => { - jest.spyOn(whatsNewService, 'changeDialogVisibleState'); - component.modals = []; - component.setDialogVisibility(true); - expect( - whatsNewService.changeDialogVisibleState - ).toHaveBeenCalledTimes(0); }); }); diff --git a/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts b/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts index d7942b099..655bcfd9e 100644 --- a/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts +++ b/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts @@ -1,6 +1,5 @@ import { Component, inject, OnInit } from '@angular/core'; -import { MatCardModule } from '@angular/material/card'; -import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { PageEvent } from '@angular/material/paginator'; import { ActivatedRoute, Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -16,13 +15,11 @@ import { XtreamStore } from '../xtream.store'; templateUrl: './category-content-view.component.html', styleUrls: ['./category-content-view.component.scss'], imports: [ - MatCardModule, - MatPaginatorModule, + GridListComponent, PlaylistErrorViewComponent, + StalkerSeriesViewComponent, TranslatePipe, VodDetailsComponent, - GridListComponent, - StalkerSeriesViewComponent, ], }) export class CategoryContentViewComponent implements OnInit { diff --git a/apps/web/src/app/xtream-tauri/player-dialog/player-dialog.component.html b/apps/web/src/app/xtream-tauri/player-dialog/player-dialog.component.html index 34bd66a04..efe236c70 100644 --- a/apps/web/src/app/xtream-tauri/player-dialog/player-dialog.component.html +++ b/apps/web/src/app/xtream-tauri/player-dialog/player-dialog.component.html @@ -1,20 +1,13 @@

{{ title }}

- - -   - @if (!isFavorite) { - + alt="Movie poster" + /> } @else { - +
} - - - - @if (item.info.youtube_trailer) { +
+

{{ item.info.name }}

+ @if (item.info.description) { +
+ {{ item.info.description }} +
+ } + @if (item.info.releasedate) { +
{{ 'XTREAM.RELEASE_DATE' | translate }}:
+ {{ item.info.releasedate }} + } + @if (item.info.genre) { +
{{ 'XTREAM.GENRE' | translate }}:
+ {{ item.info.genre }} + } + @if (item.info.country) { +
{{ 'XTREAM.COUNTRY' | translate }}:
+ {{ item.info.country }} + } + @if (item.info.actors) { +
+
{{ 'XTREAM.ACTORS' | translate }}:
+ {{ item.info.actors }} +
+ } + @if (item.info.director) { +
{{ 'XTREAM.DIRECTOR' | translate }}:
+ {{ item.info.director }} + } + @if (item.info.duration) { +
{{ 'XTREAM.DURATION' | translate }}:
+ {{ item.info.duration }} + } + @if (item.info.rating_imdb) { +
{{ 'XTREAM.IMDB_RATING' | translate }}:
+ {{ item.info.rating_imdb }} + } + @if (item.info.rating_kinopoisk) { +
+ {{ 'XTREAM.KINOPOISK_RATING' | translate }}: +
+ {{ item.info.rating_kinopoisk }} + } +
+   + @if (!isFavorite) { + + } @else { + + } +
+
+ +@if (item.info.youtube_trailer) {
-

{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}

- +

{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}

+
- } +} diff --git a/apps/web/src/app/xtream/vod-details/vod-details.component.ts b/apps/web/src/app/xtream/vod-details/vod-details.component.ts index 0abda93a3..21bb0c86e 100644 --- a/apps/web/src/app/xtream/vod-details/vod-details.component.ts +++ b/apps/web/src/app/xtream/vod-details/vod-details.component.ts @@ -13,7 +13,7 @@ import { ActivatedRoute } from '@angular/router'; import { TranslatePipe } from '@ngx-translate/core'; import { PlaylistsService } from 'services'; import { XtreamVodDetails } from 'shared-interfaces'; -import { SafePipe } from './safe.pipe'; +import { SafePipe } from '../../xtream-tauri/vod-details/safe.pipe'; @Component({ selector: 'app-vod-details', diff --git a/apps/web/src/app/xtream/xtream-main-container.component.ts b/apps/web/src/app/xtream/xtream-main-container.component.ts index fc7a0233d..baea93c55 100644 --- a/apps/web/src/app/xtream/xtream-main-container.component.ts +++ b/apps/web/src/app/xtream/xtream-main-container.component.ts @@ -41,6 +41,10 @@ import { import { LiveStreamLayoutComponent } from 'shared-portals'; import { SettingsStore } from '../services/settings-store.service'; import { ExternalPlayerInfoDialogComponent } from '../shared/components/external-player-info-dialog/external-player-info-dialog.component'; +import { + PlayerDialogComponent, + PlayerDialogData, +} from '../xtream-tauri/player-dialog/player-dialog.component'; import { PlaylistErrorViewComponent } from '../xtream/playlist-error-view/playlist-error-view.component'; import { Breadcrumb, PortalActions } from './breadcrumb.interface'; import { CategoryContentViewComponent } from './category-content-view/category-content-view.component'; @@ -48,10 +52,6 @@ import { CategoryViewComponent } from './category-view/category-view.component'; import { ContentTypeNavigationItem } from './content-type-navigation-item.interface'; import { ContentType } from './content-type.enum'; import { NavigationBarComponent } from './navigation-bar/navigation-bar.component'; -import { - PlayerDialogComponent, - PlayerDialogData, -} from './player-dialog/player-dialog.component'; import { PortalStore } from './portal.store'; import { SerialDetailsComponent } from './serial-details/serial-details.component'; import { VodDetailsComponent } from './vod-details/vod-details.component'; @@ -395,7 +395,8 @@ export class XtreamMainContainerComponent implements OnInit, OnDestroy { playEpisode(episode: XtreamSerieEpisode) { const playlist = this.currentPlaylist() as Playlist; - const { serverUrl, username, password, userAgent, referrer, origin } = playlist; + const { serverUrl, username, password, userAgent, referrer, origin } = + playlist; const player = this.settingsStore.player(); const streamUrl = `${serverUrl}/series/${username}/${password}/${episode.id}.${episode.container_extension}`; if (player === VideoPlayer.MPV) { From 8bc8eab1670de02097eccbf22dcdf01913aa6f90 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 15 Nov 2025 17:36:25 +0100 Subject: [PATCH 4/8] feat: show category icon when no channel selected Add a disabled category icon button next to the "Select category" placeholder in the live stream layout when no channel is selected, and align the placeholder text and icon with flex and spacing. This gives a clearer visual cue for the missing category. Also add missing translation keys ("SELECT_CATEGORY" and "PAGE") to English, Russian and German locale files so the new UI text and paging label are available across supported languages. --- .../live-stream-layout/live-stream-layout.component.html | 3 +++ .../live-stream-layout/live-stream-layout.component.scss | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.html b/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.html index 8f37d1aaf..81ec04de6 100644 --- a/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.html +++ b/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.html @@ -40,6 +40,9 @@ } @else {
+ {{ 'PORTALS.SELECT_CATEGORY' | translate }}
} diff --git a/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.scss b/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.scss index 85a277495..07eefc38d 100644 --- a/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.scss +++ b/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.scss @@ -53,6 +53,9 @@ margin: 0; font-size: 1.2rem; font-weight: 500; + display: flex; + align-items: center; + gap: 10px; } ::ng-deep { From c34bdf73e1f1437535a97ec85fa1cee6c6d401b6 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 15 Nov 2025 17:36:58 +0100 Subject: [PATCH 5/8] feat(xtream): show category page info and preserve page state Update category header and list views to provide clearer context and improve navigation state handling. - Display category name with current page/total pages when viewing a category (non-detail) so users see which page they're on. - Add i18n "PAGE" key and fill "SELECT_CATEGORY" in translations for en/de/ru to support the new page label and existing messaging. - Tidy live-stream layout template formatting and fix filter pipe spacing for consistency. - Change no-channel-selected fallback to use the translated "PORTALS.SELECT_CATEGORY" string. - Preserve category selection state on CategoryContentView init: only set selected category when the route category differs from the current selection to keep paging/scroll position when returning from a detail view. These changes improve UX by making pagination explicit and avoiding unnecessary state resets when navigating between list and detail. --- .../category-content-view.component.ts | 6 ++++- .../xtream-main-container.component.ts | 14 ++++++++-- apps/web/src/assets/i18n/de.json | 4 ++- apps/web/src/assets/i18n/en.json | 3 ++- apps/web/src/assets/i18n/ru.json | 4 ++- .../live-stream-layout.component.html | 27 +++++++++++-------- 6 files changed, 41 insertions(+), 17 deletions(-) diff --git a/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts b/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts index 655bcfd9e..a3d087914 100644 --- a/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts +++ b/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts @@ -46,7 +46,11 @@ export class CategoryContentViewComponent implements OnInit { ngOnInit() { const { categoryId } = this.activatedRoute.snapshot.params; - if (categoryId) this.store.setSelectedCategory(categoryId); + // Only set category if it's different from the currently selected one + // This preserves the page state when navigating back from detail view + if (categoryId && this.store.selectedCategoryId() !== Number(categoryId)) { + this.store.setSelectedCategory(categoryId); + } } onPageChange(event: PageEvent) { diff --git a/apps/web/src/app/xtream-tauri/xtream-main-container.component.ts b/apps/web/src/app/xtream-tauri/xtream-main-container.component.ts index 4e3a4b4cf..1306998a7 100644 --- a/apps/web/src/app/xtream-tauri/xtream-main-container.component.ts +++ b/apps/web/src/app/xtream-tauri/xtream-main-container.component.ts @@ -53,9 +53,19 @@ export class XtreamMainContainerComponent implements OnInit { return this.translateService.instant('PORTALS.SELECT_CATEGORY'); } else { const selectedCategory = this.xtreamStore.getSelectedCategory(); - return selectedCategory - ? `Content for ${(selectedCategory as any).name}` + const categoryName = selectedCategory + ? (selectedCategory as any).name : 'Category Content'; + + // Show page number when viewing category content (not detail view) + if (!this.xtreamStore.selectedItem() && this.xtreamStore.getTotalPages() > 1) { + const currentPage = this.xtreamStore.page() + 1; // +1 because page is 0-indexed + const totalPages = this.xtreamStore.getTotalPages(); + const pageLabel = this.translateService.instant('PORTALS.PAGE'); + return `${categoryName} (${pageLabel} ${currentPage}/${totalPages})`; + } + + return `Content for ${categoryName}`; } } diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index a97d8b5f7..ca2f0622f 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -310,7 +310,9 @@ "NO_SEARCH_RESULTS": "Nichts gefunden. Versuchen Sie, Ihre Suchanfrage zu ändern" }, "REMOVED_FROM_FAVORITES": "Aus den Favoriten entfernt", - "ADDED_TO_FAVORITES": "Zu den Favoriten hinzugefügt" + "ADDED_TO_FAVORITES": "Zu den Favoriten hinzugefügt", + "SELECT_CATEGORY": "Wählen Sie eine Kategorie aus", + "PAGE": "Seite" }, "ABOUT": { "TITLE": "Über IPTVnator", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index 658bf4f2a..b587a74a5 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -363,7 +363,8 @@ }, "STREAM_URL_COPIED": "Stream URL copied to clipboard", "COPY_STREAM_URL": "Copy Stream URL", - "SELECT_CATEGORY": "Select a category" + "SELECT_CATEGORY": "Select a category", + "PAGE": "Page" }, "INFORMATION": "Information", "REMOTE_CONTROL": { diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index f363a9341..b69f2fccb 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -306,7 +306,9 @@ "NO_SEARCH_RESULTS": "Ничего не найдено, попробуйте изменить поисковый запрос" }, "REMOVED_FROM_FAVORITES": "Удалено из избранного", - "ADDED_TO_FAVORITES": "Добавлено в избранное" + "ADDED_TO_FAVORITES": "Добавлено в избранное", + "SELECT_CATEGORY": "Выберите категорию", + "PAGE": "Страница" }, "INFORMATION": "Инфо" } \ No newline at end of file diff --git a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.html b/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.html index c580d133b..6e2b5e17f 100644 --- a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.html +++ b/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.html @@ -26,7 +26,7 @@ - @if (player === 'videojs' || player === 'html5') { @if (streamUrl) { -
- -
- } } @if (streamUrl) { -
-
- + @if (player === 'videojs' || player === 'html5') { + @if (streamUrl) { +
+ +
+ } + } + @if (streamUrl) { +
+
+ +
-
} @else { -
Please select a channel
+
+ {{ 'PORTALS.SELECT_CATEGORY' | translate }} +
}
From 80fa80ff09de539c13f050a0e986504af2c4b908 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 15 Nov 2025 17:37:17 +0100 Subject: [PATCH 6/8] feat(settings): filter pre-releases and add version compare Filter out pre-release GitHub releases (beta,, rc, preview, dev, canary, nightly) when resolving the latest app version and validate release names using semver. Sort stable releases by creation date and return the most recent stable release name. Add isVersionOutdated(current, latest) to compare coerced semver versions safely, handling invalid formats with a warning. Move version comparison logic from the settings component to the SettingsService and use the new helper to determine if the installed app is outdated. This prevents false positives from pre-release tags and improves version parsing robustness. --- apps/web/src/app/services/settings.service.ts | 77 +++++++++++++++++-- .../src/app/settings/settings.component.ts | 6 +- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/apps/web/src/app/services/settings.service.ts b/apps/web/src/app/services/settings.service.ts index deac6ddf5..a4ec4d7bf 100644 --- a/apps/web/src/app/services/settings.service.ts +++ b/apps/web/src/app/services/settings.service.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { StorageMap } from '@ngx-pwa/local-storage'; import { catchError, map, Observable } from 'rxjs'; +import * as semver from 'semver'; import { STORE_KEY, Theme } from 'shared-interfaces'; @Injectable({ @@ -52,6 +53,7 @@ export class SettingsService { /** * Returns the version of the released app + * Filters out pre-release versions (beta, alpha, rc) */ getAppVersion() { return this.http @@ -59,14 +61,48 @@ export class SettingsService { 'https://api.github.com/repos/4gray/iptvnator/releases' ) .pipe( - map( - (response) => - response.sort( - (a, b) => - new Date(b.created_at).getTime() - - new Date(a.created_at).getTime() - )[0] - ), + map((response) => { + // Filter out pre-release versions (beta, alpha, rc, etc.) + const stableReleases = response.filter((release) => { + const releaseName = release.name.toLowerCase(); + + // Check for beta/alpha/rc keywords in the release name + const prereleaseKeywords = [ + 'beta', + 'alpha', + 'rc', + 'preview', + 'dev', + 'canary', + 'nightly', + ]; + const hasPrereleaseKeyword = prereleaseKeywords.some( + (keyword) => releaseName.includes(keyword) + ); + + if (hasPrereleaseKeyword) { + return false; + } + + // Validate version format + const version = semver.valid( + semver.coerce(release.name) + ); + if (!version) return false; + + // Check if version has prerelease tags in semver format + return !semver.prerelease(release.name); + }); + + // Sort stable releases by creation date + const sortedReleases = stableReleases.sort( + (a, b) => + new Date(b.created_at).getTime() - + new Date(a.created_at).getTime() + ); + + return sortedReleases[0]; + }), map((response) => response.name), catchError((err) => { console.error(err); @@ -74,4 +110,29 @@ export class SettingsService { }) ); } + + /** + * Compares current version with latest version + * @param currentVersion current version of the app + * @param latestVersion latest stable version from GitHub + * @returns true if current version is outdated + */ + isVersionOutdated( + currentVersion: string, + latestVersion: string + ): boolean { + // Clean and coerce versions to handle invalid formats + const cleanCurrent = semver.coerce(currentVersion); + const cleanLatest = semver.coerce(latestVersion); + + if (!cleanCurrent || !cleanLatest) { + console.warn('Invalid version format:', { + currentVersion, + latestVersion, + }); + return false; + } + + return semver.lt(cleanCurrent, cleanLatest); + } } diff --git a/apps/web/src/app/settings/settings.component.ts b/apps/web/src/app/settings/settings.component.ts index fa9fd33cf..5bc12672f 100644 --- a/apps/web/src/app/settings/settings.component.ts +++ b/apps/web/src/app/settings/settings.component.ts @@ -36,7 +36,6 @@ import { DialogService } from 'components'; import * as PlaylistActions from 'm3u-state'; import { selectIsEpgAvailable } from 'm3u-state'; import { take } from 'rxjs'; -import * as semver from 'semver'; import { DataService, EpgService, PlaylistsService } from 'services'; import { Language, @@ -242,7 +241,10 @@ export class SettingsComponent implements OnInit { */ isCurrentVersionOutdated(latestVersion: string): boolean { this.version = this.dataService.getAppVersion(); - return semver.lt(this.version, latestVersion); + return this.settingsService.isVersionOutdated( + this.version, + latestVersion + ); } /** From 2bcbffa5e5e12f49ffc17a9f458b7ca862b53726 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 15 Nov 2025 17:37:34 +0100 Subject: [PATCH 7/8] chore: expand allowed Claude local commands Add additional allowed commands to .claude/settings.local.json so the assistant can run frontend build and view file contents during local debugging. Specifically, permit Bash(npm run build:frontend:*) and Bash(cat:*) alongside the existing WebFetch(domain:www.electronjs.org). This change enables local automation for building the frontend and inspecting files, improving developer workflow and troubleshooting without broadening external network permissions. --- .claude/settings.local.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ee75d9f10..41cd94774 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,9 @@ "allow": [ "Bash(tree:*)", "WebSearch", - "WebFetch(domain:www.electronjs.org)" + "WebFetch(domain:www.electronjs.org)", + "Bash(npm run build:frontend:*)", + "Bash(cat:*)" ], "deny": [], "ask": [] From f2e8e15dcdbe2684463de673a59efe7e5f0eb418 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 15 Nov 2025 18:17:40 +0100 Subject: [PATCH 8/8] feat(i18n): add German translations for sorting, EPG and playlist Add new German translation keys for sorting options used across the UI (SORT_BY, SORT_BY_DATE, SORT_BY_RATING, SORT_BY_NAME, SORT_PLAYLISTS, SORT_OPTIONS). Introduce EPG title and success message (EPG.TITLE, EPG.FETCH_SUCCESS). Expand playlist-related messages: export success/error, refresh Xtream dialog and messages (REFRESH_XTREAM, REFRESH_XTREAM_DIALOG), remove dialog success, playlist update/error messages, auto-refresh flags, and a safer CORS note for URL upload (URL_UPLOAD.CORS_NOTE). Add STREAM_FORMAT label under settings. Also improve existing German copy for GLOBAL_FAVORITES_DESCRIPTION and minor reordering of IMPORTED_AS_TEXT to keep related keys together. These changes localize newly added UI features (sorting, playlist import/ export, Xtream refresh, EPG feedback) and provide clearer user-facing messages and safety guidance. --- apps/web/src/assets/i18n/de.json | 68 +++++++++++++++++++++---- apps/web/src/assets/i18n/ru.json | 85 +++++++++++++++++++++++++++----- 2 files changed, 133 insertions(+), 20 deletions(-) diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index ca2f0622f..961242124 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -12,9 +12,14 @@ "UNNAMED_CHANNEL": "Sender ohne Namen", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Neue Playlist hochladen oder eine andere aussuchen", "USE_STAR_TO_FAVORITE": "Nutzen Sie das Star-Icon um Sender als Favoriten zu markieren", - "FAVORITES_UPDATED": "Favoriten wurden aktualisiert!" + "FAVORITES_UPDATED": "Favoriten wurden aktualisiert!", + "SORT_BY": "Sortieren nach", + "SORT_BY_DATE": "Nach Datum sortieren (Neueste zuerst)", + "SORT_BY_RATING": "Nach Bewertung sortieren", + "SORT_BY_NAME": "Nach Name sortieren (A-Z)" }, "EPG": { + "TITLE": "Elektronischer Programmführer", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Versuchen Sie bitte die TV-Guide Einstellungen zu ändern.", "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Ups, leider ist das TV-Programm nicht verfügbar.", "EPG_NOT_AVAILABLE_DATE": "Ups, leider ist das TV-Programm für das ausgewählte Datum nicht verfügbar", @@ -26,6 +31,7 @@ "FETCH_EPG": "EPG-Daten werden geladen...", "ERROR": "Ooops, EPG konnte nicht geladen werden", "DOWNLOAD_SUCCESS": "EPG wurde erfolgreich geladen.", + "FETCH_SUCCESS": "EPG wurde erfolgreich abgerufen", "PROGRAM_DIALOG": { "PROGRAM_DETAILS": "Details zum TV-Programm", "TITLE": "Titel", @@ -74,19 +80,32 @@ "PASSWORD": "Passwort", "SERVER_URL": "Server-URL", "USERNAME": "Nutzername", - "MAC_ADDRESS": "MAC-Adresse" + "MAC_ADDRESS": "MAC-Adresse", + "PLAYLIST_EXPORT_SUCCESS": "Playlist wurde erfolgreich exportiert.", + "PLAYLIST_EXPORT_ERROR": "Fehler: Playlist-Export fehlgeschlagen." }, "UPDATED": "Aktualisiert am", "REFRESH": "Aktualisieren", + "REFRESH_XTREAM": "Xtream-Playlist vom Server aktualisieren", + "REFRESH_XTREAM_DIALOG": { + "TITLE": "Xtream-Playlist aktualisieren", + "MESSAGE": "Dadurch werden lokal zwischengespeicherte Kategorien und Streams gelöscht und alles vom Remote-Server erneut importiert. Ihre Favoriten und kürzlich angesehenen Elemente bleiben erhalten. Möchten Sie wirklich fortfahren?", + "STARTED": "Aktualisierung gestartet. Navigation zur Playlist...", + "ERROR": "Playlist-Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut." + }, "REMOVE_DIALOG": { "MESSAGE": "Sind Sie sicher, dass Sie diese Wiedergabeliste vollständig löschen möchten", - "TITLE": "Wiedergabeliste entfernen" + "TITLE": "Wiedergabeliste entfernen", + "SUCCESS": "Playlist wurde erfolgreich entfernt" }, "GLOBAL_FAVORITES": "Globale Favoriten", - "GLOBAL_FAVORITES_DESCRIPTION": "Auto-generated playlist with aggregated favorites from all playlists", + "GLOBAL_FAVORITES_DESCRIPTION": "Automatisch generierte Playlist mit aggregierten Favoriten aus allen Playlists", "MY_PLAYLISTS": "Meine Playlisten", "MY_PLAYLISTS_SUBTITLE": "Alle verfügbaren Playlisten", "PLAYLIST_UPDATE_SUCCESS": "Erfolg! Die Playlist wurde erfolgreich aktualisiert.", + "PLAYLIST_UPDATE_ERROR": "Fehler beim Aktualisieren der Playlist-Details.", + "AUTO_REFRESH_UPDATE_SUCCESS": "Erfolg! Die Playlists wurden erfolgreich aktualisiert", + "AUTO_REFRESH_ENABLED": "Auto-Refresh aktiviert", "STALKER_PORTAL": "Stalker-Portal", "XTREAM_PLAYLIST": "Xtream-Playlist" }, @@ -100,7 +119,8 @@ }, "URL_UPLOAD": { "ADD_PLAYLIST": "Playlist hinzufügen", - "PLAYLIST_URL": "URL der Playlist" + "PLAYLIST_URL": "URL der Playlist", + "CORS_NOTE": "Hinweis: Um CORS-Probleme zu vermeiden, verwendet die Anwendung an dieser Stelle einen öffentlichen CORS-Proxy-Dienst. Wenn Sie eine Playlist mit sensiblen Daten hochladen möchten, ist es sicherer, sie aus einer Datei zu importieren." }, "TEXT_IMPORT": { "LABEL": "Füge m3u(8)-Wiedergabeliste hier als Text ein", @@ -123,8 +143,16 @@ "URL_VALIDATION_ERROR": "Sollte eine gültige URL mit Protokoll sein (z. B. http://example.com/c oder https://example.com/stalker_portal/c)" }, "FILTER_BY_TYPE": "Nach Typ filtern", - "IMPORTED_AS_TEXT": "Als Text importiert", "FILTER_BY_NAME": "Nach Namen filtern", + "SORT_PLAYLISTS": "Playlisten sortieren", + "SORT_BY": "Sortieren nach", + "SORT_OPTIONS": { + "NAME_ASC": "Name (A-Z)", + "NAME_DESC": "Name (Z-A)", + "NEWEST": "Hinzugefügt (Neueste zuerst)", + "OLDEST": "Hinzugefügt (Älteste zuerst)" + }, + "IMPORTED_AS_TEXT": "Als Text importiert", "PARSING_ERROR": "Fehler: Parsen fehlgeschlagen, keine gültige Playlist:" }, "LANGUAGES": { @@ -161,9 +189,11 @@ "VERSION": "Version", "VIDEO_PLAYER_LABEL": "Video player", "VIDEO_PLAYER_PLACEHOLDER": "Option auswählen", + "STREAM_FORMAT": "Stream-Format", "SETTINGS_SAVED": "Einstellungen wurden erfolgreich gespeichert!", "THEME": "Farbdesign", "SHOW_CAPTIONS": "Untertitel anzeigen", + "STREAM_FORMAT_DESCRIPTION": "Standard-Streamformat auswählen (Xtream)", "REMOVE_ALL": "Playlist entfernen", "REMOVE_ALL_BUTTON": "Entfernen", "REMOVE_DIALOG": { @@ -190,7 +220,14 @@ "VLC_PLAYER_PATH": "VLC-Playerpfad", "IMPORT_EXPORT_DATA_DESCRIPTION": "Exportieren oder importieren Sie Wiedergabelisten im JSON-Format, das in der Anwendung verwendet wird.", "EPG_NOTE_URL_TEXT": "Überprüfen Sie es hier", - "MPV_PLAYER_PATH_LABEL": "MPV-Pfad" + "MPV_PLAYER_PATH_LABEL": "MPV-Pfad", + "MPV_PLAYER_PATH_DESCRIPTION": "Legen Sie den Pfad zum MPV-Player in Ihrem System fest. Es wird der bereitgestellte Pfad zu einer MPV-Binärdatei anstelle des in $PATH gefundenen verwendet.", + "MPV_REUSE_INSTANCE_LABEL": "MPV-Instanz wiederverwenden", + "MPV_REUSE_INSTANCE_DESCRIPTION": "Wenn aktiviert, wird dasselbe MPV-Fenster zum Abspielen verschiedener Streams wiederverwendet. Wenn deaktiviert, wird jeder Stream in einem neuen MPV-Fenster geöffnet.", + "REMOTE_CONTROL": "Fernbedienung", + "REMOTE_CONTROL_DESCRIPTION": "Fernsteuerung der Anwendung aktivieren.", + "REMOTE_CONTROL_PORT": "Fernsteuerungs-Port", + "REMOTE_CONTROL_PORT_DESCRIPTION": "Port für die Fernsteuerung der Anwendung. Standardmäßig wird Port 3000 verwendet." }, "THEMES": { "DARK_THEME": "Dunkel", @@ -291,6 +328,10 @@ "TITLE": "Leere Kategorie", "DESCRIPTION": "Diese Kategorie hat keinen Inhalt. Bitte wählen Sie eine andere Kategorie aus oder ändern Sie den Inhaltstyp" }, + "NO_CATEGORY_SELECTED": { + "TITLE": "Keine Kategorie ausgewählt", + "DESCRIPTION": "Bitte wählen Sie eine Kategorie aus, um den Inhalt anzuzeigen" + }, "UNAUTHORIZED": { "TITLE": "Nicht autorisiert", "DESCRIPTION": "Bitte überprüfen Sie die Anmeldeinformationen in den Playlist-Einstellungen oder entfernen Sie sie." @@ -307,6 +348,7 @@ }, "STREAM_URL_COPIED": "Stream-URL in die Zwischenablage kopiert", "EMPTY_LIST_VIEW": { + "TITLE": "Keine Ergebnisse gefunden", "NO_SEARCH_RESULTS": "Nichts gefunden. Versuchen Sie, Ihre Suchanfrage zu ändern" }, "REMOVED_FROM_FAVORITES": "Aus den Favoriten entfernt", @@ -316,12 +358,20 @@ }, "ABOUT": { "TITLE": "Über IPTVnator", - "VERSION": "Ausführung", + "DESCRIPTION": "Plattformübergreifende IPTV-Player-Anwendung mit mehreren Funktionen wie Unterstützung von M3U- und M3U8-Wiedergabelisten, Favoriten, TV-Guide, TV-Archiv/Catchup und mehr.", + "VERSION": "Version", "TWITTER_TOOLTIP": "Auf Twitter teilen", "GITHUB_TOOLTIP": "ITPVnator-Repository auf GitHub", "BUY_ME_A_COFFEE_TOOLTIP": "Buy me a coffee" }, "REFRESH": "Aktualisieren", "UPDATE_AVAILABLE": "Update verfügbar", - "INFORMATION": "Information" + "INFORMATION": "Information", + "REMOTE_CONTROL": { + "TITLE": "Fernbedienung", + "UP_CHANNEL": "Sender aufwärts", + "DOWN_CHANNEL": "Sender abwärts", + "HEADER": "IPTVNator Fernbedienung", + "FOOTER": "IPTVNator Fernbedienung" + } } \ No newline at end of file diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index b69f2fccb..a8a8a8a14 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -41,19 +41,32 @@ "PASSWORD": "Пароль", "SERVER_URL": "URL-адрес сервера", "USERNAME": "Имя пользователя", - "MAC_ADDRESS": "MAC-адрес" + "MAC_ADDRESS": "MAC-адрес", + "PLAYLIST_EXPORT_SUCCESS": "Плейлист был успешно экспортирован.", + "PLAYLIST_EXPORT_ERROR": "Ошибка: не удалось экспортировать плейлист." }, "UPDATED": "Обновлен", "REFRESH": "Обновить плейлист", + "REFRESH_XTREAM": "Обновить Xtream плейлист с сервера", + "REFRESH_XTREAM_DIALOG": { + "TITLE": "Обновить Xtream плейлист", + "MESSAGE": "Это удалит локально кэшированные категории и потоки и повторно импортирует все с удаленного сервера. Ваши избранные и недавно просмотренные элементы будут сохранены. Вы уверены, что хотите продолжить?", + "STARTED": "Обновление начато. Переход к плейлисту...", + "ERROR": "Не удалось обновить плейлист. Пожалуйста, попробуйте снова." + }, "REMOVE_DIALOG": { "MESSAGE": "Вы уверены, что хотите удалить этот плейлист?", - "TITLE": "Удалить плейлист" + "TITLE": "Удалить плейлист", + "SUCCESS": "Плейлист был успешно удален" }, "GLOBAL_FAVORITES": "Все фавориты", "GLOBAL_FAVORITES_DESCRIPTION": "Автоматически сгенерированный плейлист, включающий в себя фавориты из всех плейлистов", "MY_PLAYLISTS": "Мои плейлисты", "MY_PLAYLISTS_SUBTITLE": "Все имеющиеся плейлисты", "PLAYLIST_UPDATE_SUCCESS": "Плейлист был успешно обновлен!", + "PLAYLIST_UPDATE_ERROR": "Ошибка при обновлении деталей плейлиста.", + "AUTO_REFRESH_UPDATE_SUCCESS": "Успешно! Плейлисты были успешно обновлены", + "AUTO_REFRESH_ENABLED": "Автообновление включено", "STALKER_PORTAL": "Stalker портал", "XTREAM_PLAYLIST": "Xtream плейлист" }, @@ -65,7 +78,8 @@ }, "URL_UPLOAD": { "PLAYLIST_URL": "Ссылка на плейлист (*.m3u или *.m3u8)", - "ADD_PLAYLIST": "Добавить плейлист" + "ADD_PLAYLIST": "Добавить плейлист", + "CORS_NOTE": "Примечание: Чтобы избежать проблем с CORS, приложение использует публичный CORS-прокси сервис. Если вы хотите загрузить плейлист с конфиденциальными данными, безопаснее импортировать его из файла." }, "TEXT_IMPORT": { "LABEL": "Вставьте плейлист формата m3u(8) в данное поле", @@ -88,8 +102,16 @@ "URL_VALIDATION_ERROR": "Должен быть действительный URL-адрес с протоколом (например, http://example.com/c или https://example.com/stalker_portal/c)." }, "FILTER_BY_TYPE": "Фильтровать по типу", - "IMPORTED_AS_TEXT": "Импортировано как текст", "FILTER_BY_NAME": "Фильтровать по имени", + "SORT_PLAYLISTS": "Сортировать плейлисты", + "SORT_BY": "Сортировать по", + "SORT_OPTIONS": { + "NAME_ASC": "Имени (А-Я)", + "NAME_DESC": "Имени (Я-А)", + "NEWEST": "Дате добавления (сначала новые)", + "OLDEST": "Дате добавления (сначала старые)" + }, + "IMPORTED_AS_TEXT": "Импортировано как текст", "PARSING_ERROR": "Ошибка парсинга, недействительный плейлист:" }, "SETTINGS": { @@ -106,6 +128,7 @@ "BACK_TO_HOME": "Назад", "NEW_VERSION_AVAILABLE": "Доступна новая версия приложения", "LATEST_VERSION": "Вы используете последнюю версию приложения", + "STREAM_FORMAT": "Формат потока", "LANGUAGE": "Язык", "SETTINGS_SAVED": "Новые настройки были успешно сохранены!", "THEME": "Тема оформления", @@ -113,6 +136,7 @@ "ADD_EPG_SOURCE": "Добавить источник EPG", "EPG_URL_ERROR": "Введенное значение не является URL-адресом.", "VIDEO_PLAYER_DESCRIPTION": "Выбрать стандартный видео плеер", + "STREAM_FORMAT_DESCRIPTION": "Выбрать формат потока по умолчанию (Xtream)", "LANGUAGE_DESCRIPTION": "Выбрать язык приложения", "THEME_DESCRIPTION": "Выбрать визуальную тему оформления", "SHOW_CAPTIONS_DESCRIPTION": "Показать или скрыть субтитры по умолчанию.", @@ -120,7 +144,7 @@ "EPG_NOTE": "На данный момент EPG доступен только в нативной версии IPTVnator.", "EPG_NOTE_URL_TEXT": "Подробнее здесь", "IMPORT_EXPORT_DATA": "Импорт/экспорт плейлистов", - "IMPORT_EXPORT_DATA_DESCRIPTION": "Export or import playlists in the JSON format used within the application.", + "IMPORT_EXPORT_DATA_DESCRIPTION": "Экспорт или импорт плейлистов в формате JSON, используемом в приложении.", "IMPORT_DATA": "Импорт", "EXPORT_DATA": "Экспорт", "REMOVE_ALL": "Удалить плейлист", @@ -134,9 +158,16 @@ "PLAYLISTS_REMOVED": "Все плейлисты были удалены.", "VLC_PLAYER_PATH_LABEL": "Путь к VLC плееру", "VLC_PLAYER_PATH": "Путь к VLC плееру", + "VLC_PLAYER_PATH_DESCRIPTION": "Установите путь к проигрывателю VLC в вашей системе. \nОн будет использовать предоставленный путь к binary файлу VLC вместо пути по умолчанию.", "MPV_PLAYER_PATH_LABEL": "Путь к mpv плееру", "MPV_PLAYER_PATH": "Путь к mpv плееру", - "VLC_PLAYER_PATH_DESCRIPTION": "Установите путь к проигрывателю VLC в вашей системе. \nОн будет использовать предоставленный путь к binary файлу VLC вместо пути по умолчанию." + "MPV_PLAYER_PATH_DESCRIPTION": "Установите путь к проигрывателю MPV в вашей системе. Он будет использовать предоставленный путь к бинарному файлу MPV вместо найденного в $PATH.", + "MPV_REUSE_INSTANCE_LABEL": "Переиспользовать экземпляр MPV", + "MPV_REUSE_INSTANCE_DESCRIPTION": "Когда включено, одно и то же окно MPV будет использоваться для воспроизведения разных потоков. Когда отключено, каждый поток открывается в новом окне MPV.", + "REMOTE_CONTROL": "Дистанционное управление", + "REMOTE_CONTROL_DESCRIPTION": "Включить дистанционное управление приложением.", + "REMOTE_CONTROL_PORT": "Порт дистанционного управления", + "REMOTE_CONTROL_PORT_DESCRIPTION": "Порт, используемый для дистанционного управления приложением. По умолчанию используется порт 3000." }, "THEMES": { "DARK_THEME": "Тёмная тема", @@ -153,15 +184,20 @@ "NO_FAVORITES": "Избранных каналов пока нет", "USE_STAR_TO_FAVORITE": "Используйте иконку звезды, чтобы добавлять каналы в избранное", "REMOVE_FAVORITE": "Удалить из списка фаворитов", - "FAVORITES_UPDATED": "Список фаворитов был обновлен!" + "FAVORITES_UPDATED": "Список фаворитов был обновлен!", + "SORT_BY": "Сортировать по", + "SORT_BY_DATE": "Сортировать по дате добавления (сначала новые)", + "SORT_BY_RATING": "Сортировать по рейтингу", + "SORT_BY_NAME": "Сортировать по имени (А-Я)" }, "TOP_MENU": { "OPEN_CHANNELS_LIST": "Открыть список каналов", "TOGGLE_FAVORITE_FLAG": "Избранный", "OPEN_EPG_LIST": "Открыть программу (EPG)", - "OPEN_MULTI_EPG": "Open Multi-EPG view" + "OPEN_MULTI_EPG": "Открыть Multi-EPG вид" }, "EPG": { + "TITLE": "Электронный программный гид", "NEXT_DAY": "Следующий день", "PREVIOUS_DAY": "Предыдущий день", "LIVE_NOW": "Сейчас", @@ -173,6 +209,7 @@ "FETCH_EPG": "Загружаю программу ТВ-передач (EPG)...", "ERROR": "Ой, не удалось загрузить программу каналов (EPG)", "DOWNLOAD_SUCCESS": "Программа каналов (EPG) была успешно загружена.", + "FETCH_SUCCESS": "Программа каналов (EPG) была успешно получена", "PROGRAM_DIALOG": { "PROGRAM_DETAILS": "Подробности передачи", "TITLE": "Название", @@ -217,7 +254,7 @@ }, "ABOUT": { "TITLE": "О приложении", - "DESCRIPTION": "Cross-platform IPTV player application with multiple features, such as support of m3u and m3u8 playlists, favorites, TV guide, TV archive/catchup and more.", + "DESCRIPTION": "Кроссплатформенное приложение IPTV-плеера с множеством функций, таких как поддержка плейлистов m3u и m3u8, избранное, телегид, ТВ-архив/catchup и многое другое.", "VERSION": "Версия", "GITHUB_TOOLTIP": "Репозиторий приложения на GitHub", "BUY_ME_A_COFFEE_TOOLTIP": "Поддержать разработчика", @@ -293,7 +330,12 @@ "DESCRIPTION": "Пожалуйста, проверьте учетные данные в настройках плейлиста или удалите их." }, "NOT_FOUND": { - "TITLE": "Портал не найден" + "TITLE": "Портал не найден", + "DESCRIPTION": "Пожалуйста, проверьте учетные данные и URL портала в настройках плейлиста или удалите его." + }, + "NO_CATEGORY_SELECTED": { + "TITLE": "Категория не выбрана", + "DESCRIPTION": "Пожалуйста, выберите категорию для просмотра содержимого" }, "UNKNOWN_ERROR": { "TITLE": "Что-то пошло не так", @@ -303,12 +345,33 @@ }, "STREAM_URL_COPIED": "URL-адрес потока был скопирован в буфер обмена.", "EMPTY_LIST_VIEW": { + "TITLE": "Результаты не найдены", "NO_SEARCH_RESULTS": "Ничего не найдено, попробуйте изменить поисковый запрос" }, + "SIDEBAR": { + "MAIN": "Главная", + "MOVIES": "Фильмы", + "LIVE_TV": "Прямой эфир", + "SERIES": "Сериалы", + "LIBRARY": "Библиотека", + "SEARCH": "Поиск", + "RECENT": "Недавние", + "FAVORITES": "Избранное", + "HOME": "Домой", + "PLAYLIST_INFO": "Информация о плейлисте", + "ACCOUNT_INFO": "Информация об аккаунте" + }, "REMOVED_FROM_FAVORITES": "Удалено из избранного", "ADDED_TO_FAVORITES": "Добавлено в избранное", "SELECT_CATEGORY": "Выберите категорию", "PAGE": "Страница" }, - "INFORMATION": "Инфо" + "INFORMATION": "Инфо", + "REMOTE_CONTROL": { + "TITLE": "Дистанционное управление", + "UP_CHANNEL": "Канал вверх", + "DOWN_CHANNEL": "Канал вниз", + "HEADER": "IPTVNator Дистанционное управление", + "FOOTER": "IPTVNator Дистанционное управление" + } } \ No newline at end of file