mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
Merge pull request #613 from 4gray/chore/update-angular-to-v20
chore: update to angular v20
This commit is contained in:
47 files changed
+31451
-29290
No files matched your search
+22
-2
@@ -137,10 +137,30 @@
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "app",
|
||||
"style": "scss"
|
||||
"style": "scss",
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "app"
|
||||
"prefix": "app",
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+29850
-27749
File diff suppressed because it is too large.
Load diff
+148
-149
@@ -1,151 +1,150 @@
|
||||
{
|
||||
"name": "iptvnator",
|
||||
"version": "1.0.0-beta.7",
|
||||
"description": "IPTV player application.",
|
||||
"homepage": "https://github.com/4gray/iptvnator",
|
||||
"author": {
|
||||
"name": "4gray",
|
||||
"email": "fourgray@proton.me"
|
||||
},
|
||||
"keywords": [
|
||||
"angular",
|
||||
"electron",
|
||||
"typescript",
|
||||
"m3u",
|
||||
"m3u8",
|
||||
"player",
|
||||
"iptv",
|
||||
"video",
|
||||
"tv"
|
||||
],
|
||||
"main": "electron/main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:dev": "ng build -c dev",
|
||||
"build:prod": "ng build -c production",
|
||||
"build:web": "ng build -c web",
|
||||
"serve": "ng serve -c dev",
|
||||
"test": "ng test",
|
||||
"test:watch": "ng test --watch",
|
||||
"e2e": "npm run build:prod && npx playwright test --ui",
|
||||
"e2e:ci": "npx playwright test --retries 3 --workers=1",
|
||||
"version": "conventional-changelog -i CHANGELOG.md -p angular -s -r 0 && git add CHANGELOG.md",
|
||||
"lint": "ng lint",
|
||||
"build-pwa": "ng build -c production --base-href /iptvnator --deploy-url /iptvnator/",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular-builders/jest": "17.0.0",
|
||||
"@angular/animations": "19.2.0",
|
||||
"@angular/cdk": "19.2.1",
|
||||
"@angular/material": "19.2.1",
|
||||
"@ngrx/component-store": "19.0.1",
|
||||
"@ngrx/effects": "19.0.1",
|
||||
"@ngrx/entity": "19.0.1",
|
||||
"@ngrx/router-store": "19.0.1",
|
||||
"@ngrx/signals": "19.0.1",
|
||||
"@ngrx/store": "19.0.1",
|
||||
"@ngrx/store-devtools": "19.0.1",
|
||||
"@ngx-pwa/local-storage": "18.0.0",
|
||||
"@rx-angular/cdk": "18.0.0",
|
||||
"@rx-angular/template": "18.0.0",
|
||||
"@tauri-apps/api": "2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "2.2.0",
|
||||
"@tauri-apps/plugin-fs": "2.2.0",
|
||||
"@tauri-apps/plugin-http": "2.3.0",
|
||||
"@tauri-apps/plugin-os": "2.2.0",
|
||||
"@tauri-apps/plugin-process": "2.2.0",
|
||||
"@tauri-apps/plugin-shell": "2.2.0",
|
||||
"@tauri-apps/plugin-sql": "2.2.0",
|
||||
"@tauri-apps/plugin-updater": "2.5.1",
|
||||
"@tauri-apps/plugin-window-state": "2.2.1",
|
||||
"@videojs/http-streaming": "2.15.0",
|
||||
"@yangkghjh/videojs-aspect-ratio-panel": "0.0.1",
|
||||
"artplayer": "5.2.1",
|
||||
"axios": "1.12.0",
|
||||
"date-fns": "2.29.3",
|
||||
"dplayer": "1.27.1",
|
||||
"epg-parser": "0.1.6",
|
||||
"hls.js": "1.6.1",
|
||||
"iptv-playlist-parser": "github:4gray/iptv-playlist-parser",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.30.1",
|
||||
"ngx-indexed-db": "19.0.0",
|
||||
"ngx-skeleton-loader": "10.0.0",
|
||||
"ngx-whats-new": "0.4.0",
|
||||
"node-mpv": "github:4gray/Node-MPV",
|
||||
"rxjs": "7.8.1",
|
||||
"semver": "7.5.2",
|
||||
"squirrelly": "9.1.0",
|
||||
"uuid": "9.0.0",
|
||||
"video.js": "7.20.3",
|
||||
"videojs-contrib-quality-levels": "2.2.0",
|
||||
"videojs-hls-quality-selector": "1.1.4",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-eslint/builder": "19.2.0",
|
||||
"@angular-eslint/eslint-plugin": "19.2.0",
|
||||
"@angular-eslint/eslint-plugin-template": "19.2.0",
|
||||
"@angular-eslint/schematics": "19.2.0",
|
||||
"@angular-eslint/template-parser": "19.2.0",
|
||||
"@angular/build": "^19.2.17",
|
||||
"@angular/cli": "19.2.0",
|
||||
"@angular/common": "19.2.0",
|
||||
"@angular/compiler": "19.2.0",
|
||||
"@angular/compiler-cli": "19.2.0",
|
||||
"@angular/core": "19.2.0",
|
||||
"@angular/forms": "19.2.0",
|
||||
"@angular/language-service": "19.2.0",
|
||||
"@angular/platform-browser": "19.2.0",
|
||||
"@angular/platform-browser-dynamic": "19.2.0",
|
||||
"@angular/router": "19.2.0",
|
||||
"@angular/service-worker": "19.2.0",
|
||||
"@commitlint/cli": "18.4.3",
|
||||
"@commitlint/config-conventional": "18.4.3",
|
||||
"@eslint/eslintrc": "3.1.0",
|
||||
"@eslint/js": "9.9.1",
|
||||
"@ngrx/eslint-plugin": "^19.0.1",
|
||||
"@ngx-translate/core": "16.0.4",
|
||||
"@ngx-translate/http-loader": "16.0.1",
|
||||
"@playwright/test": "1.46.1",
|
||||
"@semantic-release/changelog": "6.0.3",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@semantic-release/npm": "12.0.1",
|
||||
"@tauri-apps/cli": "2.3.1",
|
||||
"@types/dplayer": "1.25.5",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/mocha": "9.0.0",
|
||||
"@types/node": "20.10.0",
|
||||
"@types/video.js": "7.3.29",
|
||||
"@types/videojs-hls-quality-selector": "1.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.2.0",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "7.0.2",
|
||||
"@typescript-eslint/parser": "^8.2.0",
|
||||
"conventional-changelog-cli": "5.0.0",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"globals": "15.9.0",
|
||||
"husky": "5.0.9",
|
||||
"jest": "29.7.0",
|
||||
"jest-preset-angular": "14.5.3",
|
||||
"ng-mocks": "14.13.2",
|
||||
"node-polyfill-webpack-plugin": "1.1.4",
|
||||
"npm-run-all": "4.1.5",
|
||||
"playwright": "1.46.1",
|
||||
"playwright-core": "1.46.1",
|
||||
"prettier": "3.3.3",
|
||||
"semantic-release": "19.0.3",
|
||||
"ts-jest": "29.2.6",
|
||||
"ts-node": "10.9.2",
|
||||
"tslib": "2.3.0",
|
||||
"typescript": "5.5.4",
|
||||
"wait-on": "6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"chrome 91"
|
||||
]
|
||||
"name": "iptvnator",
|
||||
"version": "1.0.0-beta.7",
|
||||
"description": "IPTV player application.",
|
||||
"homepage": "https://github.com/4gray/iptvnator",
|
||||
"author": {
|
||||
"name": "4gray",
|
||||
"email": "fourgray@proton.me"
|
||||
},
|
||||
"keywords": [
|
||||
"angular",
|
||||
"electron",
|
||||
"typescript",
|
||||
"m3u",
|
||||
"m3u8",
|
||||
"player",
|
||||
"iptv",
|
||||
"video",
|
||||
"tv"
|
||||
],
|
||||
"main": "electron/main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:dev": "ng build -c dev",
|
||||
"build:prod": "ng build -c production",
|
||||
"build:web": "ng build -c web",
|
||||
"serve": "ng serve -c dev",
|
||||
"test": "ng test",
|
||||
"test:watch": "ng test --watch",
|
||||
"e2e": "npm run build:prod && npx playwright test --ui",
|
||||
"e2e:ci": "npx playwright test --retries 3 --workers=1",
|
||||
"version": "conventional-changelog -i CHANGELOG.md -p angular -s -r 0 && git add CHANGELOG.md",
|
||||
"lint": "ng lint",
|
||||
"build-pwa": "ng build -c production --base-href /iptvnator --deploy-url /iptvnator/",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular-builders/jest": "17.0.0",
|
||||
"@angular/animations": "20.3.1",
|
||||
"@angular/cdk": "20.2.4",
|
||||
"@angular/material": "20.2.4",
|
||||
"@ngrx/component-store": "20.0.0",
|
||||
"@ngrx/effects": "20.0.0",
|
||||
"@ngrx/entity": "20.0.0",
|
||||
"@ngrx/router-store": "20.0.0",
|
||||
"@ngrx/signals": "20.0.0",
|
||||
"@ngrx/store": "20.0.0",
|
||||
"@ngrx/store-devtools": "20.0.0",
|
||||
"@ngx-pwa/local-storage": "20.0.0",
|
||||
"@rx-angular/cdk": "18.0.0",
|
||||
"@rx-angular/template": "18.0.0",
|
||||
"@tauri-apps/api": "2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "2.2.0",
|
||||
"@tauri-apps/plugin-fs": "2.2.0",
|
||||
"@tauri-apps/plugin-http": "2.3.0",
|
||||
"@tauri-apps/plugin-os": "2.2.0",
|
||||
"@tauri-apps/plugin-process": "2.2.0",
|
||||
"@tauri-apps/plugin-shell": "2.2.0",
|
||||
"@tauri-apps/plugin-sql": "2.2.0",
|
||||
"@tauri-apps/plugin-updater": "2.5.1",
|
||||
"@tauri-apps/plugin-window-state": "2.2.1",
|
||||
"@videojs/http-streaming": "2.15.0",
|
||||
"@yangkghjh/videojs-aspect-ratio-panel": "0.0.1",
|
||||
"artplayer": "5.2.1",
|
||||
"axios": "1.12.0",
|
||||
"date-fns": "2.29.3",
|
||||
"dplayer": "1.27.1",
|
||||
"epg-parser": "0.1.6",
|
||||
"hls.js": "1.6.1",
|
||||
"iptv-playlist-parser": "github:4gray/iptv-playlist-parser",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.30.1",
|
||||
"ngx-indexed-db": "19.0.0",
|
||||
"ngx-skeleton-loader": "10.0.0",
|
||||
"ngx-whats-new": "0.4.0",
|
||||
"node-mpv": "github:4gray/Node-MPV",
|
||||
"rxjs": "7.8.1",
|
||||
"semver": "7.5.2",
|
||||
"uuid": "9.0.0",
|
||||
"video.js": "7.20.3",
|
||||
"videojs-contrib-quality-levels": "2.2.0",
|
||||
"videojs-hls-quality-selector": "1.1.4",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-eslint/builder": "19.2.0",
|
||||
"@angular-eslint/eslint-plugin": "19.2.0",
|
||||
"@angular-eslint/eslint-plugin-template": "19.2.0",
|
||||
"@angular-eslint/schematics": "19.2.0",
|
||||
"@angular-eslint/template-parser": "19.2.0",
|
||||
"@angular/build": "^20.3.2",
|
||||
"@angular/cli": "20.3.2",
|
||||
"@angular/common": "20.3.1",
|
||||
"@angular/compiler": "20.3.1",
|
||||
"@angular/compiler-cli": "20.3.1",
|
||||
"@angular/core": "20.3.1",
|
||||
"@angular/forms": "20.3.1",
|
||||
"@angular/language-service": "20.3.1",
|
||||
"@angular/platform-browser": "20.3.1",
|
||||
"@angular/platform-browser-dynamic": "20.3.1",
|
||||
"@angular/router": "20.3.1",
|
||||
"@angular/service-worker": "20.3.1",
|
||||
"@commitlint/cli": "18.4.3",
|
||||
"@commitlint/config-conventional": "18.4.3",
|
||||
"@eslint/eslintrc": "3.1.0",
|
||||
"@eslint/js": "9.9.1",
|
||||
"@ngrx/eslint-plugin": "^20.0.0",
|
||||
"@ngx-translate/core": "16.0.4",
|
||||
"@ngx-translate/http-loader": "16.0.1",
|
||||
"@playwright/test": "1.46.1",
|
||||
"@semantic-release/changelog": "6.0.3",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@semantic-release/npm": "12.0.1",
|
||||
"@tauri-apps/cli": "2.3.1",
|
||||
"@types/dplayer": "1.25.5",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/mocha": "9.0.0",
|
||||
"@types/node": "20.10.0",
|
||||
"@types/video.js": "7.3.29",
|
||||
"@types/videojs-hls-quality-selector": "1.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.2.0",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "7.0.2",
|
||||
"@typescript-eslint/parser": "^8.2.0",
|
||||
"conventional-changelog-cli": "5.0.0",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"globals": "15.9.0",
|
||||
"husky": "5.0.9",
|
||||
"jest": "29.7.0",
|
||||
"jest-preset-angular": "14.5.3",
|
||||
"ng-mocks": "14.13.2",
|
||||
"node-polyfill-webpack-plugin": "1.1.4",
|
||||
"npm-run-all": "4.1.5",
|
||||
"playwright": "1.46.1",
|
||||
"playwright-core": "1.46.1",
|
||||
"prettier": "3.3.3",
|
||||
"semantic-release": "19.0.3",
|
||||
"ts-jest": "29.2.6",
|
||||
"ts-node": "10.9.2",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "5.9.2",
|
||||
"wait-on": "6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"chrome 107"
|
||||
]
|
||||
}
|
||||
@@ -257,6 +257,12 @@ pub async fn open_in_vlc<R: Runtime>(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(origin_url) = origin {
|
||||
if !origin_url.is_empty() {
|
||||
command.arg(format!("--http-origin={}", origin_url));
|
||||
}
|
||||
}
|
||||
|
||||
// Log the complete command line
|
||||
let command_str = format!(
|
||||
"{} {}",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<router-outlet></router-outlet>
|
||||
<!-- whats new dialog -->
|
||||
<ngx-whats-new
|
||||
*ngIf="isDialogVisible$ | async"
|
||||
@if (isDialogVisible$ | async) {
|
||||
<ngx-whats-new
|
||||
(closeModal)="setDialogVisibility(false)"
|
||||
[items]="modals"
|
||||
[options]="options"
|
||||
/>
|
||||
/>
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { isTauri } from '@tauri-apps/api/core';
|
||||
import { ask } from '@tauri-apps/plugin-dialog';
|
||||
import { relaunch } from '@tauri-apps/plugin-process';
|
||||
import { check } from '@tauri-apps/plugin-updater';
|
||||
import { ModalWindow } from 'ngx-whats-new/lib/modal-window.interface';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import * as semver from 'semver';
|
||||
import { IpcCommand } from '../../shared/ipc-command.class';
|
||||
@@ -47,7 +46,7 @@ export class AppComponent {
|
||||
options = this.whatsNewService.options;
|
||||
|
||||
/** Modals to show for the updated version of the application */
|
||||
modals: ModalWindow[] = [];
|
||||
modals: any[] = [];
|
||||
|
||||
/** List of ipc commands with function mapping */
|
||||
commandsList = [
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<div class="grid-container">
|
||||
<app-header
|
||||
[title]="'HOME.TITLE' | translate"
|
||||
[subtitle]="'HOME.SUBTITLE' | translate"
|
||||
<app-header
|
||||
[title]="'HOME.TITLE' | translate"
|
||||
[subtitle]="'HOME.SUBTITLE' | translate"
|
||||
/>
|
||||
<app-recent-playlists class="recent-playlists" />
|
||||
<mat-progress-bar *ngIf="isLoading" mode="indeterminate" />
|
||||
<app-recent-playlists class="recent-playlists" />
|
||||
@if (isLoading) {
|
||||
<mat-progress-bar mode="indeterminate" />
|
||||
}
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NgIf } from '@angular/common';
|
||||
|
||||
import { Component, NgZone } from '@angular/core';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
@@ -16,12 +16,11 @@ import { RecentPlaylistsComponent } from './recent-playlists/recent-playlists.co
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: ['./home.component.scss'],
|
||||
imports: [
|
||||
HeaderComponent,
|
||||
MatProgressBarModule,
|
||||
NgIf,
|
||||
RecentPlaylistsComponent,
|
||||
TranslatePipe,
|
||||
],
|
||||
HeaderComponent,
|
||||
MatProgressBarModule,
|
||||
RecentPlaylistsComponent,
|
||||
TranslatePipe
|
||||
],
|
||||
})
|
||||
export class HomeComponent {
|
||||
/** Loading spinner state */
|
||||
|
||||
@@ -9,75 +9,69 @@
|
||||
autocomplete="off"
|
||||
[placeholder]="'HOME.PLAYLISTS.SEARCH_PLAYLISTS' | translate"
|
||||
(input)="onSearchQueryUpdate(searchQuery.value)"
|
||||
/></mat-form-field>
|
||||
/>
|
||||
</mat-form-field>
|
||||
@defer {
|
||||
<mat-nav-list
|
||||
*ngIf="playlists$ | async as playlists"
|
||||
class="playlists-list"
|
||||
>
|
||||
<mat-list-item
|
||||
*ngIf="playlists.length === 0; else playlistsTemplate"
|
||||
>
|
||||
<mat-icon matListItemIcon
|
||||
>search</mat-icon
|
||||
>
|
||||
<div matListItemTitle>
|
||||
{{ 'HOME.PLAYLISTS.NO_PLAYLISTS' | translate }}
|
||||
</div>
|
||||
<div matListItemLine class="meta">
|
||||
{{ 'HOME.PLAYLISTS.ADD_FIRST' | translate }}
|
||||
</div>
|
||||
</mat-list-item>
|
||||
|
||||
<ng-template #playlistsTemplate>
|
||||
<mat-list-item (click)="getGlobalFavorites()">
|
||||
<mat-icon class="favorites-icon" matListItemIcon
|
||||
>star</mat-icon
|
||||
>
|
||||
<div matListItemTitle>
|
||||
{{ 'HOME.PLAYLISTS.GLOBAL_FAVORITES' | translate }}
|
||||
</div>
|
||||
<div matListItemLine class="meta">
|
||||
{{
|
||||
'HOME.PLAYLISTS.GLOBAL_FAVORITES_DESCRIPTION'
|
||||
| translate
|
||||
}}
|
||||
</div>
|
||||
</mat-list-item>
|
||||
|
||||
<app-playlist-item
|
||||
*ngFor="
|
||||
let item of playlists;
|
||||
last as last;
|
||||
trackBy: trackByFn
|
||||
"
|
||||
[item]="item"
|
||||
[showActions]="!sidebarMode"
|
||||
(editPlaylistClicked)="openInfoDialog($event)"
|
||||
(playlistClicked)="getPlaylist(item)"
|
||||
(refreshClicked)="refreshPlaylist($event)"
|
||||
(removeClicked)="removeClicked($event)"
|
||||
/>
|
||||
</ng-template>
|
||||
</mat-nav-list>
|
||||
@if (playlists$ | async; as playlists) {
|
||||
<mat-nav-list class="playlists-list">
|
||||
@if (playlists.length === 0) {
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>search</mat-icon>
|
||||
<div matListItemTitle>
|
||||
{{ 'HOME.PLAYLISTS.NO_PLAYLISTS' | translate }}
|
||||
</div>
|
||||
<div matListItemLine class="meta">
|
||||
{{ 'HOME.PLAYLISTS.ADD_FIRST' | translate }}
|
||||
</div>
|
||||
</mat-list-item>
|
||||
} @else {
|
||||
<mat-list-item (click)="getGlobalFavorites()">
|
||||
<mat-icon class="favorites-icon" matListItemIcon
|
||||
>star</mat-icon
|
||||
>
|
||||
<div matListItemTitle>
|
||||
{{ 'HOME.PLAYLISTS.GLOBAL_FAVORITES' | translate }}
|
||||
</div>
|
||||
<div matListItemLine class="meta">
|
||||
{{
|
||||
'HOME.PLAYLISTS.GLOBAL_FAVORITES_DESCRIPTION'
|
||||
| translate
|
||||
}}
|
||||
</div>
|
||||
</mat-list-item>
|
||||
@for (item of playlists; track item._id; let last = $last) {
|
||||
<app-playlist-item
|
||||
[item]="item"
|
||||
[showActions]="!sidebarMode"
|
||||
(editPlaylistClicked)="openInfoDialog($event)"
|
||||
(playlistClicked)="getPlaylist(item)"
|
||||
(refreshClicked)="refreshPlaylist($event)"
|
||||
(removeClicked)="removeClicked($event)"
|
||||
/>
|
||||
}
|
||||
}
|
||||
</mat-nav-list>
|
||||
}
|
||||
} @placeholder {
|
||||
<div id="skeleton-container">
|
||||
<div class="skeleton-item" *ngFor="let a of [].constructor(10)">
|
||||
<ngx-skeleton-loader count="1" appearance="circle" />
|
||||
<div class="text">
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '30%', height: '20px' }"
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '50%', height: '10px' }"
|
||||
/>
|
||||
@for (a of ghostElements; track $index) {
|
||||
<div class="skeleton-item">
|
||||
<ngx-skeleton-loader count="1" appearance="circle" />
|
||||
<div class="text">
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '30%', height: '20px' }"
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '50%', height: '10px' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<ngx-skeleton-loader count="2" appearance="circle" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<ngx-skeleton-loader count="2" appearance="circle" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
@@ -56,8 +56,6 @@ import { PlaylistItemComponent } from './playlist-item/playlist-item.component';
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
NgFor,
|
||||
NgIf,
|
||||
NgxSkeletonLoaderComponent,
|
||||
PlaylistItemComponent,
|
||||
TranslatePipe,
|
||||
@@ -66,7 +64,9 @@ import { PlaylistItemComponent } from './playlist-item/playlist-item.component';
|
||||
export class RecentPlaylistsComponent implements OnDestroy {
|
||||
@ViewChild('searchQuery') searchQueryInput!: ElementRef<HTMLInputElement>;
|
||||
|
||||
searchQuery = new BehaviorSubject('');
|
||||
readonly searchQuery = new BehaviorSubject('');
|
||||
|
||||
readonly ghostElements = new Array(10);
|
||||
|
||||
playlists$ = combineLatest([
|
||||
this.store.select(selectAllPlaylistsMeta),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
@@ -14,7 +14,7 @@ import { Channel } from '../../../../../shared/channel.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-art-player',
|
||||
imports: [CommonModule],
|
||||
imports: [],
|
||||
template: `<div #artplayer class="artplayer-container"></div>`,
|
||||
styles: [
|
||||
`
|
||||
|
||||
@@ -109,3 +109,15 @@
|
||||
transform: scale(2);
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 599px) {
|
||||
.radio-logo {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
|
||||
img {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { NgIf } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
@@ -29,59 +28,59 @@ import { setAdjacentChannelAsActive } from '../../../state/actions';
|
||||
<mat-icon>skip_previous</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="icon-button-large"
|
||||
mat-icon-button
|
||||
(click)="play()"
|
||||
*ngIf="playState === 'paused'"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
class="icon-button-large"
|
||||
mat-icon-button
|
||||
(click)="stop()"
|
||||
*ngIf="playState === 'play'"
|
||||
>
|
||||
<mat-icon>pause</mat-icon>
|
||||
</button>
|
||||
@if (playState === 'paused') {
|
||||
<button
|
||||
class="icon-button-large"
|
||||
mat-icon-button
|
||||
(click)="play()"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (playState === 'play') {
|
||||
<button
|
||||
class="icon-button-large"
|
||||
mat-icon-button
|
||||
(click)="stop()"
|
||||
>
|
||||
<mat-icon>pause</mat-icon>
|
||||
</button>
|
||||
}
|
||||
<button mat-icon-button (click)="switchChannel('next')">
|
||||
<mat-icon>skip_next</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="volume-panel">
|
||||
<div class="playing" *ngIf="playState === 'play'">
|
||||
<span class="playing-bar playing-bar1"></span>
|
||||
<span class="playing-bar playing-bar2"></span>
|
||||
<span class="playing-bar playing-bar3"></span>
|
||||
</div>
|
||||
<div class="playing" *ngIf="playState === 'paused'">
|
||||
<span class="playing-bar-stopped playing-bar1"></span>
|
||||
<span class="playing-bar-stopped playing-bar2"></span>
|
||||
<span class="playing-bar-stopped playing-bar3"></span>
|
||||
</div>
|
||||
@if (playState === 'play') {
|
||||
<div class="playing">
|
||||
<span class="playing-bar playing-bar1"></span>
|
||||
<span class="playing-bar playing-bar2"></span>
|
||||
<span class="playing-bar playing-bar3"></span>
|
||||
</div>
|
||||
}
|
||||
@if (playState === 'paused') {
|
||||
<div class="playing">
|
||||
<span class="playing-bar-stopped playing-bar1"></span>
|
||||
<span class="playing-bar-stopped playing-bar2"></span>
|
||||
<span class="playing-bar-stopped playing-bar3"></span>
|
||||
</div>
|
||||
}
|
||||
<mat-slider min="0" max="1" step="0.1" color="accent">
|
||||
<input matSliderThumb [(ngModel)]="audio.volume" />
|
||||
</mat-slider>
|
||||
<button mat-icon-button (click)="mute()">
|
||||
<mat-icon *ngIf="audio.volume > 0 && !audio.muted"
|
||||
>volume_up</mat-icon
|
||||
>
|
||||
<mat-icon *ngIf="audio.volume === 0 || audio.muted"
|
||||
>volume_off</mat-icon
|
||||
>
|
||||
@if (audio.volume > 0 && !audio.muted) {
|
||||
<mat-icon>volume_up</mat-icon>
|
||||
}
|
||||
@if (audio.volume === 0 || audio.muted) {
|
||||
<mat-icon>volume_off</mat-icon>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./audio-player.component.scss'],
|
||||
imports: [
|
||||
MatSliderModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
NgIf,
|
||||
FormsModule,
|
||||
],
|
||||
imports: [MatSliderModule, MatIconModule, MatButtonModule, FormsModule],
|
||||
})
|
||||
export class AudioPlayerComponent implements OnChanges {
|
||||
@Input() icon: string;
|
||||
@@ -125,7 +124,6 @@ export class AudioPlayerComponent implements OnChanges {
|
||||
}
|
||||
|
||||
switchChannel(direction: 'next' | 'previous') {
|
||||
console.log(direction);
|
||||
this.store.dispatch(setAdjacentChannelAsActive({ direction }));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
@@ -13,7 +13,7 @@ import { Channel } from '../../../../../shared/channel.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-d-player',
|
||||
imports: [CommonModule],
|
||||
imports: [],
|
||||
template: `<div #dplayer class="dplayer-container"></div>`,
|
||||
styles: [
|
||||
`
|
||||
|
||||
+36
-34
@@ -1,40 +1,42 @@
|
||||
<h2 mat-dialog-title>
|
||||
{{ 'EPG.PROGRAM_DIALOG.PROGRAM_DETAILS' | translate }}
|
||||
{{ 'EPG.PROGRAM_DIALOG.PROGRAM_DETAILS' | translate }}
|
||||
</h2>
|
||||
<mat-dialog-content class="mat-typography">
|
||||
<ng-container *ngIf="epgProgram?.title?.length > 0">
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.TITLE' | translate }}
|
||||
</div>
|
||||
<p data-test="title">{{ epgProgram.title }}</p>
|
||||
<div class="subheading-2" *ngIf="epgProgram.title[0].lang">
|
||||
{{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }}
|
||||
</div>
|
||||
<p data-test="lang">{{ epgProgram.title[0].lang }}</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="epgProgram?.category?.length > 0">
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.CATEGORY' | translate }}
|
||||
</div>
|
||||
<p data-test="category">{{ epgProgram.category }}</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="epgProgram?.desc?.length > 0">
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.DESCRIPTION' | translate }}
|
||||
</div>
|
||||
<p data-test="desc">{{ epgProgram.desc }}</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="epgProgram?.rating?.length > 0">
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.PARENTAL_RATING_SYSTEM' | translate }}
|
||||
</div>
|
||||
<p data-test="rating">
|
||||
{{ epgProgram.rating[0].value }} [{{ epgProgram.rating[0].system }}]
|
||||
</p>
|
||||
</ng-container>
|
||||
@if (epgProgram?.title?.length > 0) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.TITLE' | translate }}
|
||||
</div>
|
||||
<p data-test="title">{{ epgProgram.title }}</p>
|
||||
@if (epgProgram.title[0].lang) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }}
|
||||
</div>
|
||||
}
|
||||
<p data-test="lang">{{ epgProgram.title[0].lang }}</p>
|
||||
}
|
||||
@if (epgProgram?.category?.length > 0) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.CATEGORY' | translate }}
|
||||
</div>
|
||||
<p data-test="category">{{ epgProgram.category }}</p>
|
||||
}
|
||||
@if (epgProgram?.desc?.length > 0) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.DESCRIPTION' | translate }}
|
||||
</div>
|
||||
<p data-test="desc">{{ epgProgram.desc }}</p>
|
||||
}
|
||||
@if (epgProgram?.rating?.length > 0) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.PARENTAL_RATING_SYSTEM' | translate }}
|
||||
</div>
|
||||
<p data-test="rating">
|
||||
{{ epgProgram.rating[0].value }} [{{ epgProgram.rating[0].system }}]
|
||||
</p>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close cdkFocusInitial color="accent">
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.CLOSE' | translate }}
|
||||
</button>
|
||||
<button mat-button mat-dialog-close cdkFocusInitial color="accent">
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.CLOSE' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { NgIf } from '@angular/common';
|
||||
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
@@ -9,7 +9,7 @@ import { EpgProgram } from './../../../models/epg-program.model';
|
||||
selector: 'app-epg-item-description',
|
||||
templateUrl: './epg-item-description.component.html',
|
||||
styleUrls: ['./epg-item-description.component.scss'],
|
||||
imports: [MatButtonModule, MatDialogModule, NgIf, TranslatePipe],
|
||||
imports: [MatButtonModule, MatDialogModule, TranslatePipe],
|
||||
})
|
||||
export class EpgItemDescriptionComponent {
|
||||
/** EPG program object */
|
||||
|
||||
@@ -1,136 +1,144 @@
|
||||
<div id="epg-navigation">
|
||||
<button
|
||||
[matTooltip]="'CLOSE' | translate"
|
||||
mat-button
|
||||
(click)="close()"
|
||||
color="accent"
|
||||
<button
|
||||
[matTooltip]="'CLOSE' | translate"
|
||||
mat-button
|
||||
(click)="close()"
|
||||
color="accent"
|
||||
>
|
||||
<mat-icon>close</mat-icon> {{ 'CLOSE' | translate }}
|
||||
<mat-icon>close</mat-icon> {{ 'CLOSE' | translate }}
|
||||
</button>
|
||||
<button
|
||||
[matTooltip]="'EPG.PREVIOUS_DAY' | translate"
|
||||
mat-icon-button
|
||||
(click)="switchDay('prev')"
|
||||
>
|
||||
<mat-icon>navigate_before</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
[matTooltip]="'EPG.NEXT_DAY' | translate"
|
||||
mat-icon-button
|
||||
(click)="switchDay('next')"
|
||||
>
|
||||
<mat-icon>navigate_next</mat-icon>
|
||||
</button>
|
||||
<div class="today-date">
|
||||
{{ today | momentDate: 'YYYYMMDD' : 'MMMM Do, dddd' }}
|
||||
</div>
|
||||
<button mat-icon-button (click)="zoomIn()" [disabled]="(hourWidth$ | async) >= 800">
|
||||
<mat-icon>zoom_in</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button [disabled]="(hourWidth$ | async) <= 50" (click)="zoomOut()">
|
||||
<mat-icon>zoom_out</mat-icon>
|
||||
</button>
|
||||
<div class="navigation-buttons">
|
||||
<button
|
||||
mat-button
|
||||
(click)="previousChannels()"
|
||||
[disabled]="channelsLowerRange === 0"
|
||||
matTooltip="Previous channels"
|
||||
>
|
||||
<mat-icon>keyboard_arrow_up</mat-icon> Previous channels
|
||||
</button>
|
||||
<button
|
||||
[matTooltip]="'EPG.NEXT_DAY' | translate"
|
||||
mat-icon-button
|
||||
(click)="switchDay('prev')"
|
||||
>
|
||||
<mat-icon>navigate_before</mat-icon>
|
||||
mat-button
|
||||
(click)="nextChannels()"
|
||||
[disabled]="isLastPage"
|
||||
matTooltip="Next channels"
|
||||
>
|
||||
<mat-icon>keyboard_arrow_down</mat-icon> Next channels
|
||||
</button>
|
||||
<button
|
||||
[matTooltip]="'EPG.PREVIOUS_DAY' | translate"
|
||||
mat-icon-button
|
||||
(click)="switchDay('next')"
|
||||
>
|
||||
<mat-icon>navigate_next</mat-icon>
|
||||
</button>
|
||||
<div class="today-date">
|
||||
{{ today | momentDate: 'YYYYMMDD' : 'MMMM Do, dddd' }}
|
||||
</div>
|
||||
<button mat-icon-button (click)="zoomIn()" [disabled]="(hourWidth$ | async) >= 800">
|
||||
<mat-icon>zoom_in</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button [disabled]="(hourWidth$ | async) <= 50" (click)="zoomOut()">
|
||||
<mat-icon>zoom_out</mat-icon>
|
||||
</button>
|
||||
<div class="navigation-buttons">
|
||||
<button
|
||||
mat-button
|
||||
(click)="previousChannels()"
|
||||
[disabled]="channelsLowerRange === 0"
|
||||
matTooltip="Previous channels"
|
||||
>
|
||||
<mat-icon>keyboard_arrow_up</mat-icon> Previous channels
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
(click)="nextChannels()"
|
||||
[disabled]="isLastPage"
|
||||
matTooltip="Next channels"
|
||||
>
|
||||
<mat-icon>keyboard_arrow_down</mat-icon> Next channels
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="parent" #epgContainer>
|
||||
<svg id="channels-column">
|
||||
<g *ngFor="let item of channels$ | async; let i = index; trackBy: trackByIndex">
|
||||
<!-- channel name -->
|
||||
<g class="channel">
|
||||
<rect
|
||||
width="100"
|
||||
[attr.height]="barHeight"
|
||||
[attr.y]="barHeight * i + barHeight"
|
||||
stroke="black"
|
||||
/>
|
||||
<foreignObject
|
||||
width="100"
|
||||
[attr.height]="barHeight"
|
||||
[attr.y]="barHeight * i + barHeight"
|
||||
>
|
||||
<div class="channel-name">
|
||||
<img
|
||||
*ngIf="item.icon"
|
||||
[src]="item.icon"
|
||||
[alt]="item.name"
|
||||
style="height: 20px; width: 20px; margin-right: 8px;"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</g>
|
||||
<svg id="channels-column">
|
||||
@for (item of channels$ | async; track $any(item).channel; let i = $index) {
|
||||
<g>
|
||||
<!-- channel name -->
|
||||
<g class="channel">
|
||||
<rect
|
||||
width="100"
|
||||
[attr.height]="barHeight"
|
||||
[attr.y]="barHeight * i + barHeight"
|
||||
stroke="black"
|
||||
/>
|
||||
<foreignObject
|
||||
width="100"
|
||||
[attr.height]="barHeight"
|
||||
[attr.y]="barHeight * i + barHeight"
|
||||
>
|
||||
<div class="channel-name">
|
||||
@if (item.icon) {
|
||||
<img
|
||||
[src]="item.icon"
|
||||
[alt]="item.name"
|
||||
style="height: 20px; width: 20px; margin-right: 8px;"
|
||||
>
|
||||
}
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</g>
|
||||
</g>
|
||||
}
|
||||
</svg>
|
||||
<div id="epg-container">
|
||||
<svg [attr.width]="(hourWidth$ | async) * 24" height="100%" id="epg-svg">
|
||||
<!-- time headline -->
|
||||
@for (hour of timeHeader; track hour) {
|
||||
<g>
|
||||
<rect
|
||||
[matTooltip]="hour + ':00'"
|
||||
[attr.width]="hourWidth$ | async"
|
||||
[attr.height]="barHeight"
|
||||
[attr.x]="hour * (hourWidth$ | async)"
|
||||
fill="#000"
|
||||
></rect>
|
||||
<text
|
||||
[attr.x]="hour * (hourWidth$ | async)"
|
||||
y="10"
|
||||
font-size="14"
|
||||
fill="white"
|
||||
transform="translate(20,20)"
|
||||
>
|
||||
{{ hour }}:00
|
||||
</text>
|
||||
</g>
|
||||
}
|
||||
|
||||
<!-- epg channels with programs -->
|
||||
@for (item of channels$ | async; track $any(item).channel; let i = $index) {
|
||||
<g>
|
||||
@for (program of item.programs; track trackByProgram($index, program)) {
|
||||
<g
|
||||
[attr.transform]="'translate(' + program.startPosition + ',' + (barHeight * i + barHeight) + ')'"
|
||||
class="program"
|
||||
(click)="showDescription(program)"
|
||||
>
|
||||
<rect
|
||||
[attr.width]="program.width"
|
||||
[attr.height]="barHeight"
|
||||
[matTooltip]="program.title"
|
||||
></rect>
|
||||
<foreignObject
|
||||
[attr.width]="program.width"
|
||||
[attr.height]="barHeight"
|
||||
>
|
||||
<div class="program-title">{{ program.title }}</div>
|
||||
</foreignObject>
|
||||
</g>
|
||||
}
|
||||
</g>
|
||||
}
|
||||
|
||||
<!-- current time line -->
|
||||
<line
|
||||
[attr.x1]="currentTimeLine"
|
||||
y1="0"
|
||||
[attr.x2]="currentTimeLine"
|
||||
[attr.y2]="(channels$ | async)?.length * barHeight + barHeight"
|
||||
stroke="red"
|
||||
stroke-width="2"
|
||||
></line>
|
||||
</svg>
|
||||
<div id="epg-container">
|
||||
<svg [attr.width]="(hourWidth$ | async) * 24" height="100%" id="epg-svg">
|
||||
<!-- time headline -->
|
||||
<g *ngFor="let hour of timeHeader; trackBy: trackByIndex">
|
||||
<rect
|
||||
[matTooltip]="hour + ':00'"
|
||||
[attr.width]="hourWidth$ | async"
|
||||
[attr.height]="barHeight"
|
||||
[attr.x]="hour * (hourWidth$ | async)"
|
||||
fill="#000"
|
||||
></rect>
|
||||
<text
|
||||
[attr.x]="hour * (hourWidth$ | async)"
|
||||
y="10"
|
||||
font-size="14"
|
||||
fill="white"
|
||||
transform="translate(20,20)"
|
||||
>
|
||||
{{ hour }}:00
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- epg channels with programs -->
|
||||
<g *ngFor="let item of channels$ | async; let i = index; trackBy: trackByIndex">
|
||||
<g
|
||||
*ngFor="let program of item.programs; trackBy: trackByProgram"
|
||||
[attr.transform]="'translate(' + program.startPosition + ',' + (barHeight * i + barHeight) + ')'"
|
||||
class="program"
|
||||
(click)="showDescription(program)"
|
||||
>
|
||||
<rect
|
||||
[attr.width]="program.width"
|
||||
[attr.height]="barHeight"
|
||||
[matTooltip]="program.title"
|
||||
></rect>
|
||||
<foreignObject
|
||||
[attr.width]="program.width"
|
||||
[attr.height]="barHeight"
|
||||
>
|
||||
<div class="program-title">{{ program.title }}</div>
|
||||
</foreignObject>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- current time line -->
|
||||
<line
|
||||
[attr.x1]="currentTimeLine"
|
||||
y1="0"
|
||||
[attr.x2]="currentTimeLine"
|
||||
[attr.y2]="(channels$ | async)?.length * barHeight + barHeight"
|
||||
stroke="red"
|
||||
stroke-width="2"
|
||||
></line>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,9 +134,6 @@ export class MultiEpgContainerComponent
|
||||
Math.min(calculatedVisibleChannels, 20)
|
||||
);
|
||||
this.channelsUpperRange = this.visibleChannels;
|
||||
|
||||
console.log('Container height:', containerHeight);
|
||||
console.log('Calculated visible channels:', this.visibleChannels);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +148,7 @@ export class MultiEpgContainerComponent
|
||||
}
|
||||
|
||||
trackByProgram(_: number, program: EnrichedProgram): string {
|
||||
return program.start + program.title;
|
||||
return `${program.start}|${program?.title?.toString() ?? ''}`;
|
||||
}
|
||||
|
||||
async requestPrograms(): Promise<void> {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
}
|
||||
</mat-drawer>
|
||||
<mat-drawer-content class="drawer-content">
|
||||
<ng-container *ngIf="activeChannel$ | async as activeChannel">
|
||||
@if (activeChannel$ | async; as activeChannel) {
|
||||
<!-- toolbar with drawer icon -->
|
||||
<app-toolbar
|
||||
(toggleLeftDrawerClicked)="drawer.toggle()"
|
||||
@@ -18,95 +18,94 @@
|
||||
(multiEpgClicked)="openMultiEpgView()"
|
||||
[activeChannel]="activeChannel"
|
||||
/>
|
||||
|
||||
<app-audio-player
|
||||
*ngIf="activeChannel.radio === 'true'; else videoPlayer"
|
||||
[url]="activeChannel.url"
|
||||
[icon]="activeChannel?.tvg?.logo"
|
||||
/>
|
||||
<ng-template #videoPlayer>
|
||||
@if (activeChannel.radio === 'true') {
|
||||
<app-audio-player
|
||||
[url]="activeChannel.url"
|
||||
[icon]="activeChannel?.tvg?.logo"
|
||||
/>
|
||||
} @else {
|
||||
<!-- video.js player -->
|
||||
<app-vjs-player
|
||||
*ngIf="playerSettings.player === 'videojs'"
|
||||
[options]="{
|
||||
sources: [
|
||||
{
|
||||
src:
|
||||
activeChannel?.url +
|
||||
activeChannel?.epgParams,
|
||||
type: 'application/x-mpegURL',
|
||||
},
|
||||
],
|
||||
}"
|
||||
[volume]="volume"
|
||||
[class.hide-captions]="!playerSettings.showCaptions"
|
||||
/>
|
||||
|
||||
@if (playerSettings.player === 'videojs') {
|
||||
<app-vjs-player
|
||||
[options]="{
|
||||
sources: [
|
||||
{
|
||||
src:
|
||||
activeChannel?.url +
|
||||
activeChannel?.epgParams,
|
||||
type: 'application/x-mpegURL',
|
||||
},
|
||||
],
|
||||
}"
|
||||
[volume]="volume"
|
||||
[class.hide-captions]="!playerSettings.showCaptions"
|
||||
/>
|
||||
}
|
||||
<!-- default html player component -->
|
||||
<app-html-video-player
|
||||
*ngIf="playerSettings.player === 'html5'"
|
||||
[channel]="activeChannel"
|
||||
[volume]="volume"
|
||||
[showCaptions]="playerSettings.showCaptions"
|
||||
/>
|
||||
|
||||
@if (playerSettings.player === 'html5') {
|
||||
<app-html-video-player
|
||||
[channel]="activeChannel"
|
||||
[volume]="volume"
|
||||
[showCaptions]="playerSettings.showCaptions"
|
||||
/>
|
||||
}
|
||||
<!-- Add DPlayer component -->
|
||||
<app-d-player
|
||||
*ngIf="playerSettings.player === 'dplayer'"
|
||||
[channel]="activeChannel"
|
||||
[volume]="volume"
|
||||
[showCaptions]="playerSettings.showCaptions"
|
||||
/>
|
||||
|
||||
@if (playerSettings.player === 'dplayer') {
|
||||
<app-d-player
|
||||
[channel]="activeChannel"
|
||||
[volume]="volume"
|
||||
[showCaptions]="playerSettings.showCaptions"
|
||||
/>
|
||||
}
|
||||
<!-- Add ArtPlayer component -->
|
||||
<app-art-player
|
||||
*ngIf="playerSettings.player === 'artplayer'"
|
||||
[channel]="activeChannel"
|
||||
[volume]="volume"
|
||||
[showCaptions]="playerSettings.showCaptions"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="custom-player"
|
||||
*ngIf="playerSettings.player === 'mpv'"
|
||||
>
|
||||
<img src="./assets/images/custom-player.png" />
|
||||
<p>
|
||||
The channel is played in a separate window in the MPV
|
||||
player. (<a
|
||||
[routerLink]
|
||||
style="cursor: pointer"
|
||||
(click)="
|
||||
openUrl(
|
||||
'https://github.com/4gray/iptvnator/wiki/What-is-mpv-video-player-and-how-to-install-it-on-different-operating-systems%3F'
|
||||
)
|
||||
"
|
||||
>Installation instructions</a
|
||||
>)
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="custom-player"
|
||||
*ngIf="playerSettings.player === 'vlc'"
|
||||
>
|
||||
<img src="./assets/images/custom-player.png" />
|
||||
<p>
|
||||
The channel is played in a separate window in VLC
|
||||
player. Please make sure that this player is installed
|
||||
in your system and the correct path is defined in the
|
||||
app settings.
|
||||
</p>
|
||||
</div>
|
||||
@if (playerSettings.player === 'artplayer') {
|
||||
<app-art-player
|
||||
[channel]="activeChannel"
|
||||
[volume]="volume"
|
||||
[showCaptions]="playerSettings.showCaptions"
|
||||
/>
|
||||
}
|
||||
@if (playerSettings.player === 'mpv') {
|
||||
<div class="custom-player">
|
||||
<img src="./assets/images/custom-player.png" />
|
||||
<p>
|
||||
The channel is played in a separate window in the
|
||||
MPV player. (<a
|
||||
[routerLink]
|
||||
style="cursor: pointer"
|
||||
(click)="
|
||||
openUrl(
|
||||
'https://github.com/4gray/iptvnator/wiki/What-is-mpv-video-player-and-how-to-install-it-on-different-operating-systems%3F'
|
||||
)
|
||||
"
|
||||
>Installation instructions</a
|
||||
>)
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
@if (playerSettings.player === 'vlc') {
|
||||
<div class="custom-player">
|
||||
<img src="./assets/images/custom-player.png" />
|
||||
<p>
|
||||
The channel is played in a separate window in VLC
|
||||
player. Please make sure that this player is
|
||||
installed in your system and the correct path is
|
||||
defined in the app settings.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
<!-- channel overlay -->
|
||||
<app-info-overlay
|
||||
[channel]="activeChannel"
|
||||
[epgProgram]="epgProgram$ | async"
|
||||
/>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
</mat-drawer-content>
|
||||
<!-- right sidebar content -->
|
||||
<mat-drawer position="end" #drawerRight mode="side" disableClose>
|
||||
<app-epg-list *ngIf="isTauri" />
|
||||
@if (isTauri) {
|
||||
<app-epg-list />
|
||||
}
|
||||
</mat-drawer>
|
||||
</mat-drawer-container>
|
||||
@@ -24,20 +24,20 @@
|
||||
@media only screen and (max-width: 599px) {
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
flex-direction: column-reverse;
|
||||
top: unset;
|
||||
}
|
||||
|
||||
|
||||
.mat-drawer {
|
||||
height: 50vh;
|
||||
width: 100%;
|
||||
height: 50vh;
|
||||
width: 100%;
|
||||
position: unset;
|
||||
}
|
||||
|
||||
|
||||
.drawer-content {
|
||||
height: 50vh;
|
||||
margin-left: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
@if (player === 'videojs') {
|
||||
<app-vjs-player [options]="vjsOptions" />
|
||||
<app-vjs-player [options]="vjsOptions" />
|
||||
} @else if (player === 'html5') {
|
||||
<app-html-video-player *ngIf="player === 'html5'" [channel]="channel" />
|
||||
@if (player === 'html5') {
|
||||
<app-html-video-player [channel]="channel" />
|
||||
}
|
||||
} @else if (player === 'dplayer') {
|
||||
<app-d-player [channel]="channel" [volume]="1" [showCaptions]="false" />
|
||||
<app-d-player [channel]="channel" [volume]="1" [showCaptions]="false" />
|
||||
} @else if (player === 'artplayer') {
|
||||
<app-art-player [channel]="channel" [volume]="1" [showCaptions]="false" />
|
||||
<app-art-player [channel]="channel" [volume]="1" [showCaptions]="false" />
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NgIf } from '@angular/common';
|
||||
|
||||
import {
|
||||
Component,
|
||||
Signal,
|
||||
@@ -22,12 +22,11 @@ import { STORE_KEY } from '../../shared/enums/store-keys.enum';
|
||||
templateUrl: './web-player-view.component.html',
|
||||
styleUrls: ['./web-player-view.component.scss'],
|
||||
imports: [
|
||||
HtmlVideoPlayerComponent,
|
||||
NgIf,
|
||||
VjsPlayerComponent,
|
||||
DPlayerComponent,
|
||||
ArtPlayerComponent,
|
||||
],
|
||||
HtmlVideoPlayerComponent,
|
||||
VjsPlayerComponent,
|
||||
DPlayerComponent,
|
||||
ArtPlayerComponent
|
||||
],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class WebPlayerViewComponent {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ModalWindow } from 'ngx-whats-new/lib/modal-window.interface';
|
||||
/* import { ModalWindow } from 'ngx-whats-new/lib/modal-window.interface'; */
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
@@ -300,14 +300,14 @@ export class WhatsNewService {
|
||||
* Returns an array with modals for the provided version of the application
|
||||
* @param version version string
|
||||
*/
|
||||
getModalsByVersion(version: string): ModalWindow[] {
|
||||
getModalsByVersion(version: string) {
|
||||
return this.modals[version] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns modals with latest changes
|
||||
*/
|
||||
getLatestChanges(): ModalWindow[] {
|
||||
getLatestChanges() {
|
||||
const modalsLength = Object.keys(this.modals).length;
|
||||
const lastVersion = Object.keys(this.modals)[modalsLength - 1];
|
||||
return this.modals[lastVersion];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@if (!isDialog) {
|
||||
<app-header
|
||||
[title]="'SETTINGS.TITLE' | translate"
|
||||
<app-header
|
||||
[title]="'SETTINGS.TITLE' | translate"
|
||||
[subtitle]="
|
||||
('SETTINGS.SUBTITLE' | translate) +
|
||||
'. ' +
|
||||
@@ -8,371 +8,378 @@
|
||||
"
|
||||
/>
|
||||
} @else {
|
||||
<h2 mat-dialog-title>{{ 'SETTINGS.TITLE' | translate }}</h2>
|
||||
<h2 mat-dialog-title>{{ 'SETTINGS.TITLE' | translate }}</h2>
|
||||
}
|
||||
|
||||
<div class="settings-container" data-test-id="settings-container">
|
||||
<form [formGroup]="settingsForm" (ngSubmit)="onSubmit()" novalidate>
|
||||
<ng-container formArrayName="epgUrl" *ngIf="isTauri">
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.EPG_URL_LABEL' | translate }}
|
||||
<p>Add single or multiple URLs as EPG sources</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div
|
||||
*ngFor="let _ of epgUrl.controls; index as i"
|
||||
style="display: flex"
|
||||
>
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{
|
||||
'SETTINGS.EPG_URL_PLACEHOLDER' | translate
|
||||
}}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="url"
|
||||
[formControlName]="i"
|
||||
#epgField
|
||||
/>
|
||||
<mat-error>{{
|
||||
'SETTINGS.EPG_URL_ERROR' | translate
|
||||
}}</mat-error>
|
||||
</mat-form-field>
|
||||
<button
|
||||
matLine
|
||||
mat-icon-button
|
||||
color="accent"
|
||||
[matTooltip]="'SETTINGS.REFRESH_EPG' | translate"
|
||||
type="button"
|
||||
[disabled]="epgField.value === ''"
|
||||
(click)="refreshEpg(epgUrl.value[i])"
|
||||
>
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
matLine
|
||||
color="accent"
|
||||
[matTooltip]="'SETTINGS.REFRESH_EPG' | translate"
|
||||
type="button"
|
||||
(click)="removeEpgSource(i)"
|
||||
>
|
||||
<mat-icon>remove</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
mat-button
|
||||
color="accent"
|
||||
type="button"
|
||||
(click)="addEpgSource()"
|
||||
>
|
||||
{{ 'SETTINGS.ADD_EPG_SOURCE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider />
|
||||
</ng-container>
|
||||
<form [formGroup]="settingsForm" (ngSubmit)="onSubmit()" novalidate>
|
||||
@if (isTauri) {
|
||||
<ng-container formArrayName="epgUrl">
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.VIDEO_PLAYER_LABEL' | translate }}
|
||||
<p>{{ 'SETTINGS.VIDEO_PLAYER_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{
|
||||
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate
|
||||
}}</mat-label>
|
||||
<mat-select
|
||||
formControlName="player"
|
||||
data-test-id="select-video-player"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let player of players"
|
||||
[value]="player.id"
|
||||
[attr.data-test-id]="player.id"
|
||||
>{{ player.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.EPG_URL_LABEL' | translate }}
|
||||
<p>Add single or multiple URLs as EPG sources</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
@for (_ of epgUrl.controls; track _; let i = $index) {
|
||||
<div
|
||||
style="display: flex"
|
||||
>
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{
|
||||
'SETTINGS.EPG_URL_PLACEHOLDER' | translate
|
||||
}}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="url"
|
||||
[formControlName]="i"
|
||||
#epgField
|
||||
/>
|
||||
<mat-error>{{
|
||||
'SETTINGS.EPG_URL_ERROR' | translate
|
||||
}}</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<button
|
||||
matLine
|
||||
mat-icon-button
|
||||
color="accent"
|
||||
[matTooltip]="'SETTINGS.REFRESH_EPG' | translate"
|
||||
type="button"
|
||||
[disabled]="epgField.value === ''"
|
||||
(click)="refreshEpg(epgUrl.value[i])"
|
||||
>
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
matLine
|
||||
color="accent"
|
||||
[matTooltip]="'SETTINGS.REFRESH_EPG' | translate"
|
||||
type="button"
|
||||
(click)="removeEpgSource(i)"
|
||||
>
|
||||
<mat-icon>remove</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
mat-button
|
||||
color="accent"
|
||||
type="button"
|
||||
(click)="addEpgSource()"
|
||||
>
|
||||
{{ 'SETTINGS.ADD_EPG_SOURCE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (settingsForm.value.player === 'mpv') {
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.MPV_PLAYER_PATH_LABEL' | translate }}
|
||||
<p>
|
||||
{{ 'SETTINGS.MPV_PLAYER_PATH_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label for="mpvPlayerPath">{{
|
||||
'SETTINGS.MPV_PLAYER_PATH' | translate
|
||||
}}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
id="mpvPlayerPath"
|
||||
formControlName="mpvPlayerPath"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (settingsForm.value.player === 'vlc') {
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.VLC_PLAYER_PATH_LABEL' | translate }}
|
||||
<p>
|
||||
{{ 'SETTINGS.VLC_PLAYER_PATH_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label for="vlcPlayerPath">{{
|
||||
'SETTINGS.VLC_PLAYER_PATH' | translate
|
||||
}}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
id="vlcPlayerPath"
|
||||
formControlName="vlcPlayerPath"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.STREAM_FORMAT' | translate }}
|
||||
<p>{{ 'SETTINGS.STREAM_FORMAT_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{
|
||||
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate
|
||||
}}</mat-label>
|
||||
<mat-select
|
||||
formControlName="streamFormat"
|
||||
data-test-id="select-stream-format"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let streamFormat of streamFormatEnum | keyvalue"
|
||||
[value]="streamFormat.value"
|
||||
[attr.data-test-id]="streamFormat.value"
|
||||
>{{ streamFormat.value }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.VIDEO_PLAYER_LABEL' | translate }}
|
||||
<p>{{ 'SETTINGS.VIDEO_PLAYER_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{
|
||||
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate
|
||||
}}</mat-label>
|
||||
<mat-select
|
||||
formControlName="player"
|
||||
data-test-id="select-video-player"
|
||||
>
|
||||
@for (player of players; track player) {
|
||||
<mat-option
|
||||
[value]="player.id"
|
||||
[attr.data-test-id]="player.id"
|
||||
>{{ player.label }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
@if (settingsForm.value.player === 'mpv') {
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.MPV_PLAYER_PATH_LABEL' | translate }}
|
||||
<p>
|
||||
{{ 'SETTINGS.MPV_PLAYER_PATH_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label for="mpvPlayerPath">{{
|
||||
'SETTINGS.MPV_PLAYER_PATH' | translate
|
||||
}}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
id="mpvPlayerPath"
|
||||
formControlName="mpvPlayerPath"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (settingsForm.value.player === 'vlc') {
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.VLC_PLAYER_PATH_LABEL' | translate }}
|
||||
<p>
|
||||
{{ 'SETTINGS.VLC_PLAYER_PATH_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label for="vlcPlayerPath">{{
|
||||
'SETTINGS.VLC_PLAYER_PATH' | translate
|
||||
}}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
id="vlcPlayerPath"
|
||||
formControlName="vlcPlayerPath"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.STREAM_FORMAT' | translate }}
|
||||
<p>{{ 'SETTINGS.STREAM_FORMAT_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{
|
||||
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate
|
||||
}}</mat-label>
|
||||
<mat-select
|
||||
formControlName="streamFormat"
|
||||
data-test-id="select-stream-format"
|
||||
>
|
||||
@for (streamFormat of streamFormatEnum | keyvalue; track streamFormat) {
|
||||
<mat-option
|
||||
[value]="streamFormat.value"
|
||||
[attr.data-test-id]="streamFormat.value"
|
||||
>{{ streamFormat.value }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.LANGUAGE' | translate }}
|
||||
<p>{{ 'SETTINGS.LANGUAGE_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{
|
||||
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate
|
||||
}}</mat-label>
|
||||
<mat-select
|
||||
formControlName="language"
|
||||
data-test-id="select-language"
|
||||
>
|
||||
@for (language of languageEnum | keyvalue; track language) {
|
||||
<mat-option
|
||||
[value]="language.value"
|
||||
[attr.data-test-id]="language.value"
|
||||
>{{
|
||||
'LANGUAGES.' + language.key | translate
|
||||
}}</mat-option
|
||||
>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.THEME' | translate }}
|
||||
<p>{{ 'SETTINGS.THEME_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{
|
||||
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate
|
||||
}}</mat-label>
|
||||
<mat-select
|
||||
formControlName="theme"
|
||||
data-test-id="select-theme"
|
||||
>
|
||||
@for (theme of themeEnum | keyvalue; track theme) {
|
||||
<mat-option
|
||||
[value]="theme.value"
|
||||
[attr.data-test-id]="theme.value"
|
||||
>{{
|
||||
'THEMES.' + theme.value | translate
|
||||
}}</mat-option
|
||||
>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.LANGUAGE' | translate }}
|
||||
<p>{{ 'SETTINGS.LANGUAGE_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{
|
||||
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate
|
||||
}}</mat-label>
|
||||
<mat-select
|
||||
formControlName="language"
|
||||
data-test-id="select-language"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let language of languageEnum | keyvalue"
|
||||
[value]="language.value"
|
||||
[attr.data-test-id]="language.value"
|
||||
>{{
|
||||
'LANGUAGES.' + language.key | translate
|
||||
}}</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.THEME' | translate }}
|
||||
<p>{{ 'SETTINGS.THEME_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{
|
||||
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate
|
||||
}}</mat-label>
|
||||
<mat-select
|
||||
formControlName="theme"
|
||||
data-test-id="select-theme"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let theme of themeEnum | keyvalue"
|
||||
[value]="theme.value"
|
||||
[attr.data-test-id]="theme.value"
|
||||
>{{
|
||||
'THEMES.' + theme.value | translate
|
||||
}}</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.SHOW_CAPTIONS' | translate }}
|
||||
<p>{{ 'SETTINGS.SHOW_CAPTIONS_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column" style="margin-right: 10px">
|
||||
<mat-checkbox
|
||||
class="column"
|
||||
formControlName="showCaptions"
|
||||
></mat-checkbox>
|
||||
</div>
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.SHOW_CAPTIONS' | translate }}
|
||||
<p>{{ 'SETTINGS.SHOW_CAPTIONS_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column" style="margin-right: 10px">
|
||||
<mat-checkbox
|
||||
class="column"
|
||||
formControlName="showCaptions"
|
||||
></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<!-- not ported to tauri yet
|
||||
@if (isPwa || isTauri) {
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.REMOTE_CONTROL' | translate }}
|
||||
<p>
|
||||
{{ 'SETTINGS.REMOTE_CONTROL_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column" style="margin-right: 10px">
|
||||
<mat-checkbox
|
||||
class="column"
|
||||
formControlName="remoteControl"
|
||||
></mat-checkbox>
|
||||
</div>
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.REMOTE_CONTROL' | translate }}
|
||||
<p>
|
||||
{{ 'SETTINGS.REMOTE_CONTROL_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column" style="margin-right: 10px">
|
||||
<mat-checkbox
|
||||
class="column"
|
||||
formControlName="remoteControl"
|
||||
></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
-->
|
||||
@if (settingsForm.value.remoteControl === true) {
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.REMOTE_CONTROL_PORT' | translate }}
|
||||
<p>
|
||||
{{
|
||||
'SETTINGS.REMOTE_CONTROL_PORT_DESCRIPTION'
|
||||
| translate
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column" style="margin-right: 10px">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
id="remoteControlPort"
|
||||
formControlName="remoteControlPort"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.REMOTE_CONTROL_PORT' | translate }}
|
||||
<p>
|
||||
{{
|
||||
'SETTINGS.REMOTE_CONTROL_PORT_DESCRIPTION'
|
||||
| translate
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column" style="margin-right: 10px">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
id="remoteControlPort"
|
||||
formControlName="remoteControlPort"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.VERSION' | translate }}
|
||||
<p>{{ 'SETTINGS.VERSION_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
{{ version }} <br />
|
||||
{{ updateMessage }}
|
||||
</div>
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.VERSION' | translate }}
|
||||
<p>{{ 'SETTINGS.VERSION_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
{{ version }} <br />
|
||||
{{ updateMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.IMPORT_EXPORT_DATA' | translate }}
|
||||
<p>
|
||||
{{ 'SETTINGS.IMPORT_EXPORT_DATA_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button
|
||||
mat-button
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.IMPORT_EXPORT_DATA' | translate }}
|
||||
<p>
|
||||
{{ 'SETTINGS.IMPORT_EXPORT_DATA_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button
|
||||
mat-button
|
||||
(click)="
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
importData()
|
||||
"
|
||||
>
|
||||
{{ 'SETTINGS.IMPORT_DATA' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
>
|
||||
{{ 'SETTINGS.IMPORT_DATA' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
(click)="
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
exportData()
|
||||
"
|
||||
>
|
||||
{{ 'SETTINGS.EXPORT_DATA' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
>
|
||||
{{ 'SETTINGS.EXPORT_DATA' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.REMOVE_ALL' | translate }}
|
||||
<p>
|
||||
{{ 'SETTINGS.REMOVE_ALL_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button
|
||||
mat-button
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.REMOVE_ALL' | translate }}
|
||||
<p>
|
||||
{{ 'SETTINGS.REMOVE_ALL_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button
|
||||
mat-button
|
||||
(click)="
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
removeAll()
|
||||
"
|
||||
color="warn"
|
||||
>
|
||||
{{ 'SETTINGS.REMOVE_ALL_BUTTON' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
color="warn"
|
||||
>
|
||||
{{ 'SETTINGS.REMOVE_ALL_BUTTON' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (isPwa) {
|
||||
<div class="row">
|
||||
{{ 'SETTINGS.EPG_NOTE' | translate }}
|
||||
<a href="https://github.com/4gray/iptvnator/releases">{{
|
||||
'SETTINGS.EPG_NOTE_URL_TEXT' | translate
|
||||
}}</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-divider />
|
||||
<div class="action-buttons" align="end">
|
||||
<button
|
||||
mat-button
|
||||
color="accent"
|
||||
type="reset"
|
||||
(click)="backToHome()"
|
||||
data-test-id="back-to-home"
|
||||
>
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
{{ 'SETTINGS.BACK_TO_HOME' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
color="accent"
|
||||
type="submit"
|
||||
[disabled]="settingsForm.pristine || !settingsForm.valid"
|
||||
data-test-id="save-settings"
|
||||
>
|
||||
<mat-icon>save</mat-icon>
|
||||
{{ 'SETTINGS.SAVE_CHANGES' | translate }}
|
||||
</button>
|
||||
<div class="row">
|
||||
{{ 'SETTINGS.EPG_NOTE' | translate }}
|
||||
<a href="https://github.com/4gray/iptvnator/releases">{{
|
||||
'SETTINGS.EPG_NOTE_URL_TEXT' | translate
|
||||
}}</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-divider />
|
||||
<div class="action-buttons" align="end">
|
||||
<button
|
||||
mat-button
|
||||
color="accent"
|
||||
type="reset"
|
||||
(click)="backToHome()"
|
||||
data-test-id="back-to-home"
|
||||
>
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
{{ 'SETTINGS.BACK_TO_HOME' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
color="accent"
|
||||
type="submit"
|
||||
[disabled]="settingsForm.pristine || !settingsForm.valid"
|
||||
data-test-id="save-settings"
|
||||
>
|
||||
<mat-icon>save</mat-icon>
|
||||
{{ 'SETTINGS.SAVE_CHANGES' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6,253 +6,240 @@
|
||||
<div class="title">{{ title }}</div>
|
||||
<div class="subtitle">{{ subtitle }}</div>
|
||||
</div>
|
||||
<div class="menu" *ngIf="isHome">
|
||||
<button
|
||||
mat-icon-button
|
||||
class="add-playlist-btn"
|
||||
[matMenuTriggerFor]="addPlaylistMenu"
|
||||
[matTooltip]="'HOME.URL_UPLOAD.ADD_PLAYLIST' | translate"
|
||||
data-test-id="add-playlist"
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
<!-- Sort button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="sortMenu"
|
||||
[matTooltip]="'HOME.SORT_PLAYLISTS' | translate"
|
||||
data-test-id="sort-playlists"
|
||||
>
|
||||
<mat-icon>sort</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="filterPlaylistMenu"
|
||||
[matTooltip]="'HOME.FILTER_BY_TYPE' | translate"
|
||||
data-test-id="filter-playlist-by-type"
|
||||
>
|
||||
<mat-icon>filter_alt</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="isElectron || isTauri; else pwaMenu"
|
||||
mat-icon-button
|
||||
(click)="openSettings()"
|
||||
[matTooltip]="'MENU.SETTINGS' | translate"
|
||||
data-test-id="open-settings"
|
||||
>
|
||||
<mat-icon>settings</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Sort menu -->
|
||||
<mat-menu #sortMenu="matMenu">
|
||||
<div
|
||||
class="dropdown-menu-header"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
{{ 'HOME.SORT_BY' | translate }}
|
||||
</div>
|
||||
@if (isHome) {
|
||||
<div class="menu">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.NAME, SortOrder.ASC)"
|
||||
[class.active-sort]="isSortActive(SortBy.NAME, SortOrder.ASC)"
|
||||
mat-icon-button
|
||||
class="add-playlist-btn"
|
||||
[matMenuTriggerFor]="addPlaylistMenu"
|
||||
[matTooltip]="'HOME.URL_UPLOAD.ADD_PLAYLIST' | translate"
|
||||
data-test-id="add-playlist"
|
||||
>
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.NAME_ASC' | translate }}</span>
|
||||
<mat-icon
|
||||
*ngIf="isSortActive(SortBy.NAME, SortOrder.ASC)"
|
||||
class="check-icon"
|
||||
>check</mat-icon
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
<!-- Sort button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="sortMenu"
|
||||
[matTooltip]="'HOME.SORT_PLAYLISTS' | translate"
|
||||
data-test-id="sort-playlists"
|
||||
>
|
||||
<mat-icon>sort</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.NAME, SortOrder.DESC)"
|
||||
[class.active-sort]="isSortActive(SortBy.NAME, SortOrder.DESC)"
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="filterPlaylistMenu"
|
||||
[matTooltip]="'HOME.FILTER_BY_TYPE' | translate"
|
||||
data-test-id="filter-playlist-by-type"
|
||||
>
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.NAME_DESC' | translate }}</span>
|
||||
<mat-icon
|
||||
*ngIf="isSortActive(SortBy.NAME, SortOrder.DESC)"
|
||||
class="check-icon"
|
||||
>check</mat-icon
|
||||
>
|
||||
<mat-icon>filter_alt</mat-icon>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.DATE_ADDED, SortOrder.DESC)"
|
||||
[class.active-sort]="
|
||||
isSortActive(SortBy.DATE_ADDED, SortOrder.DESC)
|
||||
"
|
||||
>
|
||||
<mat-icon>date_range</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.NEWEST' | translate }}</span>
|
||||
<mat-icon
|
||||
*ngIf="isSortActive(SortBy.DATE_ADDED, SortOrder.DESC)"
|
||||
class="check-icon"
|
||||
>check</mat-icon
|
||||
@if (isElectron || isTauri) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="openSettings()"
|
||||
[matTooltip]="'MENU.SETTINGS' | translate"
|
||||
data-test-id="open-settings"
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.DATE_ADDED, SortOrder.ASC)"
|
||||
[class.active-sort]="
|
||||
isSortActive(SortBy.DATE_ADDED, SortOrder.ASC)
|
||||
"
|
||||
>
|
||||
<mat-icon>date_range</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.OLDEST' | translate }}</span>
|
||||
<mat-icon
|
||||
*ngIf="isSortActive(SortBy.DATE_ADDED, SortOrder.ASC)"
|
||||
class="check-icon"
|
||||
>check</mat-icon
|
||||
<mat-icon>settings</mat-icon>
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="menu"
|
||||
[attr.aria-label]="'MENU.OPEN' | translate"
|
||||
data-test-id="pwa-menu"
|
||||
>
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #filterPlaylistMenu="matMenu">
|
||||
<section
|
||||
(click)="$event.stopPropagation()"
|
||||
class="filter-container"
|
||||
>
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
@if (isHome) {
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'MENU.SETTINGS_ARIA' | translate"
|
||||
(click)="openSettings()"
|
||||
data-test-id="pwa-open-settings"
|
||||
>
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span>{{ 'MENU.SETTINGS' | translate }}</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
}
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'MENU.SUPPORT_ARIA' | translate"
|
||||
(click)="openUrl('https:///github.com/4gray/iptvnator')"
|
||||
>
|
||||
<mat-icon>recommend</mat-icon>
|
||||
<span>GitHub</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'MENU.BUG_REPORT' | translate"
|
||||
(click)="
|
||||
openUrl('https://github.com/4gray/iptvnator/issues')
|
||||
"
|
||||
>
|
||||
<mat-icon>bug_report</mat-icon>
|
||||
<span>{{ 'MENU.BUG_REPORT' | translate }}</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'MENU.WHAT_IS_NEW' | translate"
|
||||
(click)="setDialogVisibility(true)"
|
||||
>
|
||||
<mat-icon>new_releases</mat-icon>
|
||||
<span>{{ 'MENU.WHAT_IS_NEW' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'MENU.SUPPORT_PROJECT' | translate"
|
||||
(click)="openUrl('https://github.com/sponsors/4gray')"
|
||||
>
|
||||
<mat-icon>savings</mat-icon>
|
||||
<span>{{ 'MENU.SUPPORT_PROJECT' | translate }}</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'MENU.ABOUT_ARIA' | translate"
|
||||
(click)="openAboutDialog()"
|
||||
>
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>{{ 'MENU.ABOUT' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
}
|
||||
<!-- Sort menu -->
|
||||
<mat-menu #sortMenu="matMenu">
|
||||
<div
|
||||
class="dropdown-menu-header"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
{{ 'HOME.FILTER_BY_TYPE' | translate }}
|
||||
{{ 'HOME.SORT_BY' | translate }}
|
||||
</div>
|
||||
@for (type of playlistTypes; track $index) {
|
||||
<p>
|
||||
<mat-checkbox
|
||||
(click)="$event.stopPropagation()"
|
||||
[checked]="true"
|
||||
(change)="onPlaylistFilterChange()"
|
||||
[(ngModel)]="type.checked"
|
||||
>{{ type.title }}</mat-checkbox
|
||||
>
|
||||
</p>
|
||||
}
|
||||
</section>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #addPlaylistMenu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'HOME.TABS.FILE_UPLOAD' | translate"
|
||||
(click)="opedAddPlaylistDialog('file')"
|
||||
>
|
||||
<mat-icon>attach_file</mat-icon>
|
||||
<span>{{ 'HOME.TABS.FILE_UPLOAD' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'HOME.TABS.URL_UPLOAD' | translate"
|
||||
(click)="opedAddPlaylistDialog('url')"
|
||||
>
|
||||
<mat-icon>link</mat-icon>
|
||||
<span>{{ 'HOME.TABS.URL_UPLOAD' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'HOME.TABS.TEXT_IMPORT' | translate"
|
||||
(click)="opedAddPlaylistDialog('text')"
|
||||
>
|
||||
<mat-icon>text_format</mat-icon>
|
||||
<span>{{ 'HOME.TABS.TEXT_IMPORT' | translate }}</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'HOME.TABS.XTREME_IMPORT' | translate"
|
||||
(click)="opedAddPlaylistDialog('xtream')"
|
||||
>
|
||||
<mat-icon>video_library</mat-icon>
|
||||
<span>{{ 'HOME.TABS.XTREME_IMPORT' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="
|
||||
'HOME.TABS.STALKER_PORTAL_IMPORT' | translate
|
||||
"
|
||||
(click)="opedAddPlaylistDialog('stalker')"
|
||||
>
|
||||
<mat-icon>dashboard</mat-icon>
|
||||
<span>{{ 'HOME.TABS.STALKER_PORTAL_IMPORT' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.NAME, SortOrder.ASC)"
|
||||
[class.active-sort]="
|
||||
isSortActive(SortBy.NAME, SortOrder.ASC)
|
||||
"
|
||||
>
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.NAME_ASC' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.NAME, SortOrder.DESC)"
|
||||
[class.active-sort]="
|
||||
isSortActive(SortBy.NAME, SortOrder.DESC)
|
||||
"
|
||||
>
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.NAME_DESC' | translate }}</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.DATE_ADDED, SortOrder.DESC)"
|
||||
[class.active-sort]="
|
||||
isSortActive(SortBy.DATE_ADDED, SortOrder.DESC)
|
||||
"
|
||||
>
|
||||
<mat-icon>date_range</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.NEWEST' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.DATE_ADDED, SortOrder.ASC)"
|
||||
[class.active-sort]="
|
||||
isSortActive(SortBy.DATE_ADDED, SortOrder.ASC)
|
||||
"
|
||||
>
|
||||
<mat-icon>date_range</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.OLDEST' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
<mat-menu #filterPlaylistMenu="matMenu">
|
||||
<section
|
||||
(click)="$event.stopPropagation()"
|
||||
class="filter-container"
|
||||
>
|
||||
<div
|
||||
class="dropdown-menu-header"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
{{ 'HOME.FILTER_BY_TYPE' | translate }}
|
||||
</div>
|
||||
@for (type of playlistTypes; track $index) {
|
||||
<p>
|
||||
<mat-checkbox
|
||||
(click)="$event.stopPropagation()"
|
||||
[checked]="true"
|
||||
(change)="onPlaylistFilterChange()"
|
||||
[(ngModel)]="type.checked"
|
||||
>{{ type.title }}</mat-checkbox
|
||||
>
|
||||
</p>
|
||||
}
|
||||
</section>
|
||||
</mat-menu>
|
||||
<mat-menu #addPlaylistMenu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'HOME.TABS.FILE_UPLOAD' | translate"
|
||||
(click)="opedAddPlaylistDialog('file')"
|
||||
>
|
||||
<mat-icon>attach_file</mat-icon>
|
||||
<span>{{ 'HOME.TABS.FILE_UPLOAD' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'HOME.TABS.URL_UPLOAD' | translate"
|
||||
(click)="opedAddPlaylistDialog('url')"
|
||||
>
|
||||
<mat-icon>link</mat-icon>
|
||||
<span>{{ 'HOME.TABS.URL_UPLOAD' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'HOME.TABS.TEXT_IMPORT' | translate"
|
||||
(click)="opedAddPlaylistDialog('text')"
|
||||
>
|
||||
<mat-icon>text_format</mat-icon>
|
||||
<span>{{ 'HOME.TABS.TEXT_IMPORT' | translate }}</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'HOME.TABS.XTREME_IMPORT' | translate"
|
||||
(click)="opedAddPlaylistDialog('xtream')"
|
||||
>
|
||||
<mat-icon>video_library</mat-icon>
|
||||
<span>{{ 'HOME.TABS.XTREME_IMPORT' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="
|
||||
'HOME.TABS.STALKER_PORTAL_IMPORT' | translate
|
||||
"
|
||||
(click)="opedAddPlaylistDialog('stalker')"
|
||||
>
|
||||
<mat-icon>dashboard</mat-icon>
|
||||
<span>{{
|
||||
'HOME.TABS.STALKER_PORTAL_IMPORT' | translate
|
||||
}}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
}
|
||||
</mat-toolbar>
|
||||
|
||||
<ng-template #pwaMenu>
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="menu"
|
||||
[attr.aria-label]="'MENU.OPEN' | translate"
|
||||
data-test-id="pwa-menu"
|
||||
>
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<ng-container *ngIf="isHome">
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'MENU.SETTINGS_ARIA' | translate"
|
||||
(click)="openSettings()"
|
||||
data-test-id="pwa-open-settings"
|
||||
>
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span>{{ 'MENU.SETTINGS' | translate }}</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
</ng-container>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'MENU.SUPPORT_ARIA' | translate"
|
||||
(click)="openUrl('https:///github.com/4gray/iptvnator')"
|
||||
>
|
||||
<mat-icon>recommend</mat-icon>
|
||||
<span>GitHub</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'MENU.BUG_REPORT' | translate"
|
||||
(click)="openUrl('https://github.com/4gray/iptvnator/issues')"
|
||||
>
|
||||
<mat-icon>bug_report</mat-icon>
|
||||
<span>{{ 'MENU.BUG_REPORT' | translate }}</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'MENU.WHAT_IS_NEW' | translate"
|
||||
(click)="setDialogVisibility(true)"
|
||||
>
|
||||
<mat-icon>new_releases</mat-icon>
|
||||
<span>{{ 'MENU.WHAT_IS_NEW' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'MENU.SUPPORT_PROJECT' | translate"
|
||||
(click)="openUrl('https://github.com/sponsors/4gray')"
|
||||
>
|
||||
<mat-icon>savings</mat-icon>
|
||||
<span>{{ 'MENU.SUPPORT_PROJECT' | translate }}</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'MENU.ABOUT_ARIA' | translate"
|
||||
(click)="openAboutDialog()"
|
||||
>
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>{{ 'MENU.ABOUT' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</ng-template>
|
||||
<ngx-whats-new
|
||||
*ngIf="!isElectron && isDialogVisible$ | async"
|
||||
(closeModal)="setDialogVisibility(false)"
|
||||
[items]="modals"
|
||||
[options]="options"
|
||||
/>
|
||||
@if (!isElectron && isDialogVisible$ | async) {
|
||||
<ngx-whats-new
|
||||
(closeModal)="setDialogVisibility(false)"
|
||||
[items]="modals"
|
||||
[options]="options"
|
||||
/>
|
||||
}
|
||||
@@ -58,7 +58,7 @@
|
||||
}
|
||||
|
||||
.active-sort {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
background-color: rgb(48 48 48);
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component, Input, OnInit, effect } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
MatMenuModule,
|
||||
MatToolbarModule,
|
||||
MatTooltipModule,
|
||||
NgIf,
|
||||
FormsModule,
|
||||
NgxWhatsNewModule,
|
||||
ReactiveFormsModule,
|
||||
|
||||
@@ -1,51 +1,52 @@
|
||||
<div class="mpv-player-bar" *ngIf="(activeProcesses$ | async)?.length">
|
||||
@if ((activeProcesses$ | async)?.length) {
|
||||
<div class="mpv-player-bar">
|
||||
@for (process of activeProcesses$ | async; track process.id) {
|
||||
<div class="player-item">
|
||||
<div class="player-media">
|
||||
@if (process.thumbnail) {
|
||||
<img
|
||||
[src]="process.thumbnail"
|
||||
[alt]="process.title"
|
||||
class="media-thumbnail"
|
||||
<div class="player-item">
|
||||
<div class="player-media">
|
||||
@if (process.thumbnail) {
|
||||
<img
|
||||
[src]="process.thumbnail"
|
||||
[alt]="process.title"
|
||||
class="media-thumbnail"
|
||||
(error)="
|
||||
$event.target.src =
|
||||
'./assets/images/default-poster.png'
|
||||
"
|
||||
/>
|
||||
} @else {
|
||||
<div class="media-thumbnail-placeholder">
|
||||
<mat-icon>live_tv</mat-icon>
|
||||
</div>
|
||||
}
|
||||
<div class="media-info">
|
||||
<span class="media-title">{{ process.title }}</span>
|
||||
<span class="media-url">{{ process.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="player-controls">
|
||||
<!-- <button
|
||||
mat-icon-button
|
||||
(click)="pauseStream(process.id)"
|
||||
title="Pause"
|
||||
>
|
||||
<mat-icon>pause</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="playStream(process.id)"
|
||||
title="Play"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
</button> -->
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="closeStream(process.id)"
|
||||
title="Close"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
/>
|
||||
} @else {
|
||||
<div class="media-thumbnail-placeholder">
|
||||
<mat-icon>live_tv</mat-icon>
|
||||
</div>
|
||||
}
|
||||
<div class="media-info">
|
||||
<span class="media-title">{{ process.title }}</span>
|
||||
<span class="media-url">{{ process.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="player-controls">
|
||||
<!-- <button
|
||||
mat-icon-button
|
||||
(click)="pauseStream(process.id)"
|
||||
title="Pause"
|
||||
>
|
||||
<mat-icon>pause</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="playStream(process.id)"
|
||||
title="Play"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
</button> -->
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="closeStream(process.id)"
|
||||
title="Close"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -44,12 +44,10 @@ export class RecentlyViewedComponent {
|
||||
itemDetails: any = null;
|
||||
|
||||
readonly allFavorites = rxResource({
|
||||
request: () => ({
|
||||
params: () => ({
|
||||
refreshTimestamp: this.refreshTimestamp(),
|
||||
}),
|
||||
loader: () => {
|
||||
return this.playlistService.getPortalRecentlyViewed();
|
||||
},
|
||||
stream: () => this.playlistService.getPortalRecentlyViewed(),
|
||||
});
|
||||
|
||||
readonly categories = computed(() => [
|
||||
|
||||
@@ -44,12 +44,10 @@ export class StalkerFavoritesComponent {
|
||||
itemDetails: any = null;
|
||||
|
||||
readonly allFavorites = rxResource({
|
||||
request: () => ({
|
||||
params: () => ({
|
||||
refreshTimestamp: this.refreshTimestamp(),
|
||||
}),
|
||||
loader: () => {
|
||||
return this.playlistService.getPortalFavorites();
|
||||
},
|
||||
stream: () => this.playlistService.getPortalFavorites(),
|
||||
});
|
||||
|
||||
readonly categories = computed(() => [
|
||||
|
||||
@@ -89,16 +89,18 @@ export class StalkerSearchComponent {
|
||||
itemDetails = null;
|
||||
|
||||
readonly searchResultsResource = resource({
|
||||
request: () => ({
|
||||
params: () => ({
|
||||
contentType: this.selectedFilterType(),
|
||||
search: this.searchTerm(),
|
||||
action: StalkerPortalActions.GetOrderedList,
|
||||
}),
|
||||
loader: async ({ request }) => {
|
||||
if (request.search.length < 3) {
|
||||
loader: async ({ params }) => {
|
||||
if (params.search.length < 3) {
|
||||
return [];
|
||||
}
|
||||
const { portalUrl, macAddress } = this.currentPlaylist();
|
||||
const playlist = this.currentPlaylist();
|
||||
if (!playlist) return [];
|
||||
const { portalUrl, macAddress } = playlist;
|
||||
|
||||
const response = await this.dataService.sendIpcEvent(
|
||||
STALKER_REQUEST,
|
||||
@@ -106,10 +108,10 @@ export class StalkerSearchComponent {
|
||||
url: portalUrl,
|
||||
macAddress,
|
||||
params: {
|
||||
action: StalkerContentTypes[request.contentType]
|
||||
action: StalkerContentTypes[params.contentType]
|
||||
.getContentAction,
|
||||
type: request.contentType,
|
||||
search: request.search,
|
||||
type: params.contentType,
|
||||
search: params.search,
|
||||
max_page_items: 100,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -85,15 +85,15 @@ export const StalkerStore = signalStore(
|
||||
withState(initialState),
|
||||
withProps((store, dataService = inject(DataService)) => ({
|
||||
getCategoryResource: resource({
|
||||
request: () => ({
|
||||
params: () => ({
|
||||
contentType: store.selectedContentType(),
|
||||
action: StalkerPortalActions.GetCategories,
|
||||
currentPlaylist: store.currentPlaylist(),
|
||||
}),
|
||||
loader: async ({ request }) => {
|
||||
if (request.currentPlaylist === undefined) return;
|
||||
loader: async ({ params }) => {
|
||||
if (params.currentPlaylist === undefined) return;
|
||||
|
||||
switch (request.contentType) {
|
||||
switch (params.contentType) {
|
||||
case 'itv':
|
||||
if (store.itvCategories().length > 0) {
|
||||
return store.itvCategories();
|
||||
@@ -111,7 +111,7 @@ export const StalkerStore = signalStore(
|
||||
break;
|
||||
}
|
||||
|
||||
const { portalUrl, macAddress } = request.currentPlaylist;
|
||||
const { portalUrl, macAddress } = params.currentPlaylist;
|
||||
|
||||
const response = await dataService.sendIpcEvent(
|
||||
STALKER_REQUEST,
|
||||
@@ -119,9 +119,9 @@ export const StalkerStore = signalStore(
|
||||
url: portalUrl,
|
||||
macAddress,
|
||||
params: {
|
||||
action: StalkerContentTypes[request.contentType]
|
||||
action: StalkerContentTypes[params.contentType]
|
||||
.getCategoryAction,
|
||||
type: request.contentType,
|
||||
type: params.contentType,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -135,7 +135,7 @@ export const StalkerStore = signalStore(
|
||||
a.category_name.localeCompare(b.category_name)
|
||||
);
|
||||
patchState(store, {
|
||||
[`${request.contentType}Categories`]: categories,
|
||||
[`${params.contentType}Categories`]: categories,
|
||||
});
|
||||
return categories;
|
||||
} else {
|
||||
@@ -146,34 +146,32 @@ export const StalkerStore = signalStore(
|
||||
},
|
||||
}),
|
||||
getContentResource: resource({
|
||||
request: () => ({
|
||||
params: () => ({
|
||||
contentType: store.selectedContentType(),
|
||||
category: store.selectedCategoryId(),
|
||||
action: StalkerPortalActions.GetOrderedList,
|
||||
search: store.searchPhrase(),
|
||||
pageIndex: store.page() + 1,
|
||||
}),
|
||||
loader: async ({ request }) => {
|
||||
loader: async ({ params }) => {
|
||||
if (
|
||||
!request.category ||
|
||||
request.category === null ||
|
||||
request.category === ''
|
||||
!params.category ||
|
||||
params.category === null ||
|
||||
params.category === ''
|
||||
) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
const currentPlaylist = store.currentPlaylist;
|
||||
const params = {
|
||||
action: StalkerContentTypes[request.contentType]
|
||||
const queryParams = {
|
||||
action: StalkerContentTypes[params.contentType]
|
||||
.getContentAction,
|
||||
type: request.contentType,
|
||||
category: request.category ?? '',
|
||||
genre: request.category ?? '',
|
||||
type: params.contentType,
|
||||
category: params.category ?? '',
|
||||
genre: params.category ?? '',
|
||||
sortby: 'added',
|
||||
...(request.search !== ''
|
||||
? { search: request.search }
|
||||
: {}),
|
||||
p: request.pageIndex,
|
||||
...(params.search !== '' ? { search: params.search } : {}),
|
||||
p: params.pageIndex,
|
||||
};
|
||||
|
||||
const response = await dataService.sendIpcEvent(
|
||||
@@ -181,7 +179,7 @@ export const StalkerStore = signalStore(
|
||||
{
|
||||
url: currentPlaylist().portalUrl,
|
||||
macAddress: currentPlaylist().macAddress,
|
||||
params,
|
||||
params: queryParams,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -195,7 +193,7 @@ export const StalkerStore = signalStore(
|
||||
|
||||
if (store.selectedContentType() === 'itv') {
|
||||
// Check if we're loading the first page or loading more
|
||||
if (request.pageIndex === 1) {
|
||||
if (params.pageIndex === 1) {
|
||||
patchState(store, { itvChannels: newItems });
|
||||
} else {
|
||||
patchState(store, {
|
||||
@@ -223,22 +221,22 @@ export const StalkerStore = signalStore(
|
||||
},
|
||||
}),
|
||||
serialSeasonsResource: resource({
|
||||
request: () => ({
|
||||
params: () => ({
|
||||
itemId: store.selectedSerialId(),
|
||||
}),
|
||||
loader: async ({ request }) => {
|
||||
loader: async ({ params }) => {
|
||||
const { portalUrl, macAddress } = store.currentPlaylist();
|
||||
const params = {
|
||||
const queryParams = {
|
||||
action: StalkerContentTypes.series.getContentAction,
|
||||
type: 'series',
|
||||
movie_id: request.itemId,
|
||||
movie_id: params.itemId,
|
||||
};
|
||||
const response = await dataService.sendIpcEvent(
|
||||
STALKER_REQUEST,
|
||||
{
|
||||
url: portalUrl,
|
||||
macAddress,
|
||||
params,
|
||||
params: queryParams,
|
||||
}
|
||||
);
|
||||
return sortByNumericValue(response.js.data);
|
||||
|
||||
@@ -1,34 +1,24 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Inject, inject } from '@angular/core';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { selectActivePlaylist } from '../../state/selectors';
|
||||
import { XtreamAccountInfo } from './account-info.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account-info',
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatButton,
|
||||
MatDialogModule,
|
||||
MatCardModule,
|
||||
MatListModule,
|
||||
TranslateModule,
|
||||
],
|
||||
imports: [MatButton, MatDialogModule, MatListModule, TranslatePipe],
|
||||
template: `
|
||||
<h2 mat-dialog-title>Account Information</h2>
|
||||
<mat-dialog-content class="mat-typography">
|
||||
<div
|
||||
*ngIf="accountInfo?.user_info?.message"
|
||||
class="welcome-message"
|
||||
>
|
||||
{{ accountInfo?.user_info?.message }}
|
||||
</div>
|
||||
@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>
|
||||
@@ -137,7 +127,7 @@ import { XtreamAccountInfo } from './account-info.interface';
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
styleUrl: './account-info.component.scss'
|
||||
styleUrl: './account-info.component.scss',
|
||||
})
|
||||
export class AccountInfoComponent {
|
||||
accountInfo: XtreamAccountInfo;
|
||||
|
||||
@@ -37,7 +37,7 @@ export class CategoryContentViewComponent implements OnInit {
|
||||
|
||||
readonly limit = this.store.limit;
|
||||
readonly pageIndex = this.store.page;
|
||||
readonly pageSizeOptions = [5, 10, 25, 50, 100];
|
||||
readonly pageSizeOptions = this.isStalker ? [14] : [10, 25, 50, 100];
|
||||
readonly selectedCategory = this.store.getSelectedCategory;
|
||||
readonly paginatedContent = this.store.getPaginatedContent;
|
||||
readonly isPaginatedContentLoading = this.store.isPaginatedContentLoading;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, input } from '@angular/core';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
|
||||
@Component({
|
||||
selector: 'app-loading-overlay',
|
||||
imports: [MatProgressBarModule, CommonModule],
|
||||
imports: [MatProgressBarModule],
|
||||
template: `
|
||||
<div class="overlay">
|
||||
<div class="progress-container">
|
||||
@@ -47,7 +46,7 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
}
|
||||
}
|
||||
`,
|
||||
]
|
||||
],
|
||||
})
|
||||
export class LoadingOverlayComponent {
|
||||
current = input(0);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
.link-input {
|
||||
width: 98%;
|
||||
width: 100%;
|
||||
padding-top: 5px;
|
||||
@include mat.form-field-density(-5);
|
||||
}
|
||||
@@ -19,4 +19,4 @@ mat-dialog-content {
|
||||
.video-js {
|
||||
height: 500px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +1,119 @@
|
||||
<div id="navigation">
|
||||
<button mat-button routerLink="/">
|
||||
<mat-icon>home</mat-icon>
|
||||
<span class="button-label">{{ 'PLAYLISTS' | translate }}</span>
|
||||
<button mat-button routerLink="/">
|
||||
<mat-icon>home</mat-icon>
|
||||
<span class="button-label">{{ 'PLAYLISTS' | translate }}</span>
|
||||
</button>
|
||||
|
||||
@if (showCategories) {
|
||||
<mat-button-toggle-group
|
||||
name="contentType"
|
||||
aria-label="Content Type"
|
||||
[value]="contentType"
|
||||
(change)="changeContentType($event.value)"
|
||||
hideSingleSelectionIndicator
|
||||
>
|
||||
@for (
|
||||
item of contentTypeNavigationItems; track trackByValue($index,
|
||||
item)) {
|
||||
<mat-button-toggle
|
||||
[value]="item.contentType"
|
||||
>
|
||||
<mat-icon class="icon">{{ item.icon }}</mat-icon>
|
||||
<span class="label">{{ item.label }}</span>
|
||||
</mat-button-toggle>
|
||||
}
|
||||
</mat-button-toggle-group>
|
||||
}
|
||||
|
||||
<div>
|
||||
<button mat-button (click)="openPlaylistDetails()">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span class="button-label">{{ 'Info' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<mat-button-toggle-group
|
||||
*ngIf="showCategories"
|
||||
name="contentType"
|
||||
aria-label="Content Type"
|
||||
[value]="contentType"
|
||||
(change)="changeContentType($event.value)"
|
||||
hideSingleSelectionIndicator
|
||||
>
|
||||
<mat-button-toggle
|
||||
*ngFor="
|
||||
let item of contentTypeNavigationItems;
|
||||
trackBy: trackByValue
|
||||
"
|
||||
[value]="item.contentType"
|
||||
>
|
||||
<mat-icon class="icon">{{ item.icon }}</mat-icon>
|
||||
<span class="label">{{ item.label }}</span>
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
<button mat-button (click)="favoritesClicked.emit()">
|
||||
<mat-icon>star</mat-icon>
|
||||
<span class="button-label">{{
|
||||
'CHANNELS.FAVORITES' | translate
|
||||
}}</span>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<button mat-button (click)="openPlaylistDetails()">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span class="button-label">{{ 'Info' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button mat-button (click)="favoritesClicked.emit()">
|
||||
<mat-icon>star</mat-icon>
|
||||
<span class="button-label">{{
|
||||
'CHANNELS.FAVORITES' | translate
|
||||
}}</span>
|
||||
</button>
|
||||
|
||||
<button mat-button (click)="openSettings()">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span class="button-label">{{ 'MENU.SETTINGS' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button mat-button (click)="openSettings()">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span class="button-label">{{ 'MENU.SETTINGS' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sub-panel">
|
||||
<div class="breadcrumbs">
|
||||
<div
|
||||
*ngFor="let item of breadcrumbs; let i = index"
|
||||
(click)="processBreadcrumbClick(item)"
|
||||
<div class="breadcrumbs">
|
||||
@for (item of breadcrumbs; track item; let i = $index) {
|
||||
<div
|
||||
(click)="processBreadcrumbClick(item)"
|
||||
>
|
||||
<span>{{ item.title }}</span>
|
||||
<mat-icon *ngIf="i !== breadcrumbs.length - 1"
|
||||
>chevron_right</mat-icon
|
||||
<span>{{ item.title }}</span>
|
||||
@if (i !== breadcrumbs.length - 1) {
|
||||
<mat-icon
|
||||
>chevron_right</mat-icon
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="search-container">
|
||||
<div class="favorite">
|
||||
<button
|
||||
*ngIf="favoriteVisible && activeLiveStream"
|
||||
mat-icon-button
|
||||
(click)="clickFavorites()"
|
||||
[matTooltip]="'TOP_MENU.TOGGLE_FAVORITE_FLAG' | translate"
|
||||
<div class="favorite">
|
||||
@if (favoriteVisible && activeLiveStream) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="clickFavorites()"
|
||||
[matTooltip]="'TOP_MENU.TOGGLE_FAVORITE_FLAG' | translate"
|
||||
>
|
||||
<mat-icon>
|
||||
{{ isFavoriteStream ? 'star' : 'star_outline' }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sort" *ngIf="sortVisible">
|
||||
<button
|
||||
mat-icon-button
|
||||
[matTooltip]="'CHANNELS.SORT_BY' | translate"
|
||||
[matMenuTriggerFor]="sortPlaylistMenu"
|
||||
data-test-id="sort-playlist"
|
||||
<mat-icon>
|
||||
{{ isFavoriteStream ? 'star' : 'star_outline' }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@if (sortVisible) {
|
||||
<div class="sort">
|
||||
<button
|
||||
mat-icon-button
|
||||
[matTooltip]="'CHANNELS.SORT_BY' | translate"
|
||||
[matMenuTriggerFor]="sortPlaylistMenu"
|
||||
data-test-id="sort-playlist"
|
||||
>
|
||||
<mat-icon>sort</mat-icon>
|
||||
</button>
|
||||
<mat-icon>sort</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="search" *ngIf="searchVisible">
|
||||
<mat-form-field class="search-input">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input
|
||||
matInput
|
||||
placeholder="Search by name"
|
||||
type="search"
|
||||
[ngModel]="searchPhrase()"
|
||||
(ngModelChange)="searchPhraseUpdate.next($event)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
}
|
||||
@if (searchVisible) {
|
||||
<div class="search">
|
||||
<mat-form-field class="search-input">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input
|
||||
matInput
|
||||
placeholder="Search by name"
|
||||
type="search"
|
||||
[ngModel]="searchPhrase()"
|
||||
(ngModelChange)="searchPhraseUpdate.next($event)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<mat-menu #sortPlaylistMenu="matMenu">
|
||||
<section (click)="$event.stopPropagation()">
|
||||
<button mat-menu-item (click)="setSortType('date')">
|
||||
<mat-icon>calendar_today</mat-icon>
|
||||
<span>{{ 'CHANNELS.SORT_BY_DATE' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="setSortType('rating')">
|
||||
<mat-icon>star</mat-icon>
|
||||
<span>{{ 'CHANNELS.SORT_BY_RATING' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="setSortType('alpha')">
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
<span>{{ 'CHANNELS.SORT_BY_NAME' | translate }}</span>
|
||||
</button>
|
||||
</section>
|
||||
<section (click)="$event.stopPropagation()">
|
||||
<button mat-menu-item (click)="setSortType('date')">
|
||||
<mat-icon>calendar_today</mat-icon>
|
||||
<span>{{ 'CHANNELS.SORT_BY_DATE' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="setSortType('rating')">
|
||||
<mat-icon>star</mat-icon>
|
||||
<span>{{ 'CHANNELS.SORT_BY_RATING' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="setSortType('alpha')">
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
<span>{{ 'CHANNELS.SORT_BY_NAME' | translate }}</span>
|
||||
</button>
|
||||
</section>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NgFor, NgIf } from '@angular/common';
|
||||
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -44,20 +44,18 @@ import { PortalStore } from '../portal.store';
|
||||
templateUrl: './navigation-bar.component.html',
|
||||
styleUrls: ['./navigation-bar.component.scss'],
|
||||
imports: [
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
NgFor,
|
||||
NgIf,
|
||||
RouterLink,
|
||||
TranslateModule,
|
||||
MatMenuModule,
|
||||
MatCheckboxModule,
|
||||
MatTooltipModule,
|
||||
],
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
RouterLink,
|
||||
TranslateModule,
|
||||
MatMenuModule,
|
||||
MatCheckboxModule,
|
||||
MatTooltipModule
|
||||
],
|
||||
})
|
||||
export class NavigationBarComponent implements OnChanges {
|
||||
@Input({ required: true }) breadcrumbs: Breadcrumb[];
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
<h3>Seasons and episodes</h3>
|
||||
<div class="seasons-container">
|
||||
<ng-container *ngIf="!selectedSeason; else episodesView">
|
||||
<mat-card
|
||||
class="season-item"
|
||||
*ngFor="let season of seasons | keyvalue"
|
||||
(click)="selectSeason(season.key)"
|
||||
@if (!selectedSeason) {
|
||||
@for (season of seasons | keyvalue; track season) {
|
||||
<mat-card
|
||||
class="season-item"
|
||||
(click)="selectSeason(season.key)"
|
||||
>
|
||||
<mat-card-content>Season {{ season.key }}</mat-card-content>
|
||||
</mat-card>
|
||||
</ng-container>
|
||||
<mat-card-content>Season {{ season.key }}</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
} @else {
|
||||
<mat-card class="episode-item" (click)="selectedSeason = undefined">
|
||||
<div class="episode-title">< Back to seasons</div>
|
||||
</mat-card>
|
||||
@for (episode of seasons[selectedSeason]; track episode) {
|
||||
<mat-card
|
||||
class="episode-item"
|
||||
(click)="selectEpisode(episode)"
|
||||
>
|
||||
@if (episode.info.movie_image) {
|
||||
<mat-card-content
|
||||
><img [src]="episode.info.movie_image" class="episode-cover"
|
||||
/></mat-card-content>
|
||||
}
|
||||
<div class="episode-title">
|
||||
{{ episode.episode_num }}. {{ episode.title }}
|
||||
</div>
|
||||
</mat-card>
|
||||
}
|
||||
}
|
||||
|
||||
<ng-template #episodesView>
|
||||
<mat-card class="episode-item" (click)="selectedSeason = undefined">
|
||||
<div class="episode-title">< Back to seasons</div>
|
||||
</mat-card>
|
||||
<mat-card
|
||||
class="episode-item"
|
||||
*ngFor="let episode of seasons[selectedSeason]"
|
||||
(click)="selectEpisode(episode)"
|
||||
>
|
||||
<mat-card-content *ngIf="episode.info.movie_image"
|
||||
><img [src]="episode.info.movie_image" class="episode-cover"
|
||||
/></mat-card-content>
|
||||
<div class="episode-title">
|
||||
{{ episode.episode_num }}. {{ episode.title }}
|
||||
</div>
|
||||
</mat-card>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { KeyValuePipe, NgFor, NgIf } from '@angular/common';
|
||||
import { KeyValuePipe } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { XtreamSerieEpisode } from '../../../../shared/xtream-serie-details.interface';
|
||||
@@ -7,7 +7,7 @@ import { XtreamSerieEpisode } from '../../../../shared/xtream-serie-details.inte
|
||||
selector: 'app-season-container',
|
||||
templateUrl: './season-container.component.html',
|
||||
styleUrls: ['./season-container.component.scss'],
|
||||
imports: [KeyValuePipe, NgFor, MatCardModule, NgIf],
|
||||
imports: [KeyValuePipe, MatCardModule],
|
||||
})
|
||||
export class SeasonContainerComponent {
|
||||
@Input({ required: true }) seasons: Record<string, XtreamSerieEpisode[]>;
|
||||
|
||||
@@ -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,67 +7,83 @@
|
||||
') repeat-x'
|
||||
: 'none'
|
||||
"
|
||||
>
|
||||
<div class="image">
|
||||
<img *ngIf="item.info.cover" [src]="item.info.cover" />
|
||||
</div>
|
||||
<div class="details">
|
||||
<h2>{{ item.info.name }}</h2>
|
||||
<label *ngIf="item.info.plot">
|
||||
{{ item.info.plot }}
|
||||
</label>
|
||||
<label *ngIf="item.info.releaseDate">
|
||||
<div class="label">{{ 'XTREAM.RELEASE_DATE' | translate }}:</div>
|
||||
{{ item.info.releaseDate }}
|
||||
</label>
|
||||
<label *ngIf="item.info.genre">
|
||||
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
|
||||
{{ item.info.genre }}
|
||||
</label>
|
||||
<label *ngIf="item.info.rating">
|
||||
<div class="label">{{ 'XTREAM.RATING' | translate }}:</div>
|
||||
{{ item.info.rating }}
|
||||
</label>
|
||||
<div *ngIf="item.info.cast">
|
||||
<div class="label">{{ 'XTREAM.CAST' | translate }}:</div>
|
||||
{{ item.info.cast }}
|
||||
>
|
||||
<div class="image">
|
||||
@if (item.info.cover) {
|
||||
<img [src]="item.info.cover" />
|
||||
}
|
||||
</div>
|
||||
<div class="details">
|
||||
<h2>{{ item.info.name }}</h2>
|
||||
@if (item.info.plot) {
|
||||
<label>
|
||||
{{ item.info.plot }}
|
||||
</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.rating) {
|
||||
<label>
|
||||
<div class="label">{{ 'XTREAM.RATING' | translate }}:</div>
|
||||
{{ item.info.rating }}
|
||||
</label>
|
||||
}
|
||||
@if (item.info.cast) {
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.CAST' | translate }}:</div>
|
||||
{{ item.info.cast }}
|
||||
</div>
|
||||
}
|
||||
@if (item.info.director) {
|
||||
<label>
|
||||
<div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div>
|
||||
{{ item.info.director }}
|
||||
</label>
|
||||
}
|
||||
@if (item.info.episode_run_time) {
|
||||
<label>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.EPISODE_RUN_TIME' | translate }}:
|
||||
</div>
|
||||
<label *ngIf="item.info.director">
|
||||
<div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div>
|
||||
{{ item.info.director }}
|
||||
</label>
|
||||
<label *ngIf="item.info.episode_run_time">
|
||||
<div class="label">
|
||||
{{ 'XTREAM.EPISODE_RUN_TIME' | translate }}:
|
||||
</div>
|
||||
{{ item.info.episode_run_time }}
|
||||
</label>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
*ngIf="!isFavorite; else removeFromFavoritesButton"
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star_outline</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
{{ item.info.episode_run_time }}
|
||||
</label>
|
||||
}
|
||||
<div class="action-buttons">
|
||||
@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-flat-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
}
|
||||
|
||||
<ng-template #removeFromFavoritesButton>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-season-container
|
||||
[seasons]="item.episodes"
|
||||
(episodeClicked)="playClicked.emit($event)"
|
||||
/>
|
||||
[seasons]="item.episodes"
|
||||
(episodeClicked)="playClicked.emit($event)"
|
||||
/>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NgIf } from '@angular/common';
|
||||
|
||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
@@ -18,12 +18,11 @@ import { SeasonContainerComponent } from '../season-container/season-container.c
|
||||
templateUrl: './serial-details.component.html',
|
||||
styleUrls: ['../detail-view.scss'],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
NgIf,
|
||||
SeasonContainerComponent,
|
||||
TranslateModule,
|
||||
],
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
SeasonContainerComponent,
|
||||
TranslateModule
|
||||
],
|
||||
})
|
||||
export class SerialDetailsComponent {
|
||||
@Input({ required: true }) item: XtreamSerieDetails;
|
||||
|
||||
@@ -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,93 +7,112 @@
|
||||
') 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>
|
||||
} @else {
|
||||
<div class="placeholder-cover"></div>
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="details">
|
||||
<h2>{{ item.info.name }}</h2>
|
||||
<label *ngIf="item.info.description">
|
||||
{{ item.info.description }}
|
||||
</label>
|
||||
<label *ngIf="item.info.releasedate">
|
||||
<div class="label">{{ 'XTREAM.RELEASE_DATE' | translate }}:</div>
|
||||
{{ item.info.releasedate }}
|
||||
</label>
|
||||
<label *ngIf="item.info.genre">
|
||||
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
|
||||
{{ item.info.genre }}
|
||||
</label>
|
||||
<label *ngIf="item.info.country">
|
||||
<div class="label">{{ 'XTREAM.COUNTRY' | translate }}:</div>
|
||||
{{ item.info.country }}
|
||||
</label>
|
||||
<div *ngIf="item.info.actors">
|
||||
<div class="label">{{ 'XTREAM.ACTORS' | translate }}:</div>
|
||||
{{ item.info.actors }}
|
||||
</div>
|
||||
<label *ngIf="item.info.director">
|
||||
<div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div>
|
||||
{{ item.info.director }}
|
||||
</label>
|
||||
<label *ngIf="item.info.duration">
|
||||
<div class="label">{{ 'XTREAM.DURATION' | translate }}:</div>
|
||||
{{ item.info.duration }}
|
||||
</label>
|
||||
<label *ngIf="item.info.rating_imdb">
|
||||
<div class="label">{{ 'XTREAM.IMDB_RATING' | translate }}:</div>
|
||||
{{ item.info.rating_imdb }}
|
||||
</label>
|
||||
<label *ngIf="item.info.rating_kinopoisk">
|
||||
<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
|
||||
>
|
||||
<button
|
||||
*ngIf="!isFavorite; else removeFromFavoritesButton"
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star_outline</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
|
||||
<ng-template #removeFromFavoritesButton>
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="youtube-trailer" *ngIf="item.info.youtube_trailer">
|
||||
<h3>{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}</h3>
|
||||
<iframe
|
||||
</div>
|
||||
@if (item.info.youtube_trailer) {
|
||||
<div class="youtube-trailer">
|
||||
<h3>{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}</h3>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="450"
|
||||
[src]="
|
||||
@@ -104,5 +123,6 @@
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
></iframe>
|
||||
</div>
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NgIf } from '@angular/common';
|
||||
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -21,13 +21,12 @@ import { SafePipe } from './safe.pipe';
|
||||
templateUrl: './vod-details.component.html',
|
||||
styleUrls: ['../detail-view.scss'],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatIcon,
|
||||
MatProgressSpinnerModule,
|
||||
NgIf,
|
||||
SafePipe,
|
||||
TranslatePipe,
|
||||
],
|
||||
MatButtonModule,
|
||||
MatIcon,
|
||||
MatProgressSpinnerModule,
|
||||
SafePipe,
|
||||
TranslatePipe
|
||||
],
|
||||
})
|
||||
export class VodDetailsComponent implements OnInit {
|
||||
@Input({ required: true }) item: XtreamVodDetails;
|
||||
|
||||
@@ -108,7 +108,7 @@ type LayoutView =
|
||||
PlaylistErrorViewComponent,
|
||||
TranslateModule,
|
||||
LiveStreamLayoutComponent,
|
||||
]
|
||||
],
|
||||
})
|
||||
export class XtreamMainContainerComponent implements OnInit {
|
||||
private readonly dataService = inject(DataService);
|
||||
@@ -359,6 +359,8 @@ export class XtreamMainContainerComponent implements OnInit {
|
||||
{
|
||||
data: { streamUrl, title },
|
||||
width: '80%',
|
||||
maxWidth: '1200px',
|
||||
maxHeight: '90vh',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -385,6 +387,8 @@ export class XtreamMainContainerComponent implements OnInit {
|
||||
this.dialog.open(PlayerDialogComponent, {
|
||||
data: { streamUrl, player, title: episode.title },
|
||||
width: '80%',
|
||||
maxWidth: '1200px',
|
||||
maxHeight: '90vh',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+3
-14
@@ -1,5 +1,5 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { platformBrowser } from '@angular/platform-browser';
|
||||
import { AppModule } from './app/app.module';
|
||||
import { AppConfig } from './environments/environment';
|
||||
|
||||
@@ -7,17 +7,6 @@ if (AppConfig.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
// Detect if running in Tauri
|
||||
const isTauri = window && (window as any).__TAURI__;
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule, {
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
.then(() => {
|
||||
// Only register service worker for web builds
|
||||
if (!isTauri && 'serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/ngsw-worker.js');
|
||||
}
|
||||
})
|
||||
platformBrowser()
|
||||
.bootstrapModule(AppModule)
|
||||
.catch((err) => console.error(err));
|
||||
+22
-15
@@ -1,21 +1,28 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"module": "es2020",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "ES2022",
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"lib": ["es2017", "es2016", "es2015", "dom"],
|
||||
"resolveJsonModule": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true,
|
||||
"useDefineForClassFields": false
|
||||
"module": "es2020",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"moduleResolution": "bundler",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "ES2022",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"lib": [
|
||||
"es2017",
|
||||
"es2016",
|
||||
"es2015",
|
||||
"dom"
|
||||
],
|
||||
"resolveJsonModule": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true,
|
||||
"useDefineForClassFields": false
|
||||
},
|
||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
||||
"include": ["src/**/*.d.ts"],
|
||||
|
||||
Reference in new issue
Block a user