mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
Merge pull request #651 from 4gray/feat-account-info-add-translations
feat-account-info-add-translations
This commit is contained in:
27 files changed
+635
-575
No files matched your search
@@ -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": []
|
||||
|
||||
@@ -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<AppComponent>;
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<h2 mat-dialog-title>{{ 'XTREAM.ACCOUNT_INFO.TITLE' | translate }}</h2>
|
||||
<mat-dialog-content class="mat-typography">
|
||||
@if (accountInfo?.user_info?.message) {
|
||||
<div class="welcome-message">
|
||||
{{ accountInfo?.user_info?.message }}
|
||||
</div>
|
||||
}
|
||||
<div class="info-grid">
|
||||
<div class="info-section">
|
||||
<h3>{{ 'XTREAM.ACCOUNT_INFO.USER_INFO' | translate }}</h3>
|
||||
<mat-list>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.STATUS' | translate }}:
|
||||
<span [class.active]="isActive">{{
|
||||
accountInfo?.user_info?.status
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.USERNAME' | translate }}:
|
||||
{{ accountInfo?.user_info?.username }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.ACTIVE_CONNECTIONS' | translate }}:
|
||||
{{ accountInfo?.user_info?.active_cons }}/{{
|
||||
accountInfo?.user_info?.max_connections
|
||||
}}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.CREATED' | translate }}:
|
||||
{{ formattedCreatedDate }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.EXPIRES' | translate }}:
|
||||
{{ formattedExpDate }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.TRIAL_ACCOUNT' | translate }}:
|
||||
<span [class.trial]="isTrial">{{
|
||||
(isTrial
|
||||
? 'XTREAM.ACCOUNT_INFO.YES'
|
||||
: 'XTREAM.ACCOUNT_INFO.NO'
|
||||
) | translate
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.ALLOWED_FORMATS' | translate }}:
|
||||
<span class="formats">
|
||||
{{
|
||||
accountInfo?.user_info?.allowed_output_formats?.join(
|
||||
', '
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h3>{{ 'XTREAM.ACCOUNT_INFO.SERVER_INFO' | translate }}</h3>
|
||||
<mat-list>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.LIVE_TV' | translate }}:
|
||||
{{ liveStreamsCount }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.MOVIES' | translate }}:
|
||||
{{ vodStreamsCount }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.TV_SERIES' | translate }}:
|
||||
{{ seriesCount }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.URL' | translate }}:
|
||||
{{ accountInfo?.server_info?.url }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.PROTOCOL' | translate }}:
|
||||
{{ accountInfo?.server_info?.server_protocol }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.TIMEZONE' | translate }}:
|
||||
{{ accountInfo?.server_info?.timezone }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.SERVER_TIME' | translate }}:
|
||||
{{ accountInfo?.server_info?.time_now }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
{{ 'XTREAM.ACCOUNT_INFO.PORTS' | translate }}:
|
||||
<div class="ports">
|
||||
<span
|
||||
>{{ 'XTREAM.ACCOUNT_INFO.HTTP_PORT' | translate }}:
|
||||
{{ accountInfo?.server_info?.port }}</span
|
||||
>
|
||||
<span
|
||||
>{{ 'XTREAM.ACCOUNT_INFO.HTTPS_PORT' | translate }}:
|
||||
{{ accountInfo?.server_info?.https_port }}</span
|
||||
>
|
||||
<span
|
||||
>{{ 'XTREAM.ACCOUNT_INFO.RTMP_PORT' | translate }}:
|
||||
{{ accountInfo?.server_info?.rtmp_port }}</span
|
||||
>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close color="accent">
|
||||
{{ 'CLOSE' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -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: `
|
||||
<h2 mat-dialog-title>Account Information</h2>
|
||||
<mat-dialog-content class="mat-typography">
|
||||
@if (accountInfo?.user_info?.message) {
|
||||
<div class="welcome-message">
|
||||
{{ accountInfo?.user_info?.message }}
|
||||
</div>
|
||||
}
|
||||
<div class="info-grid">
|
||||
<div class="info-section">
|
||||
<h3>User Information</h3>
|
||||
<mat-list>
|
||||
<mat-list-item>
|
||||
Status:
|
||||
<span [class.active]="isActive">{{
|
||||
accountInfo?.user_info?.status
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
Username:
|
||||
{{ accountInfo?.user_info?.username }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
Active Connections:
|
||||
{{ accountInfo?.user_info?.active_cons }}/{{
|
||||
accountInfo?.user_info?.max_connections
|
||||
}}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
Created:
|
||||
{{ formattedCreatedDate }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
Expires: {{ formattedExpDate }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
Trial Account:
|
||||
<span [class.trial]="isTrial">{{
|
||||
isTrial ? 'Yes' : 'No'
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
Allowed Formats:
|
||||
<span class="formats">
|
||||
{{
|
||||
accountInfo?.user_info?.allowed_output_formats?.join(
|
||||
', '
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h3>Server Information</h3>
|
||||
<mat-list>
|
||||
<mat-list-item>
|
||||
Live TV:
|
||||
{{ liveStreamsCount }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
Movies:
|
||||
{{ vodStreamsCount }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
TV Series:
|
||||
{{ seriesCount }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
URL:
|
||||
{{ accountInfo?.server_info?.url }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
Protocol:
|
||||
{{ accountInfo?.server_info?.server_protocol }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
Timezone:
|
||||
{{ accountInfo?.server_info?.timezone }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
Server Time:
|
||||
{{ accountInfo?.server_info?.time_now }}
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
Ports:
|
||||
<div class="ports">
|
||||
<span
|
||||
>HTTP:
|
||||
{{ accountInfo?.server_info?.port }}</span
|
||||
>
|
||||
<span
|
||||
>HTTPS:
|
||||
{{
|
||||
accountInfo?.server_info?.https_port
|
||||
}}</span
|
||||
>
|
||||
<span
|
||||
>RTMP:
|
||||
{{
|
||||
accountInfo?.server_info?.rtmp_port
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close color="accent">
|
||||
{{ 'CLOSE' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
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
|
||||
|
||||
+8
-7
@@ -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) {
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
</div>
|
||||
} @else {
|
||||
<div class="no-channel-selected">
|
||||
<button mat-icon-button disabled>
|
||||
<mat-icon>category</mat-icon>
|
||||
</button>
|
||||
{{ 'PORTALS.SELECT_CATEGORY' | translate }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
<h2 mat-dialog-title>{{ title }}</h2>
|
||||
<mat-dialog-content class="content">
|
||||
<app-web-player-view [streamUrl]="streamUrl" />
|
||||
<mat-form-field class="link-input">
|
||||
<input
|
||||
matInput
|
||||
formControlName="streamUrl"
|
||||
[value]="streamUrl"
|
||||
disabled
|
||||
/>
|
||||
</mat-form-field>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="align-actions">
|
||||
<button
|
||||
mat-button
|
||||
[cdkCopyToClipboard]="streamUrl"
|
||||
(click)="showCopyNotification()"
|
||||
[matTooltip]="streamUrl"
|
||||
>
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
{{ 'PORTALS.COPY_STREAM_URL' | translate }}
|
||||
|
||||
@@ -5,12 +5,6 @@
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.link-input {
|
||||
width: 100%;
|
||||
padding-top: 5px;
|
||||
@include mat.form-field-density(-5);
|
||||
}
|
||||
|
||||
.align-actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||
import { Component, Inject, ViewEncapsulation } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { Component, inject, ViewEncapsulation } from '@angular/core';
|
||||
import { MatButton } 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 { MatIcon } from '@angular/material/icon';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { WebPlayerViewComponent } from 'shared-portals';
|
||||
|
||||
export interface PlayerDialogData {
|
||||
@@ -18,28 +17,27 @@ export interface PlayerDialogData {
|
||||
templateUrl: './player-dialog.component.html',
|
||||
imports: [
|
||||
ClipboardModule,
|
||||
MatButtonModule,
|
||||
MatButton,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
TranslateModule,
|
||||
MatIcon,
|
||||
MatTooltip,
|
||||
TranslatePipe,
|
||||
WebPlayerViewComponent,
|
||||
],
|
||||
styleUrl: './player-dialog.component.scss',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class PlayerDialogComponent {
|
||||
title: string;
|
||||
streamUrl: string;
|
||||
readonly data = inject<PlayerDialogData>(MAT_DIALOG_DATA);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private translateService = inject(TranslateService);
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) data: PlayerDialogData,
|
||||
private snackBar: MatSnackBar,
|
||||
private translateService: TranslateService
|
||||
) {
|
||||
this.streamUrl = data.streamUrl;
|
||||
this.title = data.title;
|
||||
readonly title: string;
|
||||
readonly streamUrl: string;
|
||||
|
||||
constructor() {
|
||||
this.streamUrl = this.data.streamUrl;
|
||||
this.title = this.data.title;
|
||||
}
|
||||
|
||||
showCopyNotification() {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CommonModule, KeyValuePipe } from '@angular/common';
|
||||
import { DatePipe, KeyValuePipe, TitleCasePipe } from '@angular/common';
|
||||
import { Component, computed, inject, Optional } from '@angular/core';
|
||||
import { MatButtonModule, MatIconButton } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatButton, MatIconButton } from '@angular/material/button';
|
||||
import { MatCard } from '@angular/material/card';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { XtreamStore } from '../xtream.store';
|
||||
@@ -11,12 +11,14 @@ import { XtreamStore } from '../xtream.store';
|
||||
@Component({
|
||||
selector: 'app-recently-viewed',
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
DatePipe,
|
||||
KeyValuePipe,
|
||||
MatButton,
|
||||
MatCard,
|
||||
MatIcon,
|
||||
MatIconButton,
|
||||
MatIconButton,
|
||||
TitleCasePipe,
|
||||
],
|
||||
templateUrl: './recently-viewed.component.html',
|
||||
styleUrl: './recently-viewed.component.scss',
|
||||
|
||||
@@ -21,37 +21,35 @@
|
||||
(error)="
|
||||
$event.target.src = './assets/images/default-poster.png'
|
||||
"
|
||||
alt="Movie poster"
|
||||
/>
|
||||
} @else {
|
||||
<img src="./assets/images/default-poster.png" />
|
||||
<img
|
||||
src="./assets/images/default-poster.png"
|
||||
alt="Default poster"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div class="details">
|
||||
<h2>{{ item?.info?.name || item?.movie_data?.name }}</h2>
|
||||
@if (item?.info?.description || item?.info?.plot) {
|
||||
<label>
|
||||
<div>
|
||||
{{ item?.info?.description || item?.info?.plot }}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
@if (item?.info?.releasedate) {
|
||||
<label>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.RELEASE_DATE' | translate }}:
|
||||
</div>
|
||||
{{ item?.info?.releasedate }}
|
||||
</label>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.RELEASE_DATE' | translate }}:
|
||||
</div>
|
||||
{{ item?.info?.releasedate }}
|
||||
}
|
||||
@if (item?.info?.genre) {
|
||||
<label>
|
||||
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
|
||||
{{ item?.info?.genre }}
|
||||
</label>
|
||||
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
|
||||
{{ item?.info?.genre }}
|
||||
}
|
||||
@if (item?.info?.country) {
|
||||
<label>
|
||||
<div class="label">{{ 'XTREAM.COUNTRY' | translate }}:</div>
|
||||
{{ item?.info?.country }}
|
||||
</label>
|
||||
<div class="label">{{ 'XTREAM.COUNTRY' | translate }}:</div>
|
||||
{{ item?.info?.country }}
|
||||
}
|
||||
@if (item?.info?.actors || item?.info?.cast) {
|
||||
<div>
|
||||
@@ -60,36 +58,22 @@
|
||||
</div>
|
||||
}
|
||||
@if (item?.info?.director) {
|
||||
<label>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.DIRECTOR' | translate }}:
|
||||
</div>
|
||||
{{ item?.info?.director }}
|
||||
</label>
|
||||
<div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div>
|
||||
{{ item?.info?.director }}
|
||||
}
|
||||
@if (item?.info?.duration) {
|
||||
<label>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.DURATION' | translate }}:
|
||||
</div>
|
||||
{{ item?.info?.duration }}
|
||||
</label>
|
||||
<div class="label">{{ 'XTREAM.DURATION' | translate }}:</div>
|
||||
{{ item?.info?.duration }}
|
||||
}
|
||||
@if (item?.info?.rating_imdb) {
|
||||
<label>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.IMDB_RATING' | translate }}:
|
||||
</div>
|
||||
{{ item?.info?.rating_imdb }}
|
||||
</label>
|
||||
<div class="label">{{ 'XTREAM.IMDB_RATING' | translate }}:</div>
|
||||
{{ item?.info?.rating_imdb }}
|
||||
}
|
||||
@if (item?.info?.rating_kinopoisk) {
|
||||
<label>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.KINOPOISK_RATING' | translate }}:
|
||||
</div>
|
||||
{{ item?.info?.rating_kinopoisk }}
|
||||
</label>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.KINOPOISK_RATING' | translate }}:
|
||||
</div>
|
||||
{{ item?.info?.rating_kinopoisk }}
|
||||
}
|
||||
<div class="action-buttons">
|
||||
<button mat-flat-button color="accent" (click)="playVod(item)">
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<h2 mat-dialog-title>{{ title }}</h2>
|
||||
<mat-dialog-content class="content">
|
||||
<app-web-player-view [streamUrl]="streamUrl" />
|
||||
<mat-form-field class="link-input">
|
||||
<input
|
||||
matInput
|
||||
formControlName="streamUrl"
|
||||
[value]="streamUrl"
|
||||
disabled
|
||||
/>
|
||||
</mat-form-field>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="align-actions">
|
||||
<button
|
||||
mat-button
|
||||
[cdkCopyToClipboard]="streamUrl"
|
||||
(click)="showCopyNotification()"
|
||||
>
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
{{ 'PORTALS.COPY_STREAM_URL' | translate }}
|
||||
</button>
|
||||
<button mat-button mat-dialog-close color="accent">
|
||||
{{ 'CLOSE' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<div
|
||||
class="container"
|
||||
class="container"
|
||||
[style.background]="
|
||||
item?.info?.backdrop_path && item?.info?.backdrop_path.length > 0
|
||||
? 'linear-gradient(to top, rgba(29,29,29,1) 0%, rgba(0,0,0,0.5) 100%), url(' +
|
||||
@@ -7,122 +7,109 @@
|
||||
') repeat-x'
|
||||
: 'none'
|
||||
"
|
||||
>
|
||||
<div class="image">
|
||||
@if (item.info?.movie_image) {
|
||||
<img
|
||||
[src]="item.info?.movie_image"
|
||||
>
|
||||
<div class="image">
|
||||
@if (item.info?.movie_image) {
|
||||
<img
|
||||
[src]="item.info?.movie_image"
|
||||
(error)="
|
||||
$event.target.src = './assets/images/default-poster.png'
|
||||
"
|
||||
/>
|
||||
} @else {
|
||||
<div class="placeholder-cover"></div>
|
||||
}
|
||||
</div>
|
||||
<div class="details">
|
||||
<h2>{{ item.info.name }}</h2>
|
||||
@if (item.info.description) {
|
||||
<label>
|
||||
{{ item.info.description }}
|
||||
</label>
|
||||
}
|
||||
@if (item.info.releasedate) {
|
||||
<label>
|
||||
<div class="label">{{ 'XTREAM.RELEASE_DATE' | translate }}:</div>
|
||||
{{ item.info.releasedate }}
|
||||
</label>
|
||||
}
|
||||
@if (item.info.genre) {
|
||||
<label>
|
||||
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
|
||||
{{ item.info.genre }}
|
||||
</label>
|
||||
}
|
||||
@if (item.info.country) {
|
||||
<label>
|
||||
<div class="label">{{ 'XTREAM.COUNTRY' | translate }}:</div>
|
||||
{{ item.info.country }}
|
||||
</label>
|
||||
}
|
||||
@if (item.info.actors) {
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.ACTORS' | translate }}:</div>
|
||||
{{ item.info.actors }}
|
||||
</div>
|
||||
}
|
||||
@if (item.info.director) {
|
||||
<label>
|
||||
<div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div>
|
||||
{{ item.info.director }}
|
||||
</label>
|
||||
}
|
||||
@if (item.info.duration) {
|
||||
<label>
|
||||
<div class="label">{{ 'XTREAM.DURATION' | translate }}:</div>
|
||||
{{ item.info.duration }}
|
||||
</label>
|
||||
}
|
||||
@if (item.info.rating_imdb) {
|
||||
<label>
|
||||
<div class="label">{{ 'XTREAM.IMDB_RATING' | translate }}:</div>
|
||||
{{ item.info.rating_imdb }}
|
||||
</label>
|
||||
}
|
||||
@if (item.info.rating_kinopoisk) {
|
||||
<label>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.KINOPOISK_RATING' | translate }}:
|
||||
</div>
|
||||
{{ item.info.rating_kinopoisk }}
|
||||
</label>
|
||||
}
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="accent"
|
||||
(click)="playClicked.emit(item)"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
{{ 'XTREAM.PLAY' | translate }}</button
|
||||
>
|
||||
@if (!isFavorite) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star_outline</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
alt="Movie poster"
|
||||
/>
|
||||
} @else {
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
<div class="placeholder-cover"></div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (item.info.youtube_trailer) {
|
||||
<div class="details">
|
||||
<h2>{{ item.info.name }}</h2>
|
||||
@if (item.info.description) {
|
||||
<div>
|
||||
{{ item.info.description }}
|
||||
</div>
|
||||
}
|
||||
@if (item.info.releasedate) {
|
||||
<div class="label">{{ 'XTREAM.RELEASE_DATE' | translate }}:</div>
|
||||
{{ item.info.releasedate }}
|
||||
}
|
||||
@if (item.info.genre) {
|
||||
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
|
||||
{{ item.info.genre }}
|
||||
}
|
||||
@if (item.info.country) {
|
||||
<div class="label">{{ 'XTREAM.COUNTRY' | translate }}:</div>
|
||||
{{ item.info.country }}
|
||||
}
|
||||
@if (item.info.actors) {
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.ACTORS' | translate }}:</div>
|
||||
{{ item.info.actors }}
|
||||
</div>
|
||||
}
|
||||
@if (item.info.director) {
|
||||
<div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div>
|
||||
{{ item.info.director }}
|
||||
}
|
||||
@if (item.info.duration) {
|
||||
<div class="label">{{ 'XTREAM.DURATION' | translate }}:</div>
|
||||
{{ item.info.duration }}
|
||||
}
|
||||
@if (item.info.rating_imdb) {
|
||||
<div class="label">{{ 'XTREAM.IMDB_RATING' | translate }}:</div>
|
||||
{{ item.info.rating_imdb }}
|
||||
}
|
||||
@if (item.info.rating_kinopoisk) {
|
||||
<div class="label">
|
||||
{{ 'XTREAM.KINOPOISK_RATING' | translate }}:
|
||||
</div>
|
||||
{{ item.info.rating_kinopoisk }}
|
||||
}
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="accent"
|
||||
(click)="playClicked.emit(item)"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
{{ 'XTREAM.PLAY' | translate }}</button
|
||||
>
|
||||
@if (!isFavorite) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star_outline</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (item.info.youtube_trailer) {
|
||||
<div class="youtube-trailer">
|
||||
<h3>{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}</h3>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="450"
|
||||
[src]="
|
||||
'https://www.youtube.com/embed/' + item.info?.youtube_trailer | safe
|
||||
"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
<h3>{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}</h3>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="450"
|
||||
[src]="
|
||||
'https://www.youtube.com/embed/' + item.info?.youtube_trailer
|
||||
| safe
|
||||
"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 Дистанционное управление"
|
||||
}
|
||||
}
|
||||
+16
-11
@@ -26,7 +26,7 @@
|
||||
<mat-list-option
|
||||
*cdkVirtualFor="
|
||||
let item of channels
|
||||
| filterBy : searchString() : 'name';
|
||||
| filterBy: searchString() : 'name';
|
||||
trackBy: trackBy;
|
||||
templateCacheSize: 0
|
||||
"
|
||||
@@ -51,17 +51,22 @@
|
||||
</mat-selection-list>
|
||||
</div>
|
||||
</div>
|
||||
@if (player === 'videojs' || player === 'html5') { @if (streamUrl) {
|
||||
<div class="video-player">
|
||||
<app-web-player-view [streamUrl]="streamUrl" />
|
||||
</div>
|
||||
} } @if (streamUrl) {
|
||||
<div class="epg">
|
||||
<div class="epg-content">
|
||||
<app-epg-view [epgItems]="epgItems" />
|
||||
@if (player === 'videojs' || player === 'html5') {
|
||||
@if (streamUrl) {
|
||||
<div class="video-player">
|
||||
<app-web-player-view [streamUrl]="streamUrl" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (streamUrl) {
|
||||
<div class="epg">
|
||||
<div class="epg-content">
|
||||
<app-epg-view [epgItems]="epgItems" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="no-channel-selected">Please select a channel</div>
|
||||
<div class="no-channel-selected">
|
||||
{{ 'PORTALS.SELECT_CATEGORY' | translate }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
Reference in new issue
Block a user