mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 13:16:16 -08:00
feat: support multiple epg sources
This commit is contained in:
1 parent
c5789568ac
commit
f8c6874ad3
11 files changed
+387
-278
No files matched your search
+2
-4
@@ -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', () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
));
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 <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 <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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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: '',
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export enum VideoPlayer {
|
||||
*/
|
||||
export interface Settings {
|
||||
player: VideoPlayer;
|
||||
epgUrl: string;
|
||||
epgUrl: string[];
|
||||
language: Language;
|
||||
showCaptions: boolean;
|
||||
theme: Theme;
|
||||
|
||||
Reference in new issue
Block a user