refactor(nx): app restructuring
No files matched your search
@@ -1,16 +0,0 @@
|
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.203.0/containers/typescript-node/.devcontainer/base.Dockerfile
|
||||
|
||||
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-buster
|
||||
ARG VARIANT="16-bullseye"
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
# [Optional] Uncomment if you want to install an additional version of node using nvm
|
||||
ARG EXTRA_NODE_VERSION=16.10
|
||||
RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
||||
|
||||
# [Optional] Uncomment if you want to install more global node packages
|
||||
# RUN su node -c "npm install -g <your-package-list -here>"
|
||||
@@ -1,42 +0,0 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.203.0/containers/typescript-node
|
||||
{
|
||||
"name": "Node.js & TypeScript",
|
||||
"runArgs": ["--init"],
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
// Update 'VARIANT' to pick a Node version: 16, 14, 12.
|
||||
// Append -bullseye or -buster to pin to an OS version.
|
||||
// Use -bullseye variants on local on arm64/Apple Silicon.
|
||||
"args": {
|
||||
"VARIANT": "16-bullseye"
|
||||
}
|
||||
},
|
||||
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"GitHub.copilot",
|
||||
"esbenp.prettier-vscode",
|
||||
"JohannesRudolph.file-ext-switcher",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"wayou.vscode-todo-highlight",
|
||||
"Gruntfuggly.todo-tree"
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "npm install",
|
||||
|
||||
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node"
|
||||
}
|
||||
@@ -1,64 +1,65 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/build
|
||||
/tmp
|
||||
/out-tsc
|
||||
/app-builds
|
||||
/release
|
||||
#compiled output
|
||||
/dist
|
||||
/build
|
||||
/tmp
|
||||
/out-tsc
|
||||
/app-builds
|
||||
/release
|
||||
/e2e/tracing
|
||||
/e2e/screenshots
|
||||
/test-results
|
||||
api.js
|
||||
epg-worker.js
|
||||
main.js
|
||||
menu.js
|
||||
/e2e/screenshots
|
||||
/test-results
|
||||
api.js
|
||||
epg-worker.js
|
||||
main.js
|
||||
menu.js
|
||||
src/**/*.js
|
||||
!src/karma.conf.js
|
||||
*.js.map
|
||||
data.db
|
||||
preload.js
|
||||
shared/*.js
|
||||
playwright-report
|
||||
!src/karma.conf.js
|
||||
*.js.map
|
||||
data.db
|
||||
preload.js
|
||||
shared/*.js
|
||||
playwright-report
|
||||
.nx
|
||||
server.js
|
||||
server.js
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
testem.log
|
||||
/typings
|
||||
.angular/*
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
testem.log
|
||||
/typings
|
||||
.angular/*
|
||||
|
||||
# e2e
|
||||
/e2e/*.js
|
||||
!/e2e/protractor.conf.js
|
||||
/e2e/*.map
|
||||
/e2e/db
|
||||
/e2e/*.js
|
||||
!/e2e/protractor.conf.js
|
||||
/e2e/*.map
|
||||
/e2e/db
|
||||
|
||||
# System Files
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Thumbs.db
|
||||
.angular
|
||||
@@ -1,5 +1,6 @@
|
||||
*.yml
|
||||
dist
|
||||
build
|
||||
coverage
|
||||
release
|
||||
*.yml
|
||||
dist
|
||||
build
|
||||
coverage
|
||||
release
|
||||
.angular
|
||||
@@ -5,4 +5,7 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
platform: process.platform,
|
||||
fetchPlaylistByUrl: (url: string) =>
|
||||
ipcRenderer.invoke('fetch-playlist-by-url', url),
|
||||
updatePlaylistFromFilePath: (filePath: string, title: string) =>
|
||||
ipcRenderer.invoke('update-playlist-from-file-path', filePath, title),
|
||||
openPlaylistFromFile: () => ipcRenderer.invoke('open-playlist-from-file'),
|
||||
});
|
||||
@@ -4,9 +4,10 @@
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { ipcMain } from 'electron';
|
||||
import { dialog, ipcMain } from 'electron';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import { createPlaylistObject, getFilenameFromUrl } from 'm3u-utils';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
export default class PlaylistEvents {
|
||||
static bootstrapPlaylistEvents(): Electron.IpcMain {
|
||||
@@ -16,7 +17,6 @@ export default class PlaylistEvents {
|
||||
|
||||
const https = require('https');
|
||||
|
||||
// Retrieve app version
|
||||
ipcMain.handle('fetch-playlist-by-url', async (event, url) => {
|
||||
try {
|
||||
const agent = new https.Agent({
|
||||
@@ -45,3 +45,57 @@ ipcMain.handle('fetch-playlist-by-url', async (event, url) => {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
'update-playlist-from-file-path',
|
||||
async (event, filePath, title) => {
|
||||
const playlist = await readFile(filePath, 'utf-8');
|
||||
const parsedPlaylist = parse(playlist);
|
||||
const playlistObject = createPlaylistObject(
|
||||
title,
|
||||
parsedPlaylist,
|
||||
filePath,
|
||||
'FILE'
|
||||
);
|
||||
return playlistObject;
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle('open-playlist-from-file', async () => {
|
||||
// 1. Await the result from the dialog
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{ name: 'Playlists', extensions: ['m3u', 'm3u8', 'pls', 'txt'] },
|
||||
{ name: 'All Files', extensions: ['*'] },
|
||||
],
|
||||
});
|
||||
|
||||
if (canceled || filePaths.length === 0) {
|
||||
console.log('User canceled file open dialog.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const filePath = filePaths[0];
|
||||
|
||||
try {
|
||||
const fileContent = await readFile(filePath, 'utf-8');
|
||||
|
||||
const parsedPlaylist = parse(fileContent);
|
||||
const playlistObject = createPlaylistObject(
|
||||
'from file',
|
||||
parsedPlaylist,
|
||||
filePath,
|
||||
'FILE'
|
||||
);
|
||||
|
||||
console.log('Sending playlist object to renderer:', playlistObject);
|
||||
|
||||
// 5. Return the final object. This value is sent back to Angular.
|
||||
return playlistObject;
|
||||
} catch (error) {
|
||||
console.error('Error reading or parsing the file:', error);
|
||||
// It's good practice to throw the error so the renderer can catch it
|
||||
throw new Error('Failed to process the selected file.');
|
||||
}
|
||||
});
|
||||
@@ -2,33 +2,36 @@ import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../eslint.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'app',
|
||||
style: 'camelCase',
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'app',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'app',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {
|
||||
'@angular-eslint/template/click-events-have-key-events': 'off',
|
||||
'@angular-eslint/template/interactive-supports-focus': 'off',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'app',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
@@ -30,6 +30,7 @@
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"baseHref": "./",
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
|
||||
@@ -13,23 +13,17 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'playlists',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'./player/components/video-player/video-player.component'
|
||||
).then((c) => c.VideoPlayerComponent),
|
||||
import('components').then((c) => c.VideoPlayerComponent),
|
||||
},
|
||||
{
|
||||
path: 'iptv',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'./player/components/video-player/video-player.component'
|
||||
).then((c) => c.VideoPlayerComponent),
|
||||
import('components').then((c) => c.VideoPlayerComponent),
|
||||
},
|
||||
{
|
||||
path: 'playlists/:id',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'./player/components/video-player/video-player.component'
|
||||
).then((c) => c.VideoPlayerComponent),
|
||||
import('components').then((c) => c.VideoPlayerComponent),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
|
||||
@@ -1,9 +1 @@
|
||||
<router-outlet></router-outlet>
|
||||
<!-- whats new dialog -->
|
||||
@if (isDialogVisible$ | async) {
|
||||
<ngx-whats-new
|
||||
(closeModal)="setDialogVisibility(false)"
|
||||
[items]="modals"
|
||||
[options]="options"
|
||||
/>
|
||||
}
|
||||
<router-outlet />
|
||||
@@ -14,16 +14,16 @@ import { MockComponent, MockModule, MockPipe, MockProviders } from 'ng-mocks';
|
||||
import { NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { NgxWhatsNewComponent } from 'ngx-whats-new';
|
||||
import { of } from 'rxjs';
|
||||
import { DataService } from '../../../../libs/services/src/lib/data.service';
|
||||
import { PlaylistsService } from '../../../../libs/services/src/lib/playlists.service';
|
||||
import { Language } from '../../../../libs/shared/interfaces/src/lib/language.enum';
|
||||
import { STORE_KEY } from '../../../../libs/shared/interfaces/src/lib/store-keys.enum';
|
||||
import { Theme } from '../../../../libs/shared/interfaces/src/lib/theme.enum';
|
||||
import { AppComponent } from './app.component';
|
||||
import { DataService } from './services/data.service';
|
||||
import { ElectronServiceStub } from './services/electron.service.stub';
|
||||
import { PlaylistsService } from './services/playlists.service';
|
||||
import { SettingsService } from './services/settings.service';
|
||||
import { WhatsNewService } from './services/whats-new.service';
|
||||
import { WhatsNewServiceStub } from './services/whats-new.service.stub';
|
||||
import { Language } from './settings/language.enum';
|
||||
import { Theme } from './settings/theme.enum';
|
||||
import { STORE_KEY } from './shared/enums/store-keys.enum';
|
||||
|
||||
jest.spyOn(global.console, 'error').mockImplementation(() => {});
|
||||
|
||||
|
||||
@@ -4,25 +4,23 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import * as semver from 'semver';
|
||||
import { IpcCommand } from '../shared/ipc-command.class';
|
||||
/* import * as semver from 'semver'; */
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import {
|
||||
ERROR,
|
||||
IpcCommand,
|
||||
Language,
|
||||
OPEN_FILE,
|
||||
Settings,
|
||||
SETTINGS_UPDATE,
|
||||
SHOW_WHATS_NEW,
|
||||
STORE_KEY,
|
||||
Theme,
|
||||
VIEW_ADD_PLAYLIST,
|
||||
VIEW_SETTINGS,
|
||||
} from '../shared/ipc-commands';
|
||||
import { DataService } from './services/data.service';
|
||||
import { EpgService } from './services/epg.service';
|
||||
} from 'shared-interfaces';
|
||||
import { DataService } from '../../../../libs/services/src/lib/data.service';
|
||||
import { EpgService } from '../../../../libs/services/src/lib/epg.service';
|
||||
import { SettingsService } from './services/settings.service';
|
||||
import { WhatsNewService } from './services/whats-new.service';
|
||||
import { Language } from './settings/language.enum';
|
||||
import { Settings } from './settings/settings.interface';
|
||||
import { Theme } from './settings/theme.enum';
|
||||
import { STORE_KEY } from './shared/enums/store-keys.enum';
|
||||
import * as PlaylistActions from './state/actions';
|
||||
import { RecentlyViewedComponent } from './xtream-tauri/recently-viewed/recently-viewed.component';
|
||||
import { SearchResultsComponent } from './xtream-tauri/search-results/search-results.component';
|
||||
|
||||
@@ -40,22 +38,11 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private translate = inject(TranslateService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private whatsNewService = inject(WhatsNewService);
|
||||
|
||||
/** Visibility flag of the "what is new" modal dialog */
|
||||
readonly isDialogVisible$ = this.whatsNewService.dialogState$;
|
||||
|
||||
/** Dialog options */
|
||||
readonly options = this.whatsNewService.options;
|
||||
|
||||
/** Modals to show for the updated version of the application */
|
||||
modals: any[] = [];
|
||||
|
||||
/** List of ipc commands with function mapping */
|
||||
private readonly commandsList = [
|
||||
new IpcCommand(VIEW_ADD_PLAYLIST, () => this.navigateToRoute('/')),
|
||||
new IpcCommand(VIEW_SETTINGS, () => this.navigateToRoute('/settings')),
|
||||
new IpcCommand(SHOW_WHATS_NEW, () => this.showWhatsNewDialog()),
|
||||
new IpcCommand(ERROR, (response: { message: string; status: number }) =>
|
||||
this.showErrorAsNotification(response)
|
||||
),
|
||||
@@ -107,7 +94,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.setRendererListeners();
|
||||
this.initSettings();
|
||||
this.handleWhatsNewDialog();
|
||||
/* this.handleWhatsNewDialog(); */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,36 +160,17 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Checks the actual version of the application and shows the "what is new" dialog if the updated version was detected
|
||||
*/
|
||||
handleWhatsNewDialog(): void {
|
||||
/* handleWhatsNewDialog(): void {
|
||||
const actualVersion = this.dataService.getAppVersion();
|
||||
this.settingsService
|
||||
.getValueFromLocalStorage(STORE_KEY.Version)
|
||||
.subscribe((version: string) => {
|
||||
const isNewVersion = semver.gt(
|
||||
actualVersion,
|
||||
version || '0.0.0'
|
||||
);
|
||||
if (!version || isNewVersion) {
|
||||
this.modals =
|
||||
this.whatsNewService.getModalsByVersion(actualVersion);
|
||||
this.setDialogVisibility(true);
|
||||
}
|
||||
.subscribe(() => {
|
||||
this.settingsService.setValueToLocalStorage(
|
||||
STORE_KEY.Version,
|
||||
actualVersion
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility flag of the modal window
|
||||
* @param visible show/hide window flag
|
||||
*/
|
||||
setDialogVisibility(visible: boolean): void {
|
||||
if (this.modals.length > 0) {
|
||||
this.whatsNewService.changeDialogVisibleState(visible);
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
/**
|
||||
* Navigate to the specified route
|
||||
@@ -212,16 +180,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.router.navigateByUrl(route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the "what is new" dialog
|
||||
*/
|
||||
showWhatsNewDialog(): void {
|
||||
this.modals = this.whatsNewService.getModalsByVersion(
|
||||
this.dataService.getAppVersion()
|
||||
);
|
||||
this.setDialogVisibility(true);
|
||||
}
|
||||
|
||||
showErrorAsNotification(response: { message: string; status: number }) {
|
||||
this.snackBar.open(
|
||||
`Error: ${response?.message ?? 'Something went wrong'} (Status: ${
|
||||
|
||||
@@ -7,27 +7,23 @@ import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { DataService } from '@iptvnator/services';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { StoreRouterConnectingModule, routerReducer } from '@ngrx/router-store';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import { PlaylistEffects, playlistReducer } from 'm3u-state';
|
||||
import { NgxIndexedDBModule, NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { NgxWhatsNewModule } from 'ngx-whats-new';
|
||||
import 'reflect-metadata';
|
||||
import { dbConfig } from 'shared-interfaces';
|
||||
import { AppConfig } from '../environments/environment';
|
||||
import '../polyfills';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { dbConfig } from './indexed-db.config';
|
||||
import { DataService } from './services/data.service';
|
||||
import { PwaService } from './services/pwa.service';
|
||||
import { TauriService } from './services/tauri.service';
|
||||
import { PlaylistEffects } from './state/effects';
|
||||
import { playlistReducer } from './state/reducers';
|
||||
|
||||
// AoT requires an exported function for factories
|
||||
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
@@ -54,7 +50,6 @@ export function DataFactory() {
|
||||
AppConfig.environment === 'WEB'
|
||||
? NgxIndexedDBModule.forRoot(dbConfig)
|
||||
: [],
|
||||
NgxWhatsNewModule,
|
||||
NgxIndexedDBModule.forRoot(dbConfig),
|
||||
NgxSkeletonLoaderModule.forRoot({
|
||||
animation: 'pulse',
|
||||
@@ -68,7 +63,7 @@ export function DataFactory() {
|
||||
},
|
||||
}),
|
||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||
enabled: AppConfig.production && !isTauri(),
|
||||
enabled: AppConfig.production && !!window.electron,
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
StoreModule.forRoot({
|
||||
|
||||
@@ -2,10 +2,7 @@ import { Component, EventEmitter, inject, Output } from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { readTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { parsePlaylist } from '../../state/actions';
|
||||
import { addPlaylist } from 'm3u-state';
|
||||
import { DragDropFileUploadDirective } from './drag-drop-file-upload.directive';
|
||||
|
||||
@Component({
|
||||
@@ -25,6 +22,8 @@ export class FileUploadComponent {
|
||||
|
||||
private readonly store = inject(Store);
|
||||
|
||||
private isDesktop = !!window.electron;
|
||||
|
||||
allowedContentTypes = [
|
||||
'application/mpegurl',
|
||||
'application/x-mpegurl',
|
||||
@@ -36,27 +35,26 @@ export class FileUploadComponent {
|
||||
];
|
||||
|
||||
async openDialog(fileField: HTMLInputElement) {
|
||||
if (isTauri()) {
|
||||
const path = await open({
|
||||
multiple: false,
|
||||
directory: false,
|
||||
filters: [
|
||||
{
|
||||
name: 'Playlist files',
|
||||
extensions: ['m3u', 'm3u8'],
|
||||
},
|
||||
],
|
||||
});
|
||||
const title = path.split('/').pop();
|
||||
const fileContent = await readTextFile(path);
|
||||
this.store.dispatch(
|
||||
parsePlaylist({
|
||||
uploadType: 'FILE',
|
||||
playlist: fileContent,
|
||||
title,
|
||||
path,
|
||||
if (this.isDesktop) {
|
||||
window.electron
|
||||
.openPlaylistFromFile()
|
||||
.then((playlistObject) => {
|
||||
if (playlistObject) {
|
||||
console.log(
|
||||
'Received playlist from Electron:',
|
||||
playlistObject
|
||||
);
|
||||
this.store.dispatch(
|
||||
addPlaylist({ playlist: playlistObject })
|
||||
);
|
||||
} else {
|
||||
// User canceled the dialog
|
||||
console.log('File selection was canceled.');
|
||||
}
|
||||
})
|
||||
);
|
||||
.catch((error) => {
|
||||
console.error('Error opening file:', error);
|
||||
});
|
||||
this.closeDialog.emit();
|
||||
} else {
|
||||
fileField.click();
|
||||
@@ -64,6 +62,8 @@ export class FileUploadComponent {
|
||||
}
|
||||
|
||||
upload(fileList: FileList) {
|
||||
if (this.isDesktop) return;
|
||||
|
||||
if (!this.allowedContentTypes.includes(fileList[0].type)) {
|
||||
this.fileRejected.emit(fileList[0].name);
|
||||
return;
|
||||
|
||||
@@ -10,10 +10,10 @@ import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MockComponent, MockModule, MockProvider } from 'ng-mocks';
|
||||
import { Observable } from 'rxjs';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { DataService } from '../../../../../libs/services/src/lib/data.service';
|
||||
import { RecentPlaylistsComponent } from '../../../../../libs/ui/components/src/lib/recent-playlists/recent-playlists.component';
|
||||
import { ElectronServiceStub } from '../services/electron.service.stub';
|
||||
import { HomeComponent } from './home.component';
|
||||
import { RecentPlaylistsComponent } from './recent-playlists/recent-playlists.component';
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Component, inject, OnDestroy } from '@angular/core';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { DataService } from '@iptvnator/services';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { ERROR, PLAYLIST_PARSE_RESPONSE } from '../../shared/ipc-commands';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { addPlaylist } from 'm3u-state';
|
||||
import { ERROR, Playlist, PLAYLIST_PARSE_RESPONSE } from 'shared-interfaces';
|
||||
import { RecentPlaylistsComponent } from '../../../../../libs/ui/components/src/lib/recent-playlists/recent-playlists.component';
|
||||
import { HeaderComponent } from '../shared/components/header/header.component';
|
||||
import { addPlaylist } from '../state/actions';
|
||||
import { RecentPlaylistsComponent } from './recent-playlists/recent-playlists.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
<form
|
||||
[formGroup]="playlistDetails"
|
||||
(ngSubmit)="saveChanges(playlistDetails.value)"
|
||||
>
|
||||
<h2 mat-dialog-title>
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.PLAYLIST_DETAILS' | translate }}
|
||||
</h2>
|
||||
<mat-dialog-content class="mat-typography">
|
||||
<mat-form-field class="full-width">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.TITLE' | translate
|
||||
}}</mat-label>
|
||||
<input formControlName="title" matInput />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width" *ngIf="playlist.portalUrl">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.SERVER_URL' | translate
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="portalUrl" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width" *ngIf="playlist.macAddress">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.MAC_ADDRESS' | translate
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="macAddress" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width" *ngIf="playlist.serverUrl">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.SERVER_URL' | translate
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="serverUrl" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width" *ngIf="playlist.username">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.USERNAME' | translate
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="username" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width" *ngIf="playlist.password">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.PASSWORD' | translate
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="password" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width" *ngIf="playlist.url">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.FROM_URL' | translate
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="url" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width" *ngIf="playlist.filename">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.ORIGINAL_FILENAME' | translate
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="filename" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.IMPORT_DATE' | translate
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="importDate" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width" *ngIf="playlist.count">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.CHANNELS' | translate
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="count" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width mb-10" *ngIf="isTauri">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.USER_AGENT' | translate
|
||||
}}</mat-label>
|
||||
<input formControlName="userAgent" matInput />
|
||||
<mat-hint>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.CUSTOM_USER_AGENT' | translate
|
||||
}}</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width" *ngIf="playlist.filePath">
|
||||
<mat-label>
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.FILE_PATH' | translate }}
|
||||
</mat-label>
|
||||
<input formControlName="filePath" matInput />
|
||||
<mat-hint *ngIf="playlist.updateState === 2">
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate }}
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
<ng-container *ngIf="isTauri && (playlist.url || playlist.filePath)">
|
||||
<mat-checkbox class="full-width" formControlName="autoRefresh">
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE' | translate }}
|
||||
</mat-checkbox>
|
||||
<p>
|
||||
{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE_DESCRIPTION'
|
||||
| translate
|
||||
}}
|
||||
</p>
|
||||
</ng-container>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="accent"
|
||||
type="button"
|
||||
(click)="exportPlaylist()"
|
||||
*ngIf="!playlist.serverUrl && !playlist.portalUrl"
|
||||
>
|
||||
<mat-icon>download</mat-icon>
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.EXPORT_PLAYLIST' | translate }}
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<button mat-button mat-dialog-close>
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.CLOSE' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
mat-dialog-close
|
||||
color="accent"
|
||||
type="submit"
|
||||
[disabled]="!playlistDetails.valid || playlistDetails.pristine"
|
||||
>
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.SAVE' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
@@ -11,10 +11,10 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { addPlaylist } from 'm3u-state';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { addPlaylist } from '../../state/actions';
|
||||
import { DataService } from '../../../../../../libs/services/src/lib/data.service';
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
|
||||
@@ -6,7 +6,7 @@ import { By } from '@angular/platform-browser';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MockModule } from 'ng-mocks';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { DataService } from '../../../../../../libs/services/src/lib/data.service';
|
||||
import { ElectronServiceStub } from '../../services/electron.service.stub';
|
||||
import { UrlUploadComponent } from './url-upload.component';
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export class UrlUploadComponent implements OnInit {
|
||||
@Output() urlAdded: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
form: FormGroup;
|
||||
readonly isDesktop = window.electron;
|
||||
readonly isDesktop = !!window.electron;
|
||||
|
||||
ngOnInit() {
|
||||
const urlRegex = '(https?://.*?)';
|
||||
|
||||
@@ -12,13 +12,13 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { addPlaylist } from 'm3u-state';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
PortalStatus,
|
||||
PortalStatusService,
|
||||
} from '../../services/portal-status.service';
|
||||
import { addPlaylist } from '../../state/actions';
|
||||
} from '../../../../../../libs/services/src/lib/portal-status.service';
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { OPEN_MPV_PLAYER, OPEN_VLC_PLAYER } from '../../shared/ipc-commands';
|
||||
import { VideoPlayer } from '../settings/settings.interface';
|
||||
import {
|
||||
OPEN_MPV_PLAYER,
|
||||
OPEN_VLC_PLAYER,
|
||||
VideoPlayer,
|
||||
} from 'shared-interfaces';
|
||||
import { DataService } from '../../../../../libs/services/src/lib/data.service';
|
||||
import { ExternalPlayerInfoDialogComponent } from '../shared/components/external-player-info-dialog/external-player-info-dialog.component';
|
||||
import {
|
||||
PlayerDialogComponent,
|
||||
PlayerDialogData,
|
||||
} from '../xtream-tauri/player-dialog/player-dialog.component';
|
||||
import { DataService } from './data.service';
|
||||
import { SettingsStore } from './settings-store.service';
|
||||
|
||||
@Injectable({
|
||||
|
||||
@@ -5,20 +5,20 @@ import { Params } from '@angular/router';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { catchError, firstValueFrom, throwError } from 'rxjs';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { AppConfig } from '../../environments/environment';
|
||||
import {
|
||||
ERROR,
|
||||
Playlist,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
PLAYLIST_UPDATE,
|
||||
STALKER_REQUEST,
|
||||
XTREAM_REQUEST,
|
||||
XTREAM_RESPONSE,
|
||||
} from '../../shared/ipc-commands';
|
||||
import * as PlaylistActions from '../state/actions';
|
||||
import { DataService } from './data.service';
|
||||
} from 'shared-interfaces';
|
||||
import { DataService } from '../../../../../libs/services/src/lib/data.service';
|
||||
import { AppConfig } from '../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
||||
@@ -8,11 +8,14 @@ import {
|
||||
} from '@ngrx/signals';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Language } from '../settings/language.enum';
|
||||
import { Settings, VideoPlayer } from '../settings/settings.interface';
|
||||
import { StreamFormat } from '../settings/stream-format.enum';
|
||||
import { Theme } from '../settings/theme.enum';
|
||||
import { STORE_KEY } from '../shared/enums/store-keys.enum';
|
||||
import { Language } from '../../../../../libs/shared/interfaces/src/lib/language.enum';
|
||||
import {
|
||||
Settings,
|
||||
VideoPlayer,
|
||||
} from '../../../../../libs/shared/interfaces/src/lib/settings.interface';
|
||||
import { STORE_KEY } from '../../../../../libs/shared/interfaces/src/lib/store-keys.enum';
|
||||
import { StreamFormat } from '../../../../../libs/shared/interfaces/src/lib/stream-format.enum';
|
||||
import { Theme } from '../../../../../libs/shared/interfaces/src/lib/theme.enum';
|
||||
|
||||
const DEFAULT_SETTINGS: Settings = {
|
||||
player: VideoPlayer.VideoJs,
|
||||
|
||||
@@ -2,8 +2,8 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { of } from 'rxjs';
|
||||
import { Theme } from '../settings/theme.enum';
|
||||
import { STORE_KEY } from '../shared/enums/store-keys.enum';
|
||||
import { STORE_KEY } from '../../../../../libs/shared/interfaces/src/lib/store-keys.enum';
|
||||
import { Theme } from '../../../../../libs/shared/interfaces/src/lib/theme.enum';
|
||||
import { SettingsService } from './settings.service';
|
||||
|
||||
describe('Service: Settings', () => {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { catchError, map, Observable } from 'rxjs';
|
||||
import { STORE_KEY } from '../shared/enums/store-keys.enum';
|
||||
import { Theme } from './../settings/theme.enum';
|
||||
import { STORE_KEY } from '../../../../../libs/shared/interfaces/src/lib/store-keys.enum';
|
||||
import { Theme } from '../../../../../libs/shared/interfaces/src/lib/theme.enum';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
||||
@@ -7,20 +7,20 @@ import { readTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import { createPlaylistObject } from 'm3u-utils';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { AppConfig } from '../../environments/environment';
|
||||
import {
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
AUTO_UPDATE_PLAYLISTS_RESPONSE,
|
||||
EPG_GET_PROGRAM_DONE,
|
||||
ERROR,
|
||||
Playlist,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
PLAYLIST_UPDATE,
|
||||
PLAYLIST_UPDATE_RESPONSE,
|
||||
XTREAM_RESPONSE,
|
||||
} from '../../shared/ipc-commands';
|
||||
import { DataService } from './data.service';
|
||||
} from 'shared-interfaces';
|
||||
import { DataService } from '../../../../../libs/services/src/lib/data.service';
|
||||
import { AppConfig } from '../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -56,32 +56,16 @@ export class TauriService extends DataService {
|
||||
|
||||
async sendIpcEvent(type: string, payload?: unknown) {
|
||||
if (type === PLAYLIST_PARSE_BY_URL) {
|
||||
this.fetchFromUrl(payload);
|
||||
this.fetchM3uPlaylistFromUrl(payload);
|
||||
} else if (type === PLAYLIST_UPDATE) {
|
||||
const data = payload as {
|
||||
id: string;
|
||||
filePath: string;
|
||||
title: string;
|
||||
};
|
||||
const playlist = await readTextFile(data.filePath);
|
||||
const parsedPlaylist = parse(playlist);
|
||||
const playlistObject = createPlaylistObject(
|
||||
data.title,
|
||||
parsedPlaylist,
|
||||
data.filePath,
|
||||
'FILE'
|
||||
// TODO: update playlist from URL?
|
||||
this.updateM3uPlaylistFromFile(
|
||||
payload as {
|
||||
id: string;
|
||||
filePath: string;
|
||||
title: string;
|
||||
}
|
||||
);
|
||||
|
||||
window.postMessage({
|
||||
type: PLAYLIST_UPDATE_RESPONSE,
|
||||
payload: {
|
||||
message: 'Success! The playlist was successfully updated',
|
||||
playlist: {
|
||||
...playlistObject,
|
||||
_id: data.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (type === 'XTREAM_REQUEST') {
|
||||
return await this.forwardXtreamRequest(
|
||||
payload as { url: string; params: Record<string, string> }
|
||||
@@ -164,7 +148,7 @@ export class TauriService extends DataService {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchStalkerData(payload: {
|
||||
private async fetchStalkerData(payload: {
|
||||
url: string;
|
||||
macAddress: string;
|
||||
params: Record<string, string>;
|
||||
@@ -205,49 +189,39 @@ export class TauriService extends DataService {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchFromUrl(payload: Partial<Playlist>) {
|
||||
private async fetchM3uPlaylistFromUrl(payload: Partial<Playlist>) {
|
||||
window.electron.fetchPlaylistByUrl(payload.url).then((result) => {
|
||||
// TODO: call store and decide and store where to store based on the isTemporary flag etc
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: { ...result, isTemporary: payload.isTemporary },
|
||||
});
|
||||
});
|
||||
/* try {
|
||||
const response = await fetch(payload.url);
|
||||
const responseBody = await response.text();
|
||||
const parsedPlaylist = parse(responseBody);
|
||||
|
||||
// Extract playlist name from URL, use "Imported from URL" as fallback
|
||||
const extractedName =
|
||||
payload.url && payload.url.length > 1
|
||||
? getFilenameFromUrl(payload.url)
|
||||
: '';
|
||||
const playlistName =
|
||||
!extractedName || extractedName === 'Untitled playlist'
|
||||
? 'Imported from URL'
|
||||
: extractedName;
|
||||
|
||||
const playlist = createPlaylistObject(
|
||||
playlistName,
|
||||
parsedPlaylist,
|
||||
payload.url,
|
||||
'URL'
|
||||
);
|
||||
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: { ...playlist, isTemporary: payload.isTemporary },
|
||||
});
|
||||
} catch (error) {
|
||||
window.postMessage({
|
||||
type: ERROR,
|
||||
message: this.getErrorMessageByStatusCode(error.status),
|
||||
status: error.status,
|
||||
});
|
||||
} */
|
||||
}
|
||||
|
||||
getErrorMessageByStatusCode(status: number) {
|
||||
private async updateM3uPlaylistFromFile(data: {
|
||||
id: string;
|
||||
filePath: string;
|
||||
title: string;
|
||||
}) {
|
||||
window.electron
|
||||
.updatePlaylistFromFilePath(data.filePath, data.title)
|
||||
.then((playlistObject) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_UPDATE_RESPONSE,
|
||||
payload: {
|
||||
message:
|
||||
'Success! The playlist was successfully updated',
|
||||
playlist: {
|
||||
...playlistObject,
|
||||
_id: data.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* private getErrorMessageByStatusCode(status: number) {
|
||||
let message = 'Something went wrong';
|
||||
switch (status) {
|
||||
case 0:
|
||||
@@ -261,9 +235,9 @@ export class TauriService extends DataService {
|
||||
break;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
} */
|
||||
|
||||
async forwardXtreamRequest(payload: {
|
||||
private async forwardXtreamRequest(payload: {
|
||||
url: string;
|
||||
params: Record<string, string>;
|
||||
}) {
|
||||
@@ -347,7 +321,6 @@ export class TauriService extends DataService {
|
||||
);
|
||||
}
|
||||
|
||||
console.log(url, queryParams);
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
import { WhatsNewService } from './whats-new.service';
|
||||
|
||||
describe('Service: WhatsNew', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [WhatsNewService],
|
||||
});
|
||||
});
|
||||
|
||||
it('should ...', inject([WhatsNewService], (service: WhatsNewService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should change the state', inject(
|
||||
[WhatsNewService],
|
||||
(service: WhatsNewService) => {
|
||||
jest.spyOn(service.dialogState$, 'next');
|
||||
service.changeDialogVisibleState(true);
|
||||
expect(service.dialogState$.next).toHaveBeenCalledTimes(1);
|
||||
expect(service.dialogState$.next).toHaveBeenCalledWith(true);
|
||||
service.changeDialogVisibleState(false);
|
||||
expect(service.dialogState$.next).toHaveBeenCalledTimes(2);
|
||||
expect(service.dialogState$.next).toHaveBeenCalledWith(false);
|
||||
}
|
||||
));
|
||||
|
||||
it('should return modal windows for the requested version ', inject(
|
||||
[WhatsNewService],
|
||||
(service: WhatsNewService) => {
|
||||
const version = '0.6.0';
|
||||
const modals = service.getModalsByVersion(version);
|
||||
expect(modals).toBeDefined();
|
||||
expect(modals).toHaveLength(service.modals[version].length);
|
||||
}
|
||||
));
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
export class WhatsNewServiceStub {
|
||||
changeDialogVisibleState(): void {}
|
||||
getModalsByVersion(): any {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
/* import { ModalWindow } from 'ngx-whats-new/lib/modal-window.interface'; */
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class WhatsNewService {
|
||||
/** Visibility state of the modal */
|
||||
dialogState$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/** Array with all available modals */
|
||||
modals = {
|
||||
'0.6.0': [
|
||||
{
|
||||
imageHeight: 470,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/060/dark-theme.png',
|
||||
title: 'New in v0.6.0 - Dark theme 🎉',
|
||||
text: 'Finally, the dark theme is now available. Check the settings page of the application to change the theme.',
|
||||
button: {
|
||||
text: 'OKAY',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
imageHeight: 470,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/060/channel-logos.png',
|
||||
title: 'Channel logos 📺',
|
||||
text: 'The channel list was extended with the visualization of logotypes (only visible if they are defined in the playlist)',
|
||||
button: {
|
||||
text: 'OKAY',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
imageHeight: 470,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/060/custom-user-agent.png',
|
||||
title: 'Custom user agent 🕵️♂️',
|
||||
text: 'Some IPTV providers need a specific user-agent in order to play their playlist. Support of custom user agent is available now.',
|
||||
button: {
|
||||
text: 'GOT IT',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
],
|
||||
'0.7.0': [
|
||||
{
|
||||
imageHeight: 500,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/070/refresh-playlist.png',
|
||||
title: 'New in v0.7.0 - Refresh playlists 🎉',
|
||||
text: "Now you don't have to delete and re-add a playlist if it changes, you can simply update it directly from the user interface.",
|
||||
button: {
|
||||
text: 'Nice!',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
imageHeight: 500,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/070/auto-refresh.png',
|
||||
title: 'Auto-refresh playlists 🎉',
|
||||
text: 'In addition, an auto-refresh playlist function is available so that the playlists will be updated automatically every time the app is started.',
|
||||
button: {
|
||||
text: 'Okay',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
],
|
||||
'0.8.0': [
|
||||
{
|
||||
imageHeight: 310,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/080/translations.png',
|
||||
title: 'New in v0.8.0 - New translations 🎉',
|
||||
html: 'The new release includes the localization of the application into two new languages: Korean and Spanish. Many thanks to the contributors for the translation! (<a target="_blank" href="https://github.com/chaeya">@chaeya</a>, <a target="_blank" href="https://github.com/sguinetti">@sguinetti</a>, <a target="_blank" href="https://github.com/anthonyaxenov">anthonyaxenov</a>)',
|
||||
button: {
|
||||
text: 'Nice!',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'User-Agent Support on Channel Level',
|
||||
html: 'Now user-agent support can be specified not only on the playlist level but also on the level of individual channels. For this purpose, support for Kodi <a target="_blank" href="https://github.com/4gray/iptvnator/issues/57">playlist format</a> was added to the application.',
|
||||
button: {
|
||||
text: 'Next!',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Information about current EPG program',
|
||||
html: 'Similarly to digital receivers, after you switch channels at the bottom of the screen you will see a pop-up window with information about the current program <a target="_blank" href="https://github.com/4gray/iptvnator/issues/51">#51</a>',
|
||||
imageHeight: 250,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/080/channel-info.png',
|
||||
button: {
|
||||
text: 'Next!',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'This and that',
|
||||
html: 'In addition to the already mentioned new features, some bugs have been fixed and the internal dependencies of the application have been updated. Thanks for all the <a href="https://github.com/4gray/iptvnator/issues" target="_blank">ideas</a>, contributions and <a target="_blank" href="https://github.com/sponsors/4gray">first donations.</a>',
|
||||
button: {
|
||||
text: 'YAY!',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
],
|
||||
'0.9.0': [
|
||||
{
|
||||
imageHeight: 310,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/090/reorder-playlists.gif',
|
||||
title: '🎉 v0.9.0 - rearrange playlists',
|
||||
html: 'The new version of the app has an option to <a href="https://github.com/4gray/iptvnator/issues/77" target="_blank">sort the playlists</a> using drag&drop.',
|
||||
button: {
|
||||
text: 'YAY!',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
imageHeight: 310,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/090/subtitle-option.png',
|
||||
title: 'Global subtitle display setting',
|
||||
html: 'The new version has the ability to enable/disable subtitles on a global level for all channels.',
|
||||
button: {
|
||||
text: 'YAY!',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
imageHeight: 310,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/090/languages.png',
|
||||
title: 'Chinese translation',
|
||||
html: 'Thanks to <a target="_blank" href="https://github.com/JoJenH">@JoJenH</a> for the translation.',
|
||||
button: {
|
||||
text: 'YAY!',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'This & That',
|
||||
html: `
|
||||
* improved english translation (thanks to <a target="_blank" href="https://github.com/mbuett">@mbuett</a>)<br />
|
||||
* bugfixes and visual improvements, see <a href="https://github.com/4gray/iptvnator/blob/master/CHANGELOG.md">changelog</a> for more details
|
||||
`,
|
||||
button: {
|
||||
text: 'YAY!',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
],
|
||||
'0.10.0': [
|
||||
{
|
||||
title: "🎉 v0.10.0 - What's new",
|
||||
html: `
|
||||
<h3>Playlist navigation from the sidebar</h3>
|
||||
Switching between playlists is now possible directly from the sidebar and without interrupting channel viewing.
|
||||
|
||||
<h3>Global favorites</h3>
|
||||
Additional playlist has been added, which is generated on the fly and contains favorite channels from all existing playlists.
|
||||
|
||||
<h3>PWA</h3>
|
||||
IPTVnator is now available as a <a href="https://iptvnator.vercel.app/" target="_blank">web-application</a>, which means that you can use it directly from the browser. This means that application can be run on smartphones, tablets, set-top boxes or smart tv browsers.
|
||||
|
||||
<h3>French localization</h3>
|
||||
The app has been translated into another language and is now available in French. Many thanks to <a href="https://github.com/m-p-3" target="_blank">@m-p-3</a> for the translation!
|
||||
`,
|
||||
button: {
|
||||
text: 'Close',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
],
|
||||
'0.11.0': [
|
||||
{
|
||||
imageHeight: 400,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/0110/multiple-epg-sources.png',
|
||||
title: 'Multiple EPG sources',
|
||||
html: 'In the new version of the application, you can add more than one URL as a source of the EPG program.',
|
||||
button: {
|
||||
text: 'NEXT',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
imageHeight: 400,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/0110/multi-epg-view.png',
|
||||
title: '🎉 Multi-EPG view',
|
||||
html: 'The first version of multi-EPG view was developed, which is familiar to many users from set-top boxes. At this stage the view works in purely informative mode.',
|
||||
button: {
|
||||
text: 'NEXT',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
imageHeight: 400,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/0110/import-playlist-as-text.png',
|
||||
title: 'Import playlist from plain text',
|
||||
html: 'Another playlist import option became available - import m3u(8) as text. Just copy the playlist to the clipboard and paste it into the application without having to save it to disk.',
|
||||
button: {
|
||||
text: 'NEXT',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'This&that',
|
||||
html: `<h2>Improvements in PWA</h2>
|
||||
A number of visual changes have been made to improve the experience of using the app on mobile devices.
|
||||
<br />
|
||||
<h2>Internalization</h2> The localization of the project has been improved, but help is still needed with the translation into different languages.
|
||||
<br />
|
||||
<h2>Dependencies updates</h2>
|
||||
The basic libraries used in the application have been updated (angular, electron etc)`,
|
||||
button: {
|
||||
text: 'CLOSE',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
],
|
||||
'0.13.0': [
|
||||
{
|
||||
imageHeight: 400,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/0130/mpv-player.png',
|
||||
title: '🚀 MPV player integration',
|
||||
html: 'Long-awaited mpv player support, which can now be selected from the settings. Before activating the player from the settings page, make sure it is installed on your system. For more details about mvp integration, check the <a target="_blank" href="https://github.com/4gray/iptvnator/wiki/What-is-mpv-video-player-and-how-to-install-it-on-different-operating-systems%3F">wiki page</a>.',
|
||||
button: {
|
||||
text: 'YAY',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
],
|
||||
'0.14.0': [
|
||||
{
|
||||
title: '🚀 New in v0.14.0',
|
||||
html: `The main feature of this release is — Xtream Code IPTV support. The feature is also available in PWA, but works best in electron version of the application and combination with mpv player.<br />
|
||||
|
||||
<h3>Other updates:</h3>
|
||||
* possibility to specify custom path for mpv player<br/>
|
||||
* fixed an annoying bug that appeared when reopening mpv player<br/>
|
||||
* updated libraries used under the hood<br/>
|
||||
* fixed bug related to user-agent handling`,
|
||||
button: {
|
||||
text: 'GO!',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** Options for the "what is new" modal dialogs */
|
||||
options = {
|
||||
width: '500px',
|
||||
customStyle: {
|
||||
boxShadow: '0px 0px 10px 5px #111',
|
||||
backgroundColor: '#333',
|
||||
textColor: '#eee',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes the visibility state of the modal dialog
|
||||
* @param value flag to set
|
||||
*/
|
||||
changeDialogVisibleState(value: boolean): void {
|
||||
this.dialogState$.next(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with modals for the provided version of the application
|
||||
* @param version version string
|
||||
*/
|
||||
getModalsByVersion(version: string) {
|
||||
return this.modals[version] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns modals with latest changes
|
||||
*/
|
||||
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,241 +8,253 @@
|
||||
"
|
||||
/>
|
||||
} @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>
|
||||
@if (isTauri) {
|
||||
<ng-container formArrayName="epgUrl">
|
||||
<form [formGroup]="settingsForm" (ngSubmit)="onSubmit()" novalidate>
|
||||
@if (isDesktop) {
|
||||
<ng-container formArrayName="epgUrl">
|
||||
<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">
|
||||
@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>
|
||||
<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>
|
||||
}
|
||||
<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">
|
||||
@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>
|
||||
<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>
|
||||
<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>
|
||||
</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 />
|
||||
</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 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.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.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.SHOW_CAPTIONS' | translate }}
|
||||
<p>{{ 'SETTINGS.SHOW_CAPTIONS_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column margin-right">
|
||||
<mat-checkbox
|
||||
class="column"
|
||||
formControlName="showCaptions"
|
||||
></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<!-- not ported to tauri yet
|
||||
@if (isPwa || isTauri) {
|
||||
@if (isPwa || isDesktop) {
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
@@ -251,7 +263,7 @@
|
||||
{{ 'SETTINGS.REMOTE_CONTROL_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column" style="margin-right: 10px">
|
||||
<div class="column margin-right">
|
||||
<mat-checkbox
|
||||
class="column"
|
||||
formControlName="remoteControl"
|
||||
@@ -261,125 +273,125 @@
|
||||
}
|
||||
-->
|
||||
@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>
|
||||
<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 margin-right">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
id="remoteControlPort"
|
||||
formControlName="remoteControlPort"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</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>
|
||||
}
|
||||
<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>
|
||||
<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>
|
||||
@@ -32,6 +32,10 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
.margin-right {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 10px;
|
||||
|
||||
|
||||
@@ -25,23 +25,19 @@ import {
|
||||
MockProvider,
|
||||
MockProviders,
|
||||
} from 'ng-mocks';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { Language, StreamFormat, Theme, VideoPlayer } from 'shared-interfaces';
|
||||
import { DataService } from '../../../../../libs/services/src/lib/data.service';
|
||||
import { HeaderComponent } from '../shared/components';
|
||||
import { StreamFormat } from './stream-format.enum';
|
||||
import { Language } from './language.enum';
|
||||
import { SettingsComponent } from './settings.component';
|
||||
import { VideoPlayer } from './settings.interface';
|
||||
import { Theme } from './theme.enum';
|
||||
|
||||
import { signal } from '@angular/core';
|
||||
import { NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { NgxWhatsNewModule } from 'ngx-whats-new';
|
||||
import { of } from 'rxjs';
|
||||
import { SETTINGS_UPDATE } from '../../../shared/ipc-commands';
|
||||
import { DialogService } from '../services/dialog.service';
|
||||
import { SETTINGS_UPDATE } from 'shared-interfaces';
|
||||
import { DialogService } from '../../../../../libs/services/src/lib/dialog.service';
|
||||
import { EpgService } from '../../../../../libs/services/src/lib/epg.service';
|
||||
import { PlaylistsService } from '../../../../../libs/services/src/lib/playlists.service';
|
||||
import { ElectronServiceStub } from '../services/electron.service.stub';
|
||||
import { EpgService } from '../services/epg.service';
|
||||
import { PlaylistsService } from '../services/playlists.service';
|
||||
import { SettingsStore } from '../services/settings-store.service';
|
||||
import { SettingsService } from '../services/settings.service';
|
||||
|
||||
@@ -129,7 +125,6 @@ describe('SettingsComponent', () => {
|
||||
MockModule(MatCheckboxModule),
|
||||
MockModule(MatDividerModule),
|
||||
TranslateModule.forRoot(),
|
||||
NgxWhatsNewModule,
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
@@ -32,27 +32,27 @@ import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { selectIsEpgAvailable } from 'm3u-state';
|
||||
import { take } from 'rxjs';
|
||||
import * as semver from 'semver';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import {
|
||||
Language,
|
||||
Playlist,
|
||||
SET_MPV_PLAYER_PATH,
|
||||
SET_VLC_PLAYER_PATH,
|
||||
SETTINGS_UPDATE,
|
||||
} from '../../shared/ipc-commands';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { DialogService } from '../services/dialog.service';
|
||||
import { EpgService } from '../services/epg.service';
|
||||
import { PlaylistsService } from '../services/playlists.service';
|
||||
StreamFormat,
|
||||
Theme,
|
||||
VideoPlayer,
|
||||
} from 'shared-interfaces';
|
||||
import { DataService } from '../../../../../libs/services/src/lib/data.service';
|
||||
import { DialogService } from '../../../../../libs/services/src/lib/dialog.service';
|
||||
import { EpgService } from '../../../../../libs/services/src/lib/epg.service';
|
||||
import { PlaylistsService } from '../../../../../libs/services/src/lib/playlists.service';
|
||||
import { SettingsStore } from '../services/settings-store.service';
|
||||
import { HeaderComponent } from '../shared/components/header/header.component';
|
||||
import * as PlaylistActions from '../state/actions';
|
||||
import { selectIsEpgAvailable } from '../state/selectors';
|
||||
import { SettingsService } from './../services/settings.service';
|
||||
import { Language } from './language.enum';
|
||||
import { VideoPlayer } from './settings.interface';
|
||||
import { StreamFormat } from './stream-format.enum';
|
||||
import { Theme } from './theme.enum';
|
||||
|
||||
@Component({
|
||||
templateUrl: './settings.component.html',
|
||||
@@ -74,6 +74,18 @@ import { Theme } from './theme.enum';
|
||||
],
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
private dialogService = inject(DialogService);
|
||||
public dataService = inject(DataService);
|
||||
private epgService = inject(EpgService);
|
||||
private formBuilder = inject(FormBuilder);
|
||||
private playlistsService = inject(PlaylistsService);
|
||||
private router = inject(Router);
|
||||
private settingsService = inject(SettingsService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private store = inject(Store);
|
||||
private translate = inject(TranslateService);
|
||||
private matDialog = inject(MatDialog);
|
||||
|
||||
@Input() isDialog = false;
|
||||
/** List with available languages as enum */
|
||||
readonly languageEnum = Language;
|
||||
@@ -82,7 +94,7 @@ export class SettingsComponent implements OnInit {
|
||||
readonly streamFormatEnum = StreamFormat;
|
||||
|
||||
/** Flag that indicates whether the app runs in electron environment */
|
||||
readonly isTauri = this.dataService.getAppEnvironment() === 'tauri';
|
||||
readonly isDesktop = !!window.electron;
|
||||
|
||||
isPwa = this.dataService.getAppEnvironment() === 'pwa';
|
||||
|
||||
@@ -111,7 +123,7 @@ export class SettingsComponent implements OnInit {
|
||||
id: VideoPlayer.ArtPlayer,
|
||||
label: 'ArtPlayer',
|
||||
},
|
||||
...(this.isTauri ? this.osPlayers : []),
|
||||
...(this.isDesktop ? this.osPlayers : []),
|
||||
];
|
||||
|
||||
/** Current version of the app */
|
||||
@@ -129,7 +141,7 @@ export class SettingsComponent implements OnInit {
|
||||
/** Settings form object */
|
||||
settingsForm = this.formBuilder.group({
|
||||
player: [VideoPlayer.VideoJs],
|
||||
...(this.isTauri ? { epgUrl: new FormArray([]) } : {}),
|
||||
...(this.isDesktop ? { epgUrl: new FormArray([]) } : {}),
|
||||
streamFormat: StreamFormat.M3u8StreamFormat,
|
||||
language: Language.ENGLISH,
|
||||
showCaptions: false,
|
||||
@@ -145,22 +157,7 @@ export class SettingsComponent implements OnInit {
|
||||
|
||||
private settingsStore = inject(SettingsStore);
|
||||
|
||||
/**
|
||||
* Creates an instance of SettingsComponent and injects
|
||||
* required dependencies into the component
|
||||
*/
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
public dataService: DataService,
|
||||
private epgService: EpgService,
|
||||
private formBuilder: FormBuilder,
|
||||
private playlistsService: PlaylistsService,
|
||||
private router: Router,
|
||||
private settingsService: SettingsService,
|
||||
private snackBar: MatSnackBar,
|
||||
private store: Store,
|
||||
private translate: TranslateService,
|
||||
private matDialog: MatDialog,
|
||||
@Optional() @Inject(MAT_DIALOG_DATA) data?: { isDialog: boolean }
|
||||
) {
|
||||
this.isDialog = data?.isDialog ?? false;
|
||||
@@ -182,7 +179,7 @@ export class SettingsComponent implements OnInit {
|
||||
const currentSettings = this.settingsStore.getSettings();
|
||||
this.settingsForm.patchValue(currentSettings);
|
||||
|
||||
if (this.isTauri && currentSettings.epgUrl) {
|
||||
if (this.isDesktop && currentSettings.epgUrl) {
|
||||
this.setEpgUrls(currentSettings.epgUrl);
|
||||
}
|
||||
}
|
||||
@@ -282,7 +279,7 @@ export class SettingsComponent implements OnInit {
|
||||
*/
|
||||
applyChangedSettings(): void {
|
||||
this.settingsForm.markAsPristine();
|
||||
if (this.isTauri) {
|
||||
if (this.isDesktop) {
|
||||
let epgUrls = this.settingsForm.value.epgUrl;
|
||||
if (epgUrls) {
|
||||
if (!Array.isArray(epgUrls)) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { Component, inject, Inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
@@ -8,15 +8,15 @@ import {
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { parsePlaylist } from 'm3u-state';
|
||||
import { getFilenameFromUrl } from 'm3u-utils';
|
||||
import { PLAYLIST_PARSE_BY_URL } from '../../../../shared/ipc-commands';
|
||||
import { PLAYLIST_PARSE_BY_URL } from 'shared-interfaces';
|
||||
import { DataService } from '../../../../../../../libs/services/src/lib/data.service';
|
||||
import { FileUploadComponent } from '../../../home/file-upload/file-upload.component';
|
||||
import { StalkerPortalImportComponent } from '../../../home/stalker-portal-import/stalker-portal-import.component';
|
||||
import { TextImportComponent } from '../../../home/text-import/text-import.component';
|
||||
import { UrlUploadComponent } from '../../../home/url-upload/url-upload.component';
|
||||
import { XtreamCodeImportComponent } from '../../../home/xtream-code-import/xtream-code-import.component';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { parsePlaylist } from '../../../state/actions';
|
||||
|
||||
export type PlaylistType = 'xtream' | 'url' | 'text' | 'file' | 'stalker';
|
||||
|
||||
@@ -35,16 +35,15 @@ export type PlaylistType = 'xtream' | 'url' | 'text' | 'file' | 'stalker';
|
||||
templateUrl: './add-playlist-dialog.component.html',
|
||||
})
|
||||
export class AddPlaylistDialogComponent {
|
||||
playlistType!: PlaylistType;
|
||||
private dataService = inject(DataService);
|
||||
private dialogRef = inject(MatDialogRef<AddPlaylistDialogComponent>);
|
||||
private store = inject(Store);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private translateService = inject(TranslateService);
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) data: { type: PlaylistType },
|
||||
private dataService: DataService,
|
||||
private dialogRef: MatDialogRef<AddPlaylistDialogComponent>,
|
||||
private store: Store,
|
||||
private snackBar: MatSnackBar,
|
||||
private translateService: TranslateService
|
||||
) {
|
||||
readonly playlistType!: PlaylistType;
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) data: { type: PlaylistType }) {
|
||||
this.playlistType = data.type;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<mat-toolbar>
|
||||
<div class="logo">
|
||||
<img src="./assets/icons/icon-tv-256.png" height="100" />
|
||||
<img
|
||||
src="./assets/icons/icon-tv-256.png"
|
||||
height="100"
|
||||
alt="iptvnator logo"
|
||||
/>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="title">{{ title }}</div>
|
||||
<div class="subtitle">{{ subtitle }}</div>
|
||||
<div class="title">{{ title() }}</div>
|
||||
<div class="subtitle">{{ subtitle() }}</div>
|
||||
</div>
|
||||
@if (isHome) {
|
||||
<div class="menu">
|
||||
@@ -34,7 +38,7 @@
|
||||
>
|
||||
<mat-icon>filter_alt</mat-icon>
|
||||
</button>
|
||||
@if (isElectron || isTauri) {
|
||||
@if (isDesktop) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="openSettings()"
|
||||
@@ -83,15 +87,7 @@
|
||||
<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>
|
||||
<mat-divider />
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'MENU.SUPPORT_PROJECT' | translate"
|
||||
@@ -235,11 +231,3 @@
|
||||
</div>
|
||||
}
|
||||
</mat-toolbar>
|
||||
|
||||
@if (!isElectron && isDialogVisible$ | async) {
|
||||
<ngx-whats-new
|
||||
(closeModal)="setDialogVisibility(false)"
|
||||
[items]="modals"
|
||||
[options]="options"
|
||||
/>
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component, Input, OnInit, effect } from '@angular/core';
|
||||
import { Component, OnInit, effect, inject, input } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
@@ -13,13 +12,14 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { NgxWhatsNewModule } from 'ngx-whats-new';
|
||||
import { selectActiveTypeFilters, setSelectedFilters } from 'm3u-state';
|
||||
import { DataService } from '../../../../../../../libs/services/src/lib/data.service';
|
||||
import {
|
||||
SortBy,
|
||||
SortOrder,
|
||||
SortService,
|
||||
} from '../../../../../../../libs/services/src/lib/sort.service';
|
||||
import { HomeComponent } from '../../../home/home.component';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { SortBy, SortOrder, SortService } from '../../../services/sort.service';
|
||||
import { WhatsNewService } from '../../../services/whats-new.service';
|
||||
import { setSelectedFilters } from '../../../state/actions';
|
||||
import { selectActiveTypeFilters } from '../../../state/selectors';
|
||||
import { AboutDialogComponent } from '../about-dialog/about-dialog.component';
|
||||
import {
|
||||
AddPlaylistDialogComponent,
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss'],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatDividerModule,
|
||||
@@ -39,33 +39,21 @@ import {
|
||||
MatMenuModule,
|
||||
MatToolbarModule,
|
||||
MatTooltipModule,
|
||||
FormsModule,
|
||||
NgxWhatsNewModule,
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class HeaderComponent implements OnInit {
|
||||
/** Title of the header */
|
||||
@Input() title!: string;
|
||||
private activatedRoute = inject(ActivatedRoute);
|
||||
private dialog = inject(MatDialog);
|
||||
private dataService = inject(DataService);
|
||||
private router = inject(Router);
|
||||
private store = inject(Store);
|
||||
private sortService = inject(SortService);
|
||||
|
||||
/** Subtitle of the header */
|
||||
@Input() subtitle!: string;
|
||||
|
||||
/** Environment flag */
|
||||
isElectron = this.dataService.isElectron;
|
||||
|
||||
/** Environment flag for Tauri */
|
||||
isTauri = this.dataService.getAppEnvironment() === 'tauri';
|
||||
|
||||
/** Visibility flag of the "what is new" modal dialog */
|
||||
isDialogVisible$ = this.whatsNewService.dialogState$;
|
||||
|
||||
/** Dialog options */
|
||||
options = this.whatsNewService.options;
|
||||
|
||||
/** Modals to show for the updated version of the application */
|
||||
modals = this.whatsNewService.getLatestChanges();
|
||||
readonly isDesktop = !!window.electron;
|
||||
readonly title = input.required<string>();
|
||||
readonly subtitle = input.required<string>();
|
||||
|
||||
isHome = true;
|
||||
|
||||
@@ -87,21 +75,15 @@ export class HeaderComponent implements OnInit {
|
||||
},
|
||||
];
|
||||
|
||||
selectedTypeFilters = this.store.selectSignal(selectActiveTypeFilters);
|
||||
private readonly selectedTypeFilters = this.store.selectSignal(
|
||||
selectActiveTypeFilters
|
||||
);
|
||||
|
||||
SortBy = SortBy;
|
||||
SortOrder = SortOrder;
|
||||
currentSortOptions: { by: SortBy; order: SortOrder };
|
||||
readonly SortBy = SortBy;
|
||||
readonly SortOrder = SortOrder;
|
||||
private currentSortOptions: { by: SortBy; order: SortOrder };
|
||||
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private dialog: MatDialog,
|
||||
private dataService: DataService,
|
||||
private router: Router,
|
||||
private store: Store,
|
||||
private whatsNewService: WhatsNewService,
|
||||
private sortService: SortService
|
||||
) {
|
||||
constructor() {
|
||||
effect(() => {
|
||||
if (this.selectedTypeFilters) {
|
||||
this.playlistTypes = this.playlistTypes.map((type) => {
|
||||
@@ -133,23 +115,13 @@ export class HeaderComponent implements OnInit {
|
||||
* @param url url to open
|
||||
*/
|
||||
async openUrl(url: string): Promise<void> {
|
||||
if (this.isTauri) {
|
||||
if (this.isDesktop) {
|
||||
await open(url);
|
||||
} else {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility flag of the modal window
|
||||
* @param visible show/hide window flag
|
||||
*/
|
||||
setDialogVisibility(visible: boolean): void {
|
||||
if (this.modals.length > 0) {
|
||||
this.whatsNewService.changeDialogVisibleState(visible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the about dialog with description and version of
|
||||
* the app
|
||||
|
||||
@@ -2,9 +2,10 @@ import { AsyncPipe } from '@angular/common';
|
||||
import { Component, inject, input, output } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { PlaylistsService } from '@iptvnator/services';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, map, switchMap } from 'rxjs';
|
||||
import { PlaylistsService } from '../../services/playlists.service';
|
||||
import { StalkerStore } from '../stalker.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-favorites-button',
|
||||
@@ -13,6 +14,7 @@ import { PlaylistsService } from '../../services/playlists.service';
|
||||
})
|
||||
export class FavoritesButtonComponent {
|
||||
private playlistService = inject(PlaylistsService);
|
||||
private stalkerStore = inject(StalkerStore);
|
||||
|
||||
readonly itemId = input.required<string>();
|
||||
|
||||
@@ -22,7 +24,11 @@ export class FavoritesButtonComponent {
|
||||
private readonly favoritesChanged$ = new BehaviorSubject<void>(undefined);
|
||||
|
||||
readonly isFavorite$ = this.favoritesChanged$.pipe(
|
||||
switchMap(() => this.playlistService.getPortalFavorites()),
|
||||
switchMap(() =>
|
||||
this.playlistService.getPortalFavorites(
|
||||
this.stalkerStore.currentPlaylist()?._id
|
||||
)
|
||||
),
|
||||
map((favorites) =>
|
||||
favorites.some(
|
||||
(i) =>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { rxResource } from '@angular/core/rxjs-interop';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { PlaylistsService } from '../../services/playlists.service';
|
||||
import { PlaylistsService } from '../../../../../../libs/services/src/lib/playlists.service';
|
||||
import { FavoritesLayoutComponent } from '../../shared/components/favorites-layout/favorites-layout.component';
|
||||
import { VodDetailsComponent } from '../../xtream/vod-details/vod-details.component';
|
||||
import { StalkerSeriesViewComponent } from '../stalker-series-view/stalker-series-view.component';
|
||||
@@ -47,7 +47,10 @@ export class RecentlyViewedComponent {
|
||||
params: () => ({
|
||||
refreshTimestamp: this.refreshTimestamp(),
|
||||
}),
|
||||
stream: () => this.playlistService.getPortalRecentlyViewed(),
|
||||
stream: () =>
|
||||
this.playlistService.getPortalRecentlyViewed(
|
||||
this.stalkerStore.currentPlaylist()?._id
|
||||
),
|
||||
});
|
||||
|
||||
readonly categories = computed(() => [
|
||||
|
||||
@@ -3,7 +3,7 @@ import { rxResource } from '@angular/core/rxjs-interop';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { PlaylistsService } from '../../services/playlists.service';
|
||||
import { PlaylistsService } from '../../../../../../libs/services/src/lib/playlists.service';
|
||||
import { FavoritesLayoutComponent } from '../../shared/components/favorites-layout/favorites-layout.component';
|
||||
import { VodDetailsComponent } from '../../xtream/vod-details/vod-details.component';
|
||||
import { StalkerSeriesViewComponent } from '../stalker-series-view/stalker-series-view.component';
|
||||
@@ -47,7 +47,10 @@ export class StalkerFavoritesComponent {
|
||||
params: () => ({
|
||||
refreshTimestamp: this.refreshTimestamp(),
|
||||
}),
|
||||
stream: () => this.playlistService.getPortalFavorites(),
|
||||
stream: () =>
|
||||
this.playlistService.getPortalFavorites(
|
||||
this.stalkerStore.currentPlaylist()?._id
|
||||
),
|
||||
});
|
||||
|
||||
readonly categories = computed(() => [
|
||||
|
||||
@@ -7,7 +7,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { PlaylistsService } from '../services/playlists.service';
|
||||
import { PlaylistsService } from '../../../../../libs/services/src/lib/playlists.service';
|
||||
import { MpvPlayerBarComponent } from '../shared/components/mpv-player-bar/mpv-player-bar.component';
|
||||
import { CategoryViewComponent } from '../xtream-tauri/category-view/category-view.component';
|
||||
import { PlaylistErrorViewComponent } from '../xtream/playlist-error-view/playlist-error-view.component';
|
||||
@@ -164,11 +164,13 @@ export class StalkerMainContainerComponent {
|
||||
}
|
||||
});
|
||||
|
||||
this.playlistService.getPortalFavorites().subscribe((favorites) => {
|
||||
favorites.forEach((fav: any) => {
|
||||
this.favorites.set(fav.id, true);
|
||||
this.playlistService
|
||||
.getPortalFavorites(this.stalkerStore.currentPlaylist()?._id)
|
||||
.subscribe((favorites) => {
|
||||
favorites.forEach((fav: any) => {
|
||||
this.favorites.set(fav.id, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handshake() {
|
||||
|
||||
@@ -6,12 +6,12 @@ import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { STALKER_REQUEST } from '../../../shared/ipc-commands';
|
||||
import { selectPlaylistById } from 'm3u-state';
|
||||
import { STALKER_REQUEST } from 'shared-interfaces';
|
||||
import { DataService } from '../../../../../../libs/services/src/lib/data.service';
|
||||
import { StalkerPortalActions } from '../../../shared/stalker-portal-actions.enum';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { SearchFormComponent } from '../../shared/components/search-form/search-form.component';
|
||||
import { SearchResultItemComponent } from '../../shared/components/search-result-item/search-result-item.component';
|
||||
import { selectPlaylistById } from '../../state/selectors';
|
||||
import { PlaylistErrorViewComponent } from '../../xtream-tauri/playlist-error-view/playlist-error-view.component';
|
||||
import { VodDetailsComponent } from '../../xtream/vod-details/vod-details.component';
|
||||
import { StalkerContentTypes } from '../stalker-content-types';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, effect, inject, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import * as PlaylistActions from '../../state/actions';
|
||||
import { selectPlaylistById } from '../../state/selectors';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { selectPlaylistById } from 'm3u-state';
|
||||
import { NavigationComponent } from '../../xtream-tauri/navigation/navigation.component';
|
||||
import { NavigationItem } from '../../xtream-tauri/navigation/navigation.interface';
|
||||
import { StalkerStore } from '../stalker.store';
|
||||
|
||||
@@ -9,12 +9,11 @@ import {
|
||||
withState,
|
||||
} from '@ngrx/signals';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { STALKER_REQUEST } from '../../shared/ipc-commands';
|
||||
import { PlaylistMeta, STALKER_REQUEST } from 'shared-interfaces';
|
||||
import { DataService } from '../../../../../libs/services/src/lib/data.service';
|
||||
import { PlaylistsService } from '../../../../../libs/services/src/lib/playlists.service';
|
||||
import { StalkerPortalActions } from '../../shared/stalker-portal-actions.enum';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { PlayerService } from '../services/player.service';
|
||||
import { PlaylistsService } from '../services/playlists.service';
|
||||
import { PlaylistMeta } from '../shared/playlist-meta.type';
|
||||
import { ContentType } from '../xtream/content-type.enum';
|
||||
import { StalkerSeason } from './models/stalker-season.interface';
|
||||
import { StalkerContentTypes } from './stalker-content-types';
|
||||
@@ -483,7 +482,7 @@ export const StalkerStore = signalStore(
|
||||
addToRecentlyViewed(item: any) {
|
||||
console.log('Adding to recently viewed', item);
|
||||
playlistService
|
||||
.addPortalRecentlyViewed({
|
||||
.addPortalRecentlyViewed(this.currentPlaylist()?._id, {
|
||||
...item,
|
||||
category_id: store.selectedContentType(),
|
||||
added_at: Date.now(),
|
||||
@@ -492,7 +491,10 @@ export const StalkerStore = signalStore(
|
||||
},
|
||||
removeFromRecentlyViewed(itemId: number) {
|
||||
playlistService
|
||||
.removeFromPortalRecentlyViewed(itemId)
|
||||
.removeFromPortalRecentlyViewed(
|
||||
this.currentPlaylist()?._id,
|
||||
itemId
|
||||
)
|
||||
.subscribe();
|
||||
},
|
||||
})
|
||||
|
||||
@@ -4,8 +4,8 @@ import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { selectActivePlaylist } from '../../state/selectors';
|
||||
import { selectActivePlaylist } from 'm3u-state';
|
||||
import { DataService } from '../../../../../../libs/services/src/lib/data.service';
|
||||
import { XtreamAccountInfo } from './account-info.interface';
|
||||
|
||||
@Component({
|
||||
@@ -159,7 +159,6 @@ export class AccountInfoComponent {
|
||||
|
||||
async setAccountInfo() {
|
||||
const playlist = this.currentPlaylist();
|
||||
console.log(playlist);
|
||||
if (!playlist) return;
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,10 +2,10 @@ import { Component, OnInit, inject, signal } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { selectActivePlaylist } from 'm3u-state';
|
||||
import { BehaviorSubject, switchMap } from 'rxjs';
|
||||
import { XtreamCategory } from '../../../shared/xtream-category.interface';
|
||||
import { FavoritesLayoutComponent } from '../../shared/components/favorites-layout/favorites-layout.component';
|
||||
import { selectActivePlaylist } from '../../state/selectors';
|
||||
import { FavoriteItem } from '../services/favorite-item.interface';
|
||||
import { FavoritesService } from '../services/favorites.service';
|
||||
import { XtreamStore } from '../xtream.store';
|
||||
|
||||
@@ -13,8 +13,7 @@ import { MatListModule } from '@angular/material/list';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { XtreamCategory } from '../../../shared/xtream-category.interface';
|
||||
import { EpgViewComponent } from '../../portals/epg-view/epg-view.component';
|
||||
import { WebPlayerViewComponent } from '../../portals/web-player-view/web-player-view.component';
|
||||
import { EpgViewComponent, WebPlayerViewComponent } from 'shared-portals';
|
||||
import { SettingsStore } from '../../services/settings-store.service';
|
||||
import { MpvPlayerBarComponent } from '../../shared/components/mpv-player-bar/mpv-player-bar.component';
|
||||
import { CategoryViewComponent } from '../category-view/category-view.component';
|
||||
|
||||
@@ -7,9 +7,10 @@ import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { ActivatedRoute, RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { PlaylistInfoComponent } from '../../home/recent-playlists/playlist-info/playlist-info.component';
|
||||
import { PlaylistInfoComponent } from 'components';
|
||||
import { selectPlaylistById } from 'm3u-state';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { SettingsComponent } from '../../settings/settings.component';
|
||||
import { selectPlaylistById } from '../../state/selectors';
|
||||
import { AccountInfoComponent } from '../account-info/account-info.component';
|
||||
import { XtreamStore } from '../xtream.store';
|
||||
import { NavigationItem } from './navigation.interface';
|
||||
@@ -45,7 +46,7 @@ export class NavigationComponent {
|
||||
);
|
||||
|
||||
readonly isStalkerPlaylist = computed(
|
||||
() => this.currentPlaylist().macAddress
|
||||
() => (this.currentPlaylist() as Playlist).macAddress
|
||||
);
|
||||
|
||||
readonly navigationItems = input<NavigationItem[]>();
|
||||
|
||||
@@ -7,7 +7,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { WebPlayerViewComponent } from '../../portals/web-player-view/web-player-view.component';
|
||||
import { WebPlayerViewComponent } from 'shared-portals';
|
||||
|
||||
export interface PlayerDialogData {
|
||||
streamUrl: string;
|
||||
|
||||
@@ -3,19 +3,19 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { DatabaseService, DialogService } from '@iptvnator/services';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { PlaylistInfoComponent } from '../../home/recent-playlists/playlist-info/playlist-info.component';
|
||||
import { DatabaseService } from '../../services/database.service';
|
||||
import { DialogService } from '../../services/dialog.service';
|
||||
import * as PlaylistActions from '../../state/actions';
|
||||
import { selectCurrentPlaylist } from '../../state/selectors';
|
||||
import { PlaylistInfoComponent } from 'components';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { selectCurrentPlaylist } from 'm3u-state';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-playlist-error-view',
|
||||
templateUrl: './playlist-error-view.component.html',
|
||||
styleUrls: ['./playlist-error-view.component.scss'],
|
||||
imports: [MatButtonModule, MatIconModule, RouterLink, TranslateModule]
|
||||
imports: [MatButtonModule, MatIconModule, RouterLink, TranslateModule],
|
||||
})
|
||||
export class PlaylistErrorViewComponent {
|
||||
private databaseService = inject(DatabaseService);
|
||||
@@ -46,7 +46,7 @@ export class PlaylistErrorViewComponent {
|
||||
'HOME.PLAYLISTS.REMOVE_DIALOG.MESSAGE'
|
||||
),
|
||||
onConfirm: (): void =>
|
||||
this.removePlaylist(this.currentPlaylist()._id),
|
||||
this.removePlaylist((this.currentPlaylist() as Playlist)._id),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,10 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { FilterPipe } from '@iptvnator/pipes';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { XtreamItem } from '../../../../../../libs/shared/interfaces/src/lib/xtream-item.interface';
|
||||
import { XtreamCategory } from '../../../shared/xtream-category.interface';
|
||||
import { XtreamItem } from '../../../shared/xtream-item.interface';
|
||||
import { FilterPipe } from '../../shared/pipes/filter.pipe';
|
||||
import { FavoritesService } from '../services/favorites.service';
|
||||
import { XtreamStore } from '../xtream.store';
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { XtreamItem } from '../../../shared/xtream-item.interface';
|
||||
import { DatabaseService } from '../../services/database.service';
|
||||
import { DatabaseService } from '../../../../../../libs/services/src/lib/database.service';
|
||||
import { XtreamItem } from '../../../../../../libs/shared/interfaces/src/lib/xtream-item.interface';
|
||||
import { SearchFormComponent } from '../../shared/components/search-form/search-form.component';
|
||||
import { SearchResultItemComponent } from '../../shared/components/search-result-item/search-result-item.component';
|
||||
import { XtreamStore } from '../xtream.store';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { from, map, mergeMap, Observable } from 'rxjs';
|
||||
import { DatabaseService } from '../../services/database.service';
|
||||
import { DatabaseService } from '../../../../../../libs/services/src/lib/database.service';
|
||||
import { FavoriteItem } from './favorite-item.interface';
|
||||
|
||||
@Injectable({
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
withMethods,
|
||||
withState,
|
||||
} from '@ngrx/signals';
|
||||
import { DatabaseService } from '../services/database.service';
|
||||
import { DatabaseService } from '../../../../../libs/services/src/lib/database.service';
|
||||
import { FavoritesService } from './services/favorites.service';
|
||||
|
||||
export const withFavorites = function () {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@ngrx/signals';
|
||||
import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { pipe, switchMap, tap } from 'rxjs';
|
||||
import { DatabaseService } from '../services/database.service';
|
||||
import { DatabaseService } from '../../../../../libs/services/src/lib/database.service';
|
||||
|
||||
export interface RecentlyViewedItem {
|
||||
id: number;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, effect, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import * as PlaylistActions from '../../state/actions';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { LoadingOverlayComponent } from '../loading-overlay/loading-overlay.component';
|
||||
import { NavigationComponent } from '../navigation/navigation.component';
|
||||
import { NavigationItem } from '../navigation/navigation.interface';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { GlobalSearchResult } from '../../../../../libs/services/src/lib/database.service';
|
||||
import { EpgItem } from '../../../../../libs/shared/interfaces/src/lib/epg-item.interface';
|
||||
import { XtreamSerieItem } from '../../../../../libs/shared/interfaces/src/lib/xtream-serie-item.interface';
|
||||
import { XtreamCategory } from '../../shared/xtream-category.interface';
|
||||
import { XtreamLiveStream } from '../../shared/xtream-live-stream.interface';
|
||||
import { XtreamSerieItem } from '../../shared/xtream-serie-item.interface';
|
||||
import { XtreamVodStream } from '../../shared/xtream-vod-stream.interface';
|
||||
import { GlobalSearchResult } from '../services/database.service';
|
||||
import { EpgItem } from '../xtream/epg-item.interface';
|
||||
|
||||
export type ContentType = 'live' | 'vod' | 'series';
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@ngrx/signals';
|
||||
import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { selectActivePlaylist } from 'm3u-state';
|
||||
import {
|
||||
catchError,
|
||||
combineLatestWith,
|
||||
@@ -19,17 +20,17 @@ import {
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { DataService } from '../../../../../libs/services/src/lib/data.service';
|
||||
import { DatabaseService } from '../../../../../libs/services/src/lib/database.service';
|
||||
import { XtreamCodeActions } from '../../shared/xtream-code-actions';
|
||||
import {
|
||||
XtreamSerieDetails,
|
||||
XtreamSerieEpisode,
|
||||
} from '../../shared/xtream-serie-details.interface';
|
||||
import { XtreamVodDetails } from '../../shared/xtream-vod-details.interface';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { DatabaseService } from '../services/database.service';
|
||||
import { PlayerService } from '../services/player.service';
|
||||
import { SettingsStore } from '../services/settings-store.service';
|
||||
import { selectActivePlaylist } from '../state/selectors';
|
||||
import { XtreamAccountInfo } from './account-info/account-info.interface';
|
||||
import { withFavorites } from './with-favorites.feature';
|
||||
import { withRecentItems } from './with-recent-items';
|
||||
@@ -361,10 +362,9 @@ export const XtreamStore = signalStore(
|
||||
currentPlaylist: playlist,
|
||||
});
|
||||
} else {
|
||||
const playlist =
|
||||
oldStore.selectSignal(
|
||||
selectActivePlaylist
|
||||
)();
|
||||
const playlist = oldStore.selectSignal(
|
||||
selectActivePlaylist
|
||||
)() as Playlist;
|
||||
if (playlist) {
|
||||
await dbService.createPlaylist(playlist);
|
||||
patchState(store, {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
$event.target.src =
|
||||
'./assets/images/default-poster.png'
|
||||
"
|
||||
alt="channel logo"
|
||||
/>
|
||||
}
|
||||
@if (!item.stream_icon && !item.cover && !item.logo) {
|
||||
|
||||
@@ -3,10 +3,9 @@ import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { FilterPipe, SortPipe } from '@iptvnator/pipes';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { XtreamItem } from '../../../shared/xtream-item.interface';
|
||||
import { FilterPipe } from '../../shared/pipes/filter.pipe';
|
||||
import { SortPipe } from '../../shared/pipes/sort.pipe';
|
||||
import { XtreamItem } from '../../../../../../libs/shared/interfaces/src/lib/xtream-item.interface';
|
||||
import { PlaylistErrorViewComponent } from '../playlist-error-view/playlist-error-view.component';
|
||||
import { PortalStore } from '../portal.store';
|
||||
|
||||
@@ -23,7 +22,7 @@ import { PortalStore } from '../portal.store';
|
||||
NgOptimizedImage,
|
||||
PlaylistErrorViewComponent,
|
||||
TranslateModule,
|
||||
]
|
||||
],
|
||||
})
|
||||
export class CategoryContentViewComponent {
|
||||
@Input({ required: true }) items: XtreamItem[];
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||
import { Component, inject, input, output } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { FilterPipe } from '@iptvnator/pipes';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { XtreamCategory } from '../../../shared/xtream-category.interface';
|
||||
import { FilterPipe } from '../../shared/pipes/filter.pipe';
|
||||
import { PlaylistErrorViewComponent } from '../playlist-error-view/playlist-error-view.component';
|
||||
import { PortalStore } from '../portal.store';
|
||||
|
||||
@@ -12,7 +12,7 @@ import { PortalStore } from '../portal.store';
|
||||
template: `
|
||||
@if (items?.length > 0) {
|
||||
@for (
|
||||
item of items | filterBy: searchPhrase() : 'category_name';
|
||||
item of items() | filterBy: searchPhrase() : 'category_name';
|
||||
track $index
|
||||
) {
|
||||
<mat-card
|
||||
@@ -30,7 +30,7 @@ import { PortalStore } from '../portal.store';
|
||||
</mat-card>
|
||||
}
|
||||
@if (
|
||||
!(items | filterBy: searchPhrase() : 'category_name')?.length
|
||||
!(items() | filterBy: searchPhrase() : 'category_name')?.length
|
||||
) {
|
||||
<app-playlist-error-view
|
||||
title="No results"
|
||||
@@ -59,13 +59,13 @@ import { PortalStore } from '../portal.store';
|
||||
MatIconModule,
|
||||
PlaylistErrorViewComponent,
|
||||
TranslateModule,
|
||||
]
|
||||
],
|
||||
})
|
||||
export class CategoryViewComponent {
|
||||
@Input({ required: true }) items: XtreamCategory[];
|
||||
readonly items = input.required<XtreamCategory[]>();
|
||||
|
||||
@Output() categoryClicked = new EventEmitter<XtreamCategory>();
|
||||
readonly categoryClicked = output<XtreamCategory>();
|
||||
|
||||
portalStore = inject(PortalStore);
|
||||
searchPhrase = this.portalStore.searchPhrase;
|
||||
private readonly portalStore = inject(PortalStore);
|
||||
readonly searchPhrase = this.portalStore.searchPhrase;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -21,6 +20,8 @@ import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { PlaylistInfoComponent } from 'components';
|
||||
import { selectCurrentPlaylist } from 'm3u-state';
|
||||
import {
|
||||
Observable,
|
||||
Subject,
|
||||
@@ -29,9 +30,7 @@ import {
|
||||
map,
|
||||
} from 'rxjs';
|
||||
import { XtreamLiveStream } from '../../../shared/xtream-live-stream.interface';
|
||||
import { PlaylistInfoComponent } from '../../home/recent-playlists/playlist-info/playlist-info.component';
|
||||
import { SettingsComponent } from '../../settings/settings.component';
|
||||
import { selectCurrentPlaylist } from '../../state/selectors';
|
||||
|
||||
import { SettingsStore } from '../../services/settings-store.service';
|
||||
import { Breadcrumb } from '../breadcrumb.interface';
|
||||
@@ -44,18 +43,18 @@ import { PortalStore } from '../portal.store';
|
||||
templateUrl: './navigation-bar.component.html',
|
||||
styleUrls: ['./navigation-bar.component.scss'],
|
||||
imports: [
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
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,5 +1,5 @@
|
||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||
import { Component, Inject, ViewEncapsulation } from '@angular/core';
|
||||
import { Component, inject, ViewEncapsulation } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -7,7 +7,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { WebPlayerViewComponent } from '../../portals/web-player-view/web-player-view.component';
|
||||
import { WebPlayerViewComponent } from 'shared-portals';
|
||||
|
||||
export interface PlayerDialogData {
|
||||
streamUrl: string;
|
||||
@@ -30,16 +30,16 @@ export interface PlayerDialogData {
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class PlayerDialogComponent {
|
||||
title: string;
|
||||
streamUrl: string;
|
||||
readonly data = inject(MAT_DIALOG_DATA) as PlayerDialogData;
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private translateService = inject(TranslateService);
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) data: PlayerDialogData,
|
||||
private snackBar: MatSnackBar,
|
||||
private translateService: TranslateService
|
||||
) {
|
||||
this.streamUrl = data.streamUrl;
|
||||
this.title = data.title;
|
||||
readonly title: string;
|
||||
readonly streamUrl: string;
|
||||
|
||||
constructor() {
|
||||
this.streamUrl = this.data.streamUrl;
|
||||
this.title = this.data.title;
|
||||
}
|
||||
|
||||
showCopyNotification() {
|
||||
|
||||
@@ -3,18 +3,19 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { DialogService } from '@iptvnator/services';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { PlaylistInfoComponent } from '../../home/recent-playlists/playlist-info/playlist-info.component';
|
||||
import { DialogService } from '../../services/dialog.service';
|
||||
import * as PlaylistActions from '../../state/actions';
|
||||
import { selectCurrentPlaylist } from '../../state/selectors';
|
||||
import { PlaylistInfoComponent } from 'components';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { selectCurrentPlaylist } from 'm3u-state';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-playlist-error-view',
|
||||
templateUrl: './playlist-error-view.component.html',
|
||||
styleUrls: ['./playlist-error-view.component.scss'],
|
||||
imports: [MatButtonModule, MatIconModule, RouterLink, TranslateModule]
|
||||
imports: [MatButtonModule, MatIconModule, RouterLink, TranslateModule],
|
||||
})
|
||||
export class PlaylistErrorViewComponent {
|
||||
dialog = inject(MatDialog);
|
||||
@@ -44,7 +45,7 @@ export class PlaylistErrorViewComponent {
|
||||
'HOME.PLAYLISTS.REMOVE_DIALOG.MESSAGE'
|
||||
),
|
||||
onConfirm: (): void =>
|
||||
this.removePlaylist(this.currentPlaylist()._id),
|
||||
this.removePlaylist((this.currentPlaylist() as Playlist)._id),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
|
||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { selectCurrentPlaylist } from 'm3u-state';
|
||||
import { PlaylistsService } from '../../../../../../libs/services/src/lib/playlists.service';
|
||||
import {
|
||||
XtreamSerieDetails,
|
||||
XtreamSerieEpisode,
|
||||
} from '../../../shared/xtream-serie-details.interface';
|
||||
import { PlaylistsService } from '../../services/playlists.service';
|
||||
import { selectCurrentPlaylist } from '../../state/selectors';
|
||||
import { SeasonContainerComponent } from '../season-container/season-container.component';
|
||||
|
||||
@Component({
|
||||
@@ -18,11 +17,11 @@ import { SeasonContainerComponent } from '../season-container/season-container.c
|
||||
templateUrl: './serial-details.component.html',
|
||||
styleUrls: ['../detail-view.scss'],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
SeasonContainerComponent,
|
||||
TranslateModule
|
||||
],
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
SeasonContainerComponent,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class SerialDetailsComponent {
|
||||
@Input({ required: true }) item: XtreamSerieDetails;
|
||||
|
||||
@@ -11,8 +11,8 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { PlaylistsService } from '../../../../../../libs/services/src/lib/playlists.service';
|
||||
import { XtreamVodDetails } from '../../../shared/xtream-vod-details.interface';
|
||||
import { PlaylistsService } from '../../services/playlists.service';
|
||||
import { SafePipe } from './safe.pipe';
|
||||
|
||||
@Component({
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 19 KiB |
@@ -13,6 +13,11 @@ declare global {
|
||||
getAppVersion: () => Promise<string>;
|
||||
platform: string;
|
||||
fetchPlaylistByUrl: (url: string) => Promise<Playlist>;
|
||||
updatePlaylistFromFilePath: (
|
||||
filePath: string,
|
||||
title: string
|
||||
) => Promise<Playlist>;
|
||||
openPlaylistFromFile: () => Promise<Playlist>;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>IPTVnator</title>
|
||||
<base href="./" />
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { XtreamItem } from './xtream-item.interface';
|
||||
import { XtreamItem } from '../../../../libs/shared/interfaces/src/lib/xtream-item.interface';
|
||||
|
||||
export interface XtreamLiveStream extends XtreamItem {
|
||||
stream_type: 'live';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { XtreamItem } from './xtream-item.interface';
|
||||
import { XtreamItem } from '../../../../libs/shared/interfaces/src/lib/xtream-item.interface';
|
||||
|
||||
export interface XtreamVodStream extends XtreamItem {
|
||||
stream_type: 'movie';
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"],
|
||||
"moduleResolution": "node10"
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"],
|
||||
"moduleResolution": "node10"
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts",
|
||||
"../../libs/services/src/lib/epg.service.spec.ts",
|
||||
"../../libs/services/src/lib/dialog.service.spec.ts",
|
||||
"../../libs/ui/pipes/src/lib/moment-date.pipe.spec.ts"
|
||||
]
|
||||
}
|
||||
@@ -37,6 +37,9 @@ export default [
|
||||
'**/*.mjs',
|
||||
],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
rules: {
|
||||
'@angular-eslint/template/click-events-have-key-events': 'off',
|
||||
'@angular-eslint/template/interactive-supports-focus': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,7 @@
|
||||
# m3u-state
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test m3u-state` to execute the unit tests.
|
||||
@@ -0,0 +1,34 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../eslint.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
displayName: 'm3u-state',
|
||||
preset: '../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../coverage/libs/m3u-state',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "m3u-state",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/m3u-state/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/m3u-state/jest.config.ts",
|
||||
"tsConfig": "libs/m3u-state/tsconfig.spec.json"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './lib/actions';
|
||||
export * from './lib/effects';
|
||||
export * from './lib/reducers';
|
||||
export * from './lib/router.selectors';
|
||||
export * from './lib/selectors';
|
||||
@@ -1,7 +1,5 @@
|
||||
import { createAction, props } from '@ngrx/store';
|
||||
import { Channel, Playlist } from 'shared-interfaces';
|
||||
import { EpgProgram } from '../player/models/epg-program.model';
|
||||
import { PlaylistMeta } from '../shared/playlist-meta.type';
|
||||
import { Channel, EpgProgram, Playlist, PlaylistMeta } from 'shared-interfaces';
|
||||
|
||||
const STORE_KEY = '[GLOBAL STORE]';
|
||||
const PLAYLISTS_STORE_KEY = '[PLAYLISTS STORE]';
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { DataService, PlaylistsService } from '@iptvnator/services';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import Database from '@tauri-apps/plugin-sql';
|
||||
import {
|
||||
combineLatestWith,
|
||||
@@ -15,17 +15,15 @@ import {
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import {
|
||||
CHANNEL_SET_USER_AGENT,
|
||||
EPG_GET_PROGRAM,
|
||||
OPEN_MPV_PLAYER,
|
||||
OPEN_VLC_PLAYER,
|
||||
} from '../../shared/ipc-commands';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { PlaylistsService } from '../services/playlists.service';
|
||||
import { Settings, VideoPlayer } from '../settings/settings.interface';
|
||||
import { STORE_KEY } from '../shared/enums/store-keys.enum';
|
||||
Playlist,
|
||||
STORE_KEY,
|
||||
VideoPlayer,
|
||||
} from 'shared-interfaces';
|
||||
import * as PlaylistActions from './actions';
|
||||
import {
|
||||
selectActive,
|
||||
@@ -36,6 +34,15 @@ import {
|
||||
|
||||
@Injectable({ providedIn: 'any' })
|
||||
export class PlaylistEffects {
|
||||
private actions$ = inject(Actions);
|
||||
private playlistsService = inject(PlaylistsService);
|
||||
private dataService = inject(DataService);
|
||||
private router = inject(Router);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private storage = inject(StorageMap);
|
||||
private store = inject(Store);
|
||||
private translate = inject(TranslateService);
|
||||
|
||||
updateFavorites$ = createEffect(
|
||||
() => {
|
||||
return this.actions$.pipe(
|
||||
@@ -50,7 +57,7 @@ export class PlaylistEffects {
|
||||
tap(() => {
|
||||
this.snackBar.open(
|
||||
this.translate.instant('CHANNELS.FAVORITES_UPDATED'),
|
||||
null,
|
||||
undefined,
|
||||
{ duration: 2000 }
|
||||
);
|
||||
})
|
||||
@@ -84,7 +91,7 @@ export class PlaylistEffects {
|
||||
combineLatestWith(this.store.select(selectActive)),
|
||||
map(([, activeChannel]) => {
|
||||
firstValueFrom(this.storage.get(STORE_KEY.Settings)).then(
|
||||
(settings: Settings) => {
|
||||
(settings: any) => {
|
||||
if (
|
||||
settings &&
|
||||
Object.keys(settings).length > 0 &&
|
||||
@@ -92,8 +99,8 @@ export class PlaylistEffects {
|
||||
)
|
||||
this.dataService.sendIpcEvent(OPEN_MPV_PLAYER, {
|
||||
url:
|
||||
activeChannel.url +
|
||||
(activeChannel.epgParams ?? ''),
|
||||
activeChannel?.url +
|
||||
(activeChannel?.epgParams ?? ''),
|
||||
mpvPlayerPath: settings?.mpvPlayerPath,
|
||||
});
|
||||
else if (
|
||||
@@ -103,8 +110,8 @@ export class PlaylistEffects {
|
||||
)
|
||||
this.dataService.sendIpcEvent(OPEN_VLC_PLAYER, {
|
||||
url:
|
||||
activeChannel.url +
|
||||
(activeChannel.epgParams ?? ''),
|
||||
activeChannel?.url +
|
||||
(activeChannel?.epgParams ?? ''),
|
||||
vlcPlayerPath: settings?.vlcPlayerPath,
|
||||
});
|
||||
}
|
||||
@@ -132,7 +139,7 @@ export class PlaylistEffects {
|
||||
}
|
||||
|
||||
firstValueFrom(this.storage.get(STORE_KEY.Settings)).then(
|
||||
(settings: Settings) => {
|
||||
(settings: any) => {
|
||||
if (
|
||||
settings &&
|
||||
Object.keys(settings).length > 0 &&
|
||||
@@ -235,7 +242,7 @@ export class PlaylistEffects {
|
||||
this.playlistsService.addPlaylist(action.playlist)
|
||||
),
|
||||
map((playlist: Playlist) => {
|
||||
if (playlist.serverUrl && !isTauri()) {
|
||||
if (playlist.serverUrl && !this.dataService.isElectron) {
|
||||
this.router.navigate(['/xtreams/', playlist._id]);
|
||||
} else if (playlist.macAddress) {
|
||||
this.router.navigate(['stalker', playlist._id]);
|
||||
@@ -245,7 +252,7 @@ export class PlaylistEffects {
|
||||
return playlist;
|
||||
}),
|
||||
map(async (playlist) => {
|
||||
if (playlist.serverUrl && isTauri()) {
|
||||
if (playlist.serverUrl && this.dataService.isElectron) {
|
||||
const db = await Database.load('sqlite:database.db');
|
||||
const result = await db.execute(
|
||||
`INSERT INTO playlists (id, name, serverUrl, username, password, type)
|
||||
@@ -330,7 +337,7 @@ export class PlaylistEffects {
|
||||
tap(() => {
|
||||
this.snackBar.open(
|
||||
this.translate.instant('SETTINGS.PLAYLISTS_REMOVED'),
|
||||
null,
|
||||
undefined,
|
||||
{
|
||||
duration: 2000,
|
||||
}
|
||||
@@ -351,7 +358,7 @@ export class PlaylistEffects {
|
||||
map(([action, channels, activeChannel]) => {
|
||||
let adjacentChannel;
|
||||
const index = channels.findIndex(
|
||||
(channel) => channel.id === activeChannel.id
|
||||
(channel) => channel.id === activeChannel?.id
|
||||
);
|
||||
if (action.direction === 'next') {
|
||||
if (index === channels.length - 1)
|
||||
@@ -363,20 +370,9 @@ export class PlaylistEffects {
|
||||
adjacentChannel = channels[index - 1];
|
||||
}
|
||||
return PlaylistActions.setActiveChannelSuccess({
|
||||
channel: adjacentChannel,
|
||||
channel: adjacentChannel!,
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private playlistsService: PlaylistsService,
|
||||
private dataService: DataService,
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar,
|
||||
private storage: StorageMap,
|
||||
private store: Store,
|
||||
private translate: TranslateService
|
||||
) {}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
|
||||
import { PlaylistMeta } from '../shared/playlist-meta.type';
|
||||
import { PlaylistMeta } from 'shared-interfaces';
|
||||
|
||||
export const playlistsAdapter: EntityAdapter<PlaylistMeta> =
|
||||
createEntityAdapter<PlaylistMeta>({
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createReducer, on } from '@ngrx/store';
|
||||
import moment from 'moment';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import * as PlaylistActions from './actions';
|
||||
import { playlistsAdapter } from './playlists.state';
|
||||
import { PlaylistState, initialState } from './state';
|
||||
@@ -13,14 +14,14 @@ export const playlistReducer = createReducer(
|
||||
const epgParams = `?utc=${from}&lutc=${now}`;
|
||||
return {
|
||||
...state,
|
||||
active: { ...state.active, epgParams },
|
||||
active: { ...state.active, epgParams } as Channel,
|
||||
};
|
||||
}),
|
||||
on(
|
||||
PlaylistActions.resetActiveEpgProgram,
|
||||
(state): PlaylistState => ({
|
||||
...state,
|
||||
active: { ...state.active, epgParams: '' },
|
||||
active: { ...state.active, epgParams: '' } as Channel,
|
||||
})
|
||||
),
|
||||
on(
|
||||
@@ -29,7 +30,7 @@ export const playlistReducer = createReducer(
|
||||
const { channel } = action;
|
||||
return {
|
||||
...state,
|
||||
active: { ...channel, epgParams: '' },
|
||||
active: { ...channel, epgParams: '' } as Channel,
|
||||
};
|
||||
}
|
||||
),
|
||||
@@ -57,13 +58,15 @@ export const playlistReducer = createReducer(
|
||||
let favorites;
|
||||
const { channel } = action;
|
||||
const playlistFavorites =
|
||||
state.playlists.entities[state.playlists.selectedId].favorites;
|
||||
if (playlistFavorites.includes(channel.url)) {
|
||||
state.playlists.entities[state.playlists.selectedId]?.favorites;
|
||||
if (playlistFavorites?.includes(channel.url)) {
|
||||
favorites = [
|
||||
...playlistFavorites.filter((url) => url !== channel.url),
|
||||
...(playlistFavorites ?? []).filter(
|
||||
(url: string) => url !== channel.url
|
||||
),
|
||||
];
|
||||
} else {
|
||||
favorites = [...playlistFavorites, channel.url];
|
||||
favorites = [...(playlistFavorites ?? []), channel.url];
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
@@ -127,7 +130,7 @@ export const playlistReducer = createReducer(
|
||||
userAgent: action.playlist.userAgent,
|
||||
favorites:
|
||||
state.playlists.entities[action.playlistId]
|
||||
.favorites ?? [],
|
||||
?.favorites ?? [],
|
||||
},
|
||||
},
|
||||
state.playlists
|
||||
@@ -254,16 +257,17 @@ export const playlistReducer = createReducer(
|
||||
)
|
||||
);
|
||||
|
||||
export const selectIsEpgAvailable = (state: PlaylistState) =>
|
||||
state.epgAvailable;
|
||||
export const selectActive = (state: PlaylistState) => state.active;
|
||||
export const selectCurrentEpgProgram = (state: PlaylistState) =>
|
||||
state.currentEpgProgram;
|
||||
export const selectChannels = (state: PlaylistState) => state.channels;
|
||||
export const selectPlaylists = (state: PlaylistState) => state.playlists;
|
||||
export const selectPlaylistId = (state: PlaylistState) =>
|
||||
state.playlists?.selectedId;
|
||||
export const selectPlaylistsLoadingFlag = (state: PlaylistState) =>
|
||||
|
||||
export const selectIsEpgAvailableReducer = (state: PlaylistState) =>
|
||||
state.epgAvailable;
|
||||
export const selectActiveReducer = (state: PlaylistState) => state.active;
|
||||
export const selectCurrentEpgProgramReducer = (state: PlaylistState) =>
|
||||
state.currentEpgProgram;
|
||||
export const selectChannelsReducer = (state: PlaylistState) => state.channels;
|
||||
export const selectPlaylistsLoadingFlagReducer = (state: PlaylistState) =>
|
||||
state.playlists?.allPlaylistsLoaded;
|
||||
export const selectCurrentPlaylistId = (state: PlaylistState) =>
|
||||
export const selectCurrentPlaylistIdReducer = (state: PlaylistState) =>
|
||||
state.currentPlaylistId;
|
||||
@@ -1,5 +1,10 @@
|
||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from 'shared-interfaces';
|
||||
import { EntityState } from '@ngrx/entity';
|
||||
import { createFeatureSelector, createSelector, Selector } from '@ngrx/store';
|
||||
import {
|
||||
GLOBAL_FAVORITES_PLAYLIST_ID,
|
||||
Playlist,
|
||||
PlaylistMeta,
|
||||
} from 'shared-interfaces';
|
||||
import * as fromPlaylistMetaState from './playlists.state';
|
||||
import * as fromPlaylistState from './reducers';
|
||||
import { selectRouteParam } from './router.selectors';
|
||||
@@ -10,32 +15,32 @@ export const selectPlaylistState =
|
||||
|
||||
export const selectIsEpgAvailable = createSelector(
|
||||
selectPlaylistState,
|
||||
fromPlaylistState.selectIsEpgAvailable
|
||||
fromPlaylistState.selectIsEpgAvailableReducer
|
||||
);
|
||||
|
||||
export const selectActive = createSelector(
|
||||
selectPlaylistState,
|
||||
fromPlaylistState.selectActive
|
||||
fromPlaylistState.selectActiveReducer
|
||||
);
|
||||
|
||||
export const selectCurrentEpgProgram = createSelector(
|
||||
selectPlaylistState,
|
||||
fromPlaylistState.selectCurrentEpgProgram
|
||||
fromPlaylistState.selectCurrentEpgProgramReducer
|
||||
);
|
||||
|
||||
export const selectCurrentPlaylistId = createSelector(
|
||||
selectPlaylistState,
|
||||
fromPlaylistState.selectCurrentPlaylistId
|
||||
fromPlaylistState.selectCurrentPlaylistIdReducer
|
||||
);
|
||||
|
||||
export const selectChannels = createSelector(
|
||||
selectPlaylistState,
|
||||
fromPlaylistState.selectChannels
|
||||
fromPlaylistState.selectChannelsReducer
|
||||
);
|
||||
|
||||
export const selectPlaylistsLoadingFlag = createSelector(
|
||||
selectPlaylistState,
|
||||
fromPlaylistState.selectPlaylistsLoadingFlag
|
||||
fromPlaylistState.selectPlaylistsLoadingFlagReducer
|
||||
);
|
||||
|
||||
/** Playlist entity store selectors */
|
||||
@@ -83,8 +88,8 @@ export const selectPlaylistTitle = createSelector(
|
||||
data.entities[data.selectedId]
|
||||
) {
|
||||
return (
|
||||
data.entities[data.selectedId].title ||
|
||||
data.entities[data.selectedId].filename
|
||||
data.entities[data.selectedId]?.title ||
|
||||
data.entities[data.selectedId]?.filename
|
||||
);
|
||||
} else return 'Untitled playlist';
|
||||
}
|
||||
@@ -101,7 +106,7 @@ export const selectCurrentPlaylist = createSelector(
|
||||
selectCurrentPlaylistId,
|
||||
(entities, id, currentPlaylistId) => {
|
||||
if (entities) {
|
||||
return entities[id] || entities[currentPlaylistId];
|
||||
return entities[id!] || entities[currentPlaylistId!];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -110,7 +115,7 @@ export const selectCurrentPlaylist = createSelector(
|
||||
export const selectPlaylistById = (id: string) =>
|
||||
createSelector(selectPlaylistEntities, (entities) => {
|
||||
if (entities) {
|
||||
return entities[id];
|
||||
return entities[id] as Playlist;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@@ -119,7 +124,7 @@ export const selectActivePlaylist = createSelector(
|
||||
selectPlaylistsMetaState,
|
||||
(state) => {
|
||||
if (state.entities && state.selectedId !== '') {
|
||||
return state.entities[state.selectedId];
|
||||
return state.entities[state.selectedId] as Playlist;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -128,14 +133,17 @@ export const selectActivePlaylist = createSelector(
|
||||
export const selectFavorites = createSelector(
|
||||
selectPlaylistsMetaState,
|
||||
fromPlaylistMetaState.getPlaylistMetaEntities,
|
||||
fromPlaylistState.selectPlaylistId,
|
||||
fromPlaylistState.selectPlaylistId as unknown as Selector<
|
||||
EntityState<PlaylistMeta>,
|
||||
string
|
||||
>,
|
||||
(data) => {
|
||||
if (
|
||||
data.entities &&
|
||||
data.selectedId &&
|
||||
data.entities[data.selectedId]
|
||||
) {
|
||||
return data.entities[data.selectedId].favorites || [];
|
||||
return data.entities[data.selectedId]?.favorites || [];
|
||||
} else return [];
|
||||
}
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import { initialPlaylistMetaState, PlaylistMetaState } from './playlists.state';
|
||||
|
||||
import { EpgProgram } from '../player/models/epg-program.model';
|
||||
import { EpgProgram } from 'shared-interfaces';
|
||||
|
||||
export interface PlaylistState {
|
||||
active: Channel | undefined;
|
||||
@@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
});
|
||||