feat: support multiple epg sources

This commit is contained in:
4gray committed 2022-07-09 20:01:20 +02:00
1 parent c5789568ac
commit f8c6874ad3
11 files changed
+387 -278

No files matched your search

+2 -4
View File
@@ -6,7 +6,7 @@ import { Api } from '../api';
import { AppMenu } from '../menu';
const contextMenu = require('electron-context-menu');
let win: BrowserWindow = null;
let win: BrowserWindow | null = null;
const args = process.argv.slice(1),
serve = args.some((val) => val === '--serve');
@@ -79,11 +79,9 @@ function createEpgWorkerWindow() {
},
});
window.loadFile('./electron/epg-worker.html');
if (serve) {
window.loadFile(path.join(__dirname, '../electron/index.html'));
window.webContents.openDevTools();
} else {
window.loadFile('./epg-worker.html');
}
window.once('ready-to-show', () => {
-23
View File
@@ -19,7 +19,6 @@ import { WhatsNewService } from './services/whats-new.service';
import { WhatsNewServiceStub } from './services/whats-new.service.stub';
import { Theme } from './settings/theme.enum';
import { STORE_KEY } from './shared/enums/store-keys.enum';
import { ChannelStore } from './state';
class MatSnackBarStub {
open(): void {}
@@ -117,28 +116,6 @@ describe('AppComponent', () => {
}
));
it('should show a notification on epg error', inject(
[MatSnackBar],
(snackbar: MatSnackBar) => {
jest.spyOn(snackbar, 'open');
component.onEpgError();
expect(snackbar.open).toHaveBeenCalledTimes(1);
}
));
it('should handle epg download success', inject(
[MatSnackBar, ChannelStore],
(snackbar: MatSnackBar, channelStore: ChannelStore) => {
jest.spyOn(snackbar, 'open');
jest.spyOn(channelStore, 'setEpgAvailableFlag');
component.onEpgFetchDone();
expect(snackbar.open).toHaveBeenCalledTimes(1);
expect(channelStore.setEpgAvailableFlag).toHaveBeenCalledWith(
true
);
}
));
it('show show whats new dialog', () => {
jest.spyOn(whatsNewService, 'getModalsByVersion');
jest.spyOn(component, 'setDialogVisibility');
+5 -46
View File
@@ -1,5 +1,4 @@
import { Component, NgZone } from '@angular/core';
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { ModalWindow } from 'ngx-whats-new/lib/modal-window.interface';
@@ -7,7 +6,6 @@ import * as semver from 'semver';
import { IpcCommand } from '../../shared/ipc-command.class';
import {
EPG_ERROR,
EPG_FETCH,
EPG_FETCH_DONE,
OPEN_FILE,
SHOW_WHATS_NEW,
@@ -15,13 +13,13 @@ import {
VIEW_SETTINGS,
} from '../../shared/ipc-commands';
import { DataService } from './services/data.service';
import { EpgService } from './services/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 { ChannelStore } from './state';
/**
* AppComponent
@@ -40,18 +38,12 @@ export class AppComponent {
/** Modals to show for the updated version of the application */
modals: ModalWindow[] = [];
/** Default options for epg snackbar notifications */
epgSnackBarOptions: MatSnackBarConfig = {
verticalPosition: 'bottom',
horizontalPosition: 'right',
};
/** List of ipc commands with function mapping */
commandsList = [
new IpcCommand(VIEW_ADD_PLAYLIST, () => this.navigateToRoute('/')),
new IpcCommand(VIEW_SETTINGS, () => this.navigateToRoute('/settings')),
new IpcCommand(EPG_FETCH_DONE, () => this.onEpgFetchDone()),
new IpcCommand(EPG_ERROR, () => this.onEpgError()),
new IpcCommand(EPG_FETCH_DONE, () => this.epgService.onEpgFetchDone()),
new IpcCommand(EPG_ERROR, () => this.epgService.onEpgError()),
new IpcCommand(SHOW_WHATS_NEW, () => this.showWhatsNewDialog()),
];
@@ -62,13 +54,12 @@ export class AppComponent {
* Creates an instance of AppComponent
*/
constructor(
private channelStore: ChannelStore,
private electronService: DataService,
private epgService: EpgService,
private ngZone: NgZone,
private router: Router,
private translate: TranslateService,
private settingsService: SettingsService,
private snackBar: MatSnackBar,
private whatsNewService: WhatsNewService
) {
if (
@@ -127,14 +118,7 @@ export class AppComponent {
if (settings && Object.keys(settings).length > 0) {
this.translate.use(settings.language ?? this.DEFAULT_LANG);
if (settings.epgUrl) {
this.electronService.sendIpcEvent(EPG_FETCH, {
url: settings.epgUrl,
});
this.snackBar.open(
this.translate.instant('EPG.FETCH_EPG'),
this.translate.instant('CLOSE'),
this.epgSnackBarOptions
);
this.epgService.fetchEpg(settings.epgUrl);
}
if (settings.theme) {
@@ -205,31 +189,6 @@ export class AppComponent {
this.router.navigateByUrl(route);
}
/**
* Handles the event when the EPG fetching is done
*/
onEpgFetchDone(): void {
this.channelStore.setEpgAvailableFlag(true);
this.snackBar.open(
this.translate.instant('EPG.DOWNLOAD_SUCCESS'),
null,
{
...this.epgSnackBarOptions,
duration: 2000,
}
);
}
/**
* Handles epg error
*/
onEpgError(): void {
this.snackBar.open(this.translate.instant('EPG.ERROR'), null, {
...this.epgSnackBarOptions,
duration: 2000,
});
}
/**
* Shows the "what is new" dialog
*/
@@ -1,12 +1,12 @@
import { Component, NgZone } from '@angular/core';
import { EpgChannel } from '../../models/epg-channel.model';
import { EpgProgram } from '../../models/epg-program.model';
import * as moment from 'moment';
import { EPG_GET_PROGRAM_DONE } from '../../../../../shared/ipc-commands';
import { ChannelQuery, ChannelStore } from '../../../state';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { EPG_GET_PROGRAM_DONE } from '../../../../../shared/ipc-commands';
import { DataService } from '../../../services/data.service';
import { ChannelQuery, ChannelStore } from '../../../state';
import { EpgChannel } from '../../models/epg-channel.model';
import { EpgProgram } from '../../models/epg-program.model';
export interface EpgData {
channel: EpgChannel;
@@ -76,7 +76,7 @@ export class EpgListComponent {
this.timeshiftUntil$ = this.channelQuery
.select(
(store) =>
store.active.tvg.rec ||
store.active.tvg?.rec ||
store.active.timeshift ||
store.active.catchup?.days
)
+50
View File
@@ -0,0 +1,50 @@
import { inject, TestBed } from '@angular/core/testing';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { TranslateService } from '@ngx-translate/core';
import { MockModule, MockProviders } from 'ng-mocks';
import { ChannelStore } from '../state';
import { DataService } from './data.service';
import { EpgService } from './epg.service';
describe('EpgService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
EpgService,
MockProviders(DataService, TranslateService, MatSnackBar),
],
imports: [MockModule(MatSnackBarModule)],
});
});
it('should create a service instance', inject(
[EpgService],
(service: EpgService) => {
expect(service).toBeTruthy();
}
));
it('should show a notification on epg error', inject(
[MatSnackBar, EpgService],
(snackbar: MatSnackBar, service: EpgService) => {
jest.spyOn(snackbar, 'open');
service.onEpgError();
expect(snackbar.open).toHaveBeenCalledTimes(1);
}
));
it('should handle epg download success', inject(
[MatSnackBar, ChannelStore, EpgService],
(
snackbar: MatSnackBar,
channelStore: ChannelStore,
service: EpgService
) => {
jest.spyOn(snackbar, 'open');
jest.spyOn(channelStore, 'setEpgAvailableFlag');
service.onEpgFetchDone();
expect(snackbar.open).toHaveBeenCalledTimes(1);
expect(channelStore.setEpgAvailableFlag).toHaveBeenCalledWith(true);
}
));
});
+69
View File
@@ -0,0 +1,69 @@
import { Injectable } from '@angular/core';
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
import { TranslateService } from '@ngx-translate/core';
import { EPG_FETCH } from '../../../shared/ipc-commands';
import { ChannelStore } from '../state';
import { DataService } from './data.service';
@Injectable({
providedIn: 'root',
})
export class EpgService {
/** Default options for epg snackbar notifications */
epgSnackBarOptions: MatSnackBarConfig = {
verticalPosition: 'bottom',
horizontalPosition: 'right',
};
constructor(
private channelStore: ChannelStore,
private electronService: DataService,
private snackBar: MatSnackBar,
private translate: TranslateService
) {}
/**
* Fetches and updates EPG from the given URL
* @param urls epg source urls
*/
fetchEpg(urls: string | string[]): void {
if (!Array.isArray(urls)) {
urls = [urls];
}
urls.forEach((url) =>
this.electronService.sendIpcEvent(EPG_FETCH, {
url,
})
);
this.snackBar.open(
this.translate.instant('EPG.FETCH_EPG'),
this.translate.instant('CLOSE'),
this.epgSnackBarOptions
);
}
/**
* Handles the event when the EPG fetching is done
*/
onEpgFetchDone(): void {
this.channelStore.setEpgAvailableFlag(true);
this.snackBar.open(
this.translate.instant('EPG.DOWNLOAD_SUCCESS'),
null,
{
...this.epgSnackBarOptions,
duration: 2000,
}
);
}
/**
* Handles epg error
*/
onEpgError(): void {
this.snackBar.open(this.translate.instant('EPG.ERROR'), null, {
...this.epgSnackBarOptions,
duration: 2000,
});
}
}
+187 -157
View File
@@ -1,187 +1,217 @@
<div class="main-container">
<app-header
[title]="'SETTINGS.TITLE' | translate"
[subtitle]="
('SETTINGS.SUBTITLE' | translate) +
'. ' +
('SETTINGS.DESCRIPTION' | translate)
"
></app-header>
<app-header
[title]="'SETTINGS.TITLE' | translate"
[subtitle]="
('SETTINGS.SUBTITLE' | translate) +
'. ' +
('SETTINGS.DESCRIPTION' | translate)
"
></app-header>
<div class="settings-container">
<form [formGroup]="settingsForm" (ngSubmit)="onSubmit()" novalidate>
<mat-card>
<mat-card-content>
<mat-list>
<mat-list-item *ngIf="isElectron">
<div class="settings-container">
<form [formGroup]="settingsForm" (ngSubmit)="onSubmit()" novalidate>
<mat-card>
<mat-card-content>
<mat-list>
<ng-container formArrayName="epgUrl" *ngIf="isElectron">
<mat-list-item
*ngFor="let _ of epgUrl.controls; index as i"
>
<div matLine fxLayout="row">
<p fxFlex>
{{ 'SETTINGS.EPG_URL_LABEL' | translate }}
</p>
<p fxFlex fxLayoutAlign="start center">
<p
fxFlex
fxLayoutAlign="start center"
fxLayout="row"
>
<mat-form-field class="full-width">
<input
matInput
[placeholder]="
'SETTINGS.EPG_URL_PLACEHOLDER'
| translate
"
matInput
type="url"
formControlName="epgUrl"
[formControlName]="i"
/>
<mat-error
>The value is not a valid
URL</mat-error
>
</mat-form-field>
<button
*ngIf="
(epgAvailable$ | async) &&
settingsForm.value.epgUrl
"
matLine
mat-icon-button
color="accent"
[matTooltip]="
'SETTINGS.REFRESH_EPG' | translate
"
type="button"
(click)="fetchEpg()"
(click)="fetchEpg(epgUrl.value)"
>
<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>
</p>
</div>
<p matLine></p>
</mat-list-item>
<mat-list-item>
<div matLine fxLayout="row">
<p fxFlex>
{{
'SETTINGS.VIDEO_PLAYER_LABEL'
</ng-container>
<mat-list-item *ngIf="isElectron">
<div matLine fxLayout="row">
<p fxFlex></p>
<p fxFlex fxLayoutAlign="start center">
<button
mat-button
color="accent"
type="button"
(click)="addEpgSource()"
>
Add another epg source
</button>
</p>
</div>
</mat-list-item>
<mat-list-item>
<div matLine fxLayout="row">
<p fxFlex>
{{ 'SETTINGS.VIDEO_PLAYER_LABEL' | translate }}
</p>
<p fxFlex fxLayoutAlign="start center">
<mat-form-field appearance="fill">
<mat-label>{{
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER'
| translate
}}
</p>
<p fxFlex fxLayoutAlign="start center">
<mat-form-field appearance="fill">
<mat-label>{{
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER'
| translate
}}</mat-label>
<mat-select formControlName="player">
<mat-option
*ngFor="let player of players"
[value]="player.id"
>{{ player.label }}
</mat-option>
</mat-select>
</mat-form-field>
</p>
</div>
</mat-list-item>
<mat-list-item>
<div matLine fxLayout="row">
<p fxFlex>
{{ 'SETTINGS.LANGUAGE' | translate }}
</p>
<p fxFlex fxLayoutAlign="start center">
<mat-form-field appearance="fill">
<mat-label>{{
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER'
| translate
}}</mat-label>
<mat-select formControlName="language">
<mat-option
*ngFor="
let language of languageEnum
| keyvalue
"
[value]="language.value"
>{{
'LANGUAGES.' + language.key
| translate
}}</mat-option
>
</mat-select>
</mat-form-field>
</p>
</div>
</mat-list-item>
<mat-list-item>
<div matLine fxLayout="row">
<p fxFlex>{{ 'SETTINGS.THEME' | translate }}</p>
<p fxFlex>
<mat-form-field appearance="fill">
<mat-label>{{
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER'
| translate
}}</mat-label>
<mat-select formControlName="theme">
<mat-option
*ngFor="
let theme of themeEnum
| keyvalue
"
[value]="theme.value"
>{{
'THEMES.' + theme.value
| translate
}}</mat-option
>
</mat-select>
</mat-form-field>
</p>
</div>
</mat-list-item>
<mat-list-item>
<div matLine fxLayout="row">
<p fxFlex>
{{ 'SETTINGS.SHOW_CAPTIONS' | translate }}
</p>
<p fxFlex>
<mat-checkbox
formControlName="showCaptions"
></mat-checkbox>
</p>
</div>
</mat-list-item>
<mat-list-item>
<div matLine fxLayout="row">
<p fxFlex>
{{ 'SETTINGS.VERSION' | translate }}
</p>
<p fxFlex>
{{ version }} <br />
{{ updateMessage }}
</p>
</div>
</mat-list-item>
<mat-list-item *ngIf="!isElectron"
>EPG is available in electron-based version of
IPTVnator. Check it&nbsp;<a
href="https://github.com/4gray/iptvnator/releases"
>here</a
></mat-list-item
>
</mat-list>
</mat-card-content>
<mat-divider></mat-divider>
<mat-card-actions align="end">
<button
mat-button
color="accent"
type="reset"
(click)="backToHome()"
}}</mat-label>
<mat-select formControlName="player">
<mat-option
*ngFor="let player of players"
[value]="player.id"
>{{ player.label }}
</mat-option>
</mat-select>
</mat-form-field>
</p>
</div>
</mat-list-item>
<mat-list-item>
<div matLine fxLayout="row">
<p fxFlex>
{{ 'SETTINGS.LANGUAGE' | translate }}
</p>
<p fxFlex fxLayoutAlign="start center">
<mat-form-field appearance="fill">
<mat-label>{{
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER'
| translate
}}</mat-label>
<mat-select formControlName="language">
<mat-option
*ngFor="
let language of languageEnum
| keyvalue
"
[value]="language.value"
>{{
'LANGUAGES.' + language.key
| translate
}}</mat-option
>
</mat-select>
</mat-form-field>
</p>
</div>
</mat-list-item>
<mat-list-item>
<div matLine fxLayout="row">
<p fxFlex>{{ 'SETTINGS.THEME' | translate }}</p>
<p fxFlex>
<mat-form-field appearance="fill">
<mat-label>{{
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER'
| translate
}}</mat-label>
<mat-select formControlName="theme">
<mat-option
*ngFor="
let theme of themeEnum
| keyvalue
"
[value]="theme.value"
>{{
'THEMES.' + theme.value
| translate
}}</mat-option
>
</mat-select>
</mat-form-field>
</p>
</div>
</mat-list-item>
<mat-list-item>
<div matLine fxLayout="row">
<p fxFlex>
{{ 'SETTINGS.SHOW_CAPTIONS' | translate }}
</p>
<p fxFlex>
<mat-checkbox
formControlName="showCaptions"
></mat-checkbox>
</p>
</div>
</mat-list-item>
<mat-list-item>
<div matLine fxLayout="row">
<p fxFlex>
{{ 'SETTINGS.VERSION' | translate }}
</p>
<p fxFlex>
{{ version }} <br />
{{ updateMessage }}
</p>
</div>
</mat-list-item>
<mat-list-item *ngIf="!isElectron"
>EPG is available in electron-based version of
IPTVnator. Check it&nbsp;<a
href="https://github.com/4gray/iptvnator/releases"
>here</a
></mat-list-item
>
<mat-icon>chevron_left</mat-icon>
{{ 'SETTINGS.BACK_TO_HOME' | translate }}
</button>
<button
mat-button
color="accent"
type="submit"
[disabled]="settingsForm.pristine"
>
<mat-icon>save</mat-icon>
{{ 'SETTINGS.SAVE_CHANGES' | translate }}
</button>
</mat-card-actions>
</mat-card>
</form>
</div>
</mat-list>
</mat-card-content>
<mat-divider></mat-divider>
<mat-card-actions align="end">
<button
mat-button
color="accent"
type="reset"
(click)="backToHome()"
>
<mat-icon>chevron_left</mat-icon>
{{ 'SETTINGS.BACK_TO_HOME' | translate }}
</button>
<button
mat-button
color="accent"
type="submit"
[disabled]="settingsForm.pristine || !settingsForm.valid"
>
<mat-icon>save</mat-icon>
{{ 'SETTINGS.SAVE_CHANGES' | translate }}
</button>
</mat-card-actions>
</mat-card>
</form>
</div>
-6
View File
@@ -16,12 +16,6 @@ button {
height: 70px !important;
}
.main-container {
display: grid;
grid-template-rows: 105px auto;
height: calc(100vh - 105px);
}
.settings-container {
overflow: auto;
height: calc(100vh - 140px);
+3 -3
View File
@@ -39,7 +39,7 @@ export class MockRouter {
const DEFAULT_SETTINGS = {
player: VideoPlayer.VideoJs,
epgUrl: '',
epgUrl: [],
language: Language.ENGLISH,
showCaptions: false,
theme: Theme.LightTheme,
@@ -108,7 +108,7 @@ describe('SettingsComponent', () => {
});
describe('Get and set settings on component init', () => {
const settings = { player: 'test', showCaptions: true };
const settings = { player: 'test', showCaptions: true, epgUrl: [] };
let spyOnStorageGet;
beforeEach(() => {
@@ -168,7 +168,7 @@ describe('SettingsComponent', () => {
it('should send epg fetch command', () => {
jest.spyOn(electronService, 'sendIpcEvent');
component.fetchEpg();
component.fetchEpg(['']);
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(EPG_FETCH, {
url: '',
});
+65 -33
View File
@@ -1,6 +1,11 @@
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import {
FormArray,
FormBuilder,
FormControl,
Validators,
} from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { StorageMap } from '@ngx-pwa/local-storage';
@@ -8,8 +13,8 @@ import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import * as semver from 'semver';
import { EPG_FETCH } from '../../../shared/ipc-commands';
import { DataService } from '../services/data.service';
import { EpgService } from '../services/epg.service';
import { STORE_KEY } from '../shared/enums/store-keys.enum';
import { ChannelQuery } from '../state';
import { SettingsService } from './../services/settings.service';
@@ -43,7 +48,13 @@ export class SettingsComponent implements OnInit {
];
/** Settings form object */
settingsForm: FormGroup;
settingsForm = this.formBuilder.group({
player: [VideoPlayer.VideoJs],
epgUrl: new FormArray([]),
language: Language.ENGLISH,
showCaptions: false,
theme: Theme.LightTheme,
});
/** Current version of the app */
version: string;
@@ -62,22 +73,17 @@ export class SettingsComponent implements OnInit {
/** All available visual themes */
themeEnum = Theme;
/** Form array with epg sources */
epgUrl = this.settingsForm.get('epgUrl') as FormArray;
/**
* Creates an instance of SettingsComponent and injects
* required dependencies into the component
* @param channelQuery
* @param electronService
* @param formBuilder
* @param http
* @param router
* @param settingsService
* @param snackBar
* @param storage
* @param translate
*/
constructor(
private channelQuery: ChannelQuery,
private electronService: DataService,
private epgService: EpgService,
private formBuilder: FormBuilder,
private http: HttpClient,
private router: Router,
@@ -85,17 +91,7 @@ export class SettingsComponent implements OnInit {
private snackBar: MatSnackBar,
private storage: StorageMap,
private translate: TranslateService
) {
this.settingsForm = this.formBuilder.group({
player: [VideoPlayer.VideoJs],
epgUrl: '',
language: Language.ENGLISH,
showCaptions: false,
theme: Theme.LightTheme,
});
this.checkAppVersion();
}
) {}
/**
* Reads the config object from the browsers
@@ -110,7 +106,7 @@ export class SettingsComponent implements OnInit {
player: settings.player
? settings.player
: VideoPlayer.VideoJs,
epgUrl: settings.epgUrl ? settings.epgUrl : '',
epgUrl: [],
language: settings.language
? settings.language
: Language.ENGLISH,
@@ -121,8 +117,33 @@ export class SettingsComponent implements OnInit {
? settings.theme
: Theme.LightTheme,
});
this.setEpgUrls(settings.epgUrl);
}
});
this.checkAppVersion();
}
/**
* Sets the epg urls to the form array
* @param epgUrls urls of the EPG sources
*/
setEpgUrls(epgUrls: string[] | string): void {
const URL_REGEX = /^(http|https):\/\/[^ "]+$/;
if (!Array.isArray(epgUrls)) {
epgUrls = [epgUrls];
}
for (const url of epgUrls) {
this.epgUrl.push(
new FormControl(url, [
Validators.required,
Validators.pattern(URL_REGEX),
])
);
}
}
/**
@@ -187,7 +208,7 @@ export class SettingsComponent implements OnInit {
this.settingsForm.markAsPristine();
// check whether the epg url was changed or not
if (this.settingsForm.value.epgUrl) {
this.fetchEpg();
this.fetchEpg(this.settingsForm.value.epgUrl);
}
this.translate.use(this.settingsForm.value.language);
this.settingsService.changeTheme(this.settingsForm.value.theme);
@@ -210,14 +231,25 @@ export class SettingsComponent implements OnInit {
/**
* Fetches and updates EPG from the given URL
* @param urls epg source urls
*/
fetchEpg(): void {
this.electronService.sendIpcEvent(EPG_FETCH, {
url: this.settingsForm.value.epgUrl,
});
this.snackBar.open(this.translate.instant('EPG.FETCH_EPG'), 'Close', {
verticalPosition: 'bottom',
horizontalPosition: 'right',
});
fetchEpg(urls: string | string[]): void {
this.epgService.fetchEpg(urls);
}
/**
* Initializes new entry in form array for EPG URL
*/
addEpgSource(): void {
this.epgUrl.insert(this.epgUrl.length, new FormControl(''));
}
/**
* Removes entry from form array for EPG URL
* @param index index of the item to remove
*/
removeEpgSource(index: number): void {
this.epgUrl.removeAt(index);
this.settingsForm.markAsDirty();
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ export enum VideoPlayer {
*/
export interface Settings {
player: VideoPlayer;
epgUrl: string;
epgUrl: string[];
language: Language;
showCaptions: boolean;
theme: Theme;