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": [] 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/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 + ); } /** 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/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..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 @@ -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 { @@ -49,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/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 @@

{{ 'PORTALS.ALL_CATEGORIES' | translate }}

} @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 { 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 }}

- - - - - 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/portal-card-item.interface.ts b/apps/web/src/app/xtream/portal-card-item.interface.ts deleted file mode 100644 index 8812636d0..000000000 --- a/apps/web/src/app/xtream/portal-card-item.interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -// should represent the content of a card element - xtream category or VOD or any item from stalker, vportal etc -export interface PortalCardItem { - // TODO - id: string; - type: 'category' | 'vod' | 'serial' | 'live'; // TODO: enum - name: string; - coverUrl?: string; - streamType?: 'live' | 'movie'; // TODO: maybe not needed, since type is there - categoryId?: string; -} 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); - } -} diff --git a/apps/web/src/app/xtream/vod-details/vod-details.component.html b/apps/web/src/app/xtream/vod-details/vod-details.component.html index 837a9dac7..fe2750368 100644 --- a/apps/web/src/app/xtream/vod-details/vod-details.component.html +++ b/apps/web/src/app/xtream/vod-details/vod-details.component.html @@ -1,5 +1,5 @@
-
- @if (item.info?.movie_image) { - +
+ @if (item.info?.movie_image) { + - } @else { -
- } -
-
-

{{ item.info.name }}

- @if (item.info.description) { - - } - @if (item.info.releasedate) { - - } - @if (item.info.genre) { - - } - @if (item.info.country) { - - } - @if (item.info.actors) { -
-
{{ 'XTREAM.ACTORS' | translate }}:
- {{ item.info.actors }} -
- } - @if (item.info.director) { - - } - @if (item.info.duration) { - - } - @if (item.info.rating_imdb) { - - } - @if (item.info.rating_kinopoisk) { - - } -
-   - @if (!isFavorite) { - + alt="Movie poster" + /> } @else { - +
+ } +
+
+

{{ 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) { +
+@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) { diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index b8715234a..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", @@ -219,6 +256,7 @@ "RELEASE_DATE": "Veröffentlichungsdatum", "ACTORS": "Schauspieler", "PLAY": "Abspielen", + "OPENING_PLAYER": "Player wird geöffnet...", "GENRE": "Genre", "DURATION": "Dauer", "COUNTRY": "Land", @@ -233,7 +271,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": { @@ -265,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." @@ -281,19 +348,30 @@ }, "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", - "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", - "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/en.json b/apps/web/src/assets/i18n/en.json index 2451434ef..b587a74a5 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", @@ -337,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 9dec7a7d4..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": "Поддержать разработчика", @@ -230,6 +267,7 @@ "RELEASE_DATE": "Дата выпуска", "ACTORS": "Актеры", "PLAY": "Воспроизвести", + "OPENING_PLAYER": "Открытие плеера...", "GENRE": "Жанр", "DURATION": "Продолжительность", "COUNTRY": "Страна", @@ -243,7 +281,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": "Все категории", @@ -267,7 +330,12 @@ "DESCRIPTION": "Пожалуйста, проверьте учетные данные в настройках плейлиста или удалите их." }, "NOT_FOUND": { - "TITLE": "Портал не найден" + "TITLE": "Портал не найден", + "DESCRIPTION": "Пожалуйста, проверьте учетные данные и URL портала в настройках плейлиста или удалите его." + }, + "NO_CATEGORY_SELECTED": { + "TITLE": "Категория не выбрана", + "DESCRIPTION": "Пожалуйста, выберите категорию для просмотра содержимого" }, "UNKNOWN_ERROR": { "TITLE": "Что-то пошло не так", @@ -277,10 +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": "Добавлено в избранное" + "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 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 }} +
}