mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
Merge pull request #149 from 4gray/141-multiple-epg-sources
Feature: Support multiple EPG sources
This commit is contained in:
30 files changed
+10058
-7309
No files matched your search
@@ -0,0 +1 @@
|
||||
**/*.worker.ts
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/build
|
||||
/tmp
|
||||
/out-tsc
|
||||
/app-builds
|
||||
/release
|
||||
/e2e/tracing
|
||||
/e2e/screenshots
|
||||
/test-results
|
||||
api.js
|
||||
epg-worker.js
|
||||
main.js
|
||||
|
||||
+5
-3
@@ -19,7 +19,7 @@
|
||||
"build": {
|
||||
"builder": "@angular-builders/custom-webpack:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"outputPath": "build",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
@@ -38,7 +38,8 @@
|
||||
"replaceDuplicatePlugins": true
|
||||
},
|
||||
"serviceWorker": true,
|
||||
"ngswConfigPath": "ngsw-config.json"
|
||||
"ngswConfigPath": "ngsw-config.json",
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
},
|
||||
"configurations": {
|
||||
"dev": {
|
||||
@@ -131,7 +132,8 @@
|
||||
"customWebpackConfig": {
|
||||
"path": "./angular.webpack.js",
|
||||
"replaceDuplicatePlugins": true
|
||||
}
|
||||
},
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
BrowserContext,
|
||||
ElectronApplication,
|
||||
Page,
|
||||
_electron as electron,
|
||||
} from 'playwright';
|
||||
const PATH = require('path');
|
||||
|
||||
test.describe('Check Home Page', async () => {
|
||||
let app: ElectronApplication;
|
||||
let firstWindow: Page;
|
||||
let context: BrowserContext;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
app = await electron.launch({
|
||||
args: [
|
||||
PATH.join(__dirname, '../electron/main.js'),
|
||||
PATH.join(__dirname, '../electron/package.json'),
|
||||
],
|
||||
});
|
||||
context = app.context();
|
||||
//await context.tracing.start({ screenshots: true, snapshots: true });
|
||||
|
||||
firstWindow = await app.windows()[0];
|
||||
const isMainWindow = (await firstWindow.title()) === 'IPTVnator';
|
||||
if (!isMainWindow) {
|
||||
firstWindow = await app.windows()[1];
|
||||
}
|
||||
await firstWindow.waitForLoadState('domcontentloaded');
|
||||
});
|
||||
|
||||
test('Launch electron app', async () => {
|
||||
const windowState = await app.evaluate(async (process) => {
|
||||
let mainWindow = process.BrowserWindow.getAllWindows()[0];
|
||||
const isMainWindow = mainWindow.title === 'IPTVnator';
|
||||
if (!isMainWindow) {
|
||||
mainWindow = process.BrowserWindow.getAllWindows()[1];
|
||||
}
|
||||
return {
|
||||
isVisible: mainWindow.isVisible(),
|
||||
isDevToolsOpened: mainWindow.webContents.isDevToolsOpened(),
|
||||
isCrashed: mainWindow.webContents.isCrashed(),
|
||||
};
|
||||
});
|
||||
|
||||
expect(windowState.isVisible).toBeTruthy();
|
||||
expect(windowState.isDevToolsOpened).toBeFalsy();
|
||||
expect(windowState.isCrashed).toBeFalsy();
|
||||
expect(app.windows()).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('Check title', async ({ page }) => {
|
||||
const title = await firstWindow.title();
|
||||
await firstWindow.screenshot({ path: 'e2e/screenshots/intro.png' });
|
||||
expect(title).toBe('IPTVnator');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: '.',
|
||||
maxFailures: 2,
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import { app, BrowserWindow, ipcMain, session } from 'electron';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from './shared/constants';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from '../shared/constants';
|
||||
import {
|
||||
CHANNEL_SET_USER_AGENT,
|
||||
EPG_ERROR,
|
||||
@@ -26,13 +26,13 @@ import {
|
||||
PLAYLIST_UPDATE_FAVORITES,
|
||||
PLAYLIST_UPDATE_POSITIONS,
|
||||
PLAYLIST_UPDATE_RESPONSE,
|
||||
} from './shared/ipc-commands';
|
||||
import { Playlist, PlaylistUpdateState } from './shared/playlist.interface';
|
||||
} from '../shared/ipc-commands';
|
||||
import { Playlist, PlaylistUpdateState } from '../shared/playlist.interface';
|
||||
import {
|
||||
aggregateFavoriteChannels,
|
||||
createFavoritesPlaylist,
|
||||
} from './shared/playlist.utils';
|
||||
import { ParsedPlaylist } from './src/typings.d';
|
||||
} from '../shared/playlist.utils';
|
||||
import { ParsedPlaylist } from '../src/typings.d';
|
||||
|
||||
const Nedb = require('nedb-promises');
|
||||
|
||||
File renamed without changes.
@@ -11,12 +11,15 @@ import {
|
||||
EPG_GET_CHANNELS_DONE,
|
||||
EPG_GET_PROGRAM,
|
||||
EPG_GET_PROGRAM_DONE,
|
||||
} from './shared/ipc-commands';
|
||||
import { EpgChannel } from './src/app/player/models/epg-channel.model';
|
||||
import { EpgProgram } from './src/app/player/models/epg-program.model';
|
||||
} from '../shared/ipc-commands';
|
||||
import { EpgChannel } from '../src/app/player/models/epg-channel.model';
|
||||
import { EpgProgram } from '../src/app/player/models/epg-program.model';
|
||||
|
||||
// EPG data store
|
||||
let EPG_DATA: { channels: EpgChannel[]; programs: EpgProgram[] };
|
||||
let EPG_DATA: { channels: EpgChannel[]; programs: EpgProgram[] } = {
|
||||
channels: [],
|
||||
programs: [],
|
||||
};
|
||||
const loggerLabel = '[EPG Worker]';
|
||||
|
||||
/**
|
||||
@@ -61,7 +64,11 @@ const fetchEpgDataFromUrl = (epgUrl: string) => {
|
||||
*/
|
||||
const parseAndSetEpg = (xmlString) => {
|
||||
console.log(loggerLabel, 'start parsing...');
|
||||
EPG_DATA = parser.parse(xmlString.toString());
|
||||
const parsedEpg = parser.parse(xmlString.toString());
|
||||
EPG_DATA = {
|
||||
channels: [...EPG_DATA.channels, ...parsedEpg.channels],
|
||||
programs: [...EPG_DATA.programs, ...parsedEpg.programs],
|
||||
};
|
||||
ipcRenderer.send(EPG_FETCH_DONE);
|
||||
console.log(loggerLabel, 'done, parsing was finished...');
|
||||
};
|
||||
@@ -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');
|
||||
|
||||
@@ -28,7 +28,7 @@ function createWindow(): BrowserWindow {
|
||||
},
|
||||
resizable: true,
|
||||
darkTheme: true,
|
||||
icon: path.join(__dirname, 'dist/assets/icons/icon.png'),
|
||||
icon: path.join(__dirname, '../build/assets/icons/icon.png'),
|
||||
titleBarStyle: 'hidden',
|
||||
frame: false,
|
||||
minWidth: 900,
|
||||
@@ -44,7 +44,7 @@ function createWindow(): BrowserWindow {
|
||||
} else {
|
||||
win.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, 'dist/index.html'),
|
||||
pathname: path.join(__dirname, '../build/index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
})
|
||||
@@ -79,11 +79,9 @@ function createEpgWorkerWindow() {
|
||||
},
|
||||
});
|
||||
|
||||
window.loadFile('./electron/epg-worker.html');
|
||||
if (serve) {
|
||||
window.loadFile('epg-worker.html');
|
||||
window.webContents.openDevTools();
|
||||
} else {
|
||||
window.loadFile('epg-worker.html');
|
||||
}
|
||||
|
||||
window.once('ready-to-show', () => {
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
SHOW_WHATS_NEW,
|
||||
VIEW_ADD_PLAYLIST,
|
||||
VIEW_SETTINGS,
|
||||
} from './shared/ipc-commands';
|
||||
} from '../shared/ipc-commands';
|
||||
const openAboutWindow = require('about-window').default;
|
||||
|
||||
export class AppMenu {
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "iptvnator-electron",
|
||||
"version": "0.10.0",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
"dependencies": {}
|
||||
}
|
||||
File renamed without changes.
+1
-1
@@ -2,7 +2,7 @@ module.exports = {
|
||||
preset: 'jest-preset-angular',
|
||||
resetMocks: true,
|
||||
setupFilesAfterEnv: ['<rootDir>/src/setup-jest.ts'],
|
||||
testMatch: ['**/+(*.)+(spec|test).+(ts)?(x)'],
|
||||
testMatch: ['**/+(*.)+(spec).+(ts)?(x)'],
|
||||
coverageReporters: ['html', 'lcov'],
|
||||
transformIgnorePatterns: ['node_modules/(?!.*.mjs$|@datorama/akita)'],
|
||||
};
|
||||
Generated
+9500
-6953
File diff suppressed because it is too large.
Load diff
+35
-29
@@ -18,13 +18,14 @@
|
||||
"video",
|
||||
"tv"
|
||||
],
|
||||
"main": "main.js",
|
||||
"main": "electron/main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps && husky install",
|
||||
"ng": "ng",
|
||||
"start": "npm-run-all -p electron:serve ng:serve",
|
||||
"build": "npm run electron:serve-tsc && ng build --base-href ./",
|
||||
"build:electron": "npm run electron:serve-tsc",
|
||||
"build:dev": "npm run build -- -c dev",
|
||||
"build:prod": "npm run build -- -c production",
|
||||
"ng:build": "ng build -c production",
|
||||
@@ -39,14 +40,14 @@
|
||||
"electron:build:windows": "npm run build:prod && electron-builder build -w",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"e2e": "npm run build:prod && cross-env TS_NODE_PROJECT='e2e/tsconfig.e2e.json' mocha e2e/**/*.ts --config ./.mocharc.json --exit",
|
||||
"e2e:watch": "npm run e2e -- --watch",
|
||||
"e2e": "npm run build:electron && npx playwright test -c e2e/playwright.config.ts e2e/",
|
||||
"e2e:debug": "npm run build:electron && PWDEBUG=1 npx playwright test -c e2e/playwright.config.ts e2e/",
|
||||
"version": "conventional-changelog -i CHANGELOG.md -p angular -s -r 0 && git add CHANGELOG.md",
|
||||
"lint": "ng lint",
|
||||
"build-pwa": "ng build -c production --base-href /iptvnator --deploy-url /iptvnator/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "13.0.2",
|
||||
"@angular/animations": "13.3.11",
|
||||
"@angular/cdk": "13.0.2",
|
||||
"@angular/flex-layout": "13.0.0-beta.36",
|
||||
"@angular/material": "13.0.2",
|
||||
@@ -57,7 +58,7 @@
|
||||
"@yangkghjh/videojs-aspect-ratio-panel": "0.0.1",
|
||||
"about-window": "1.13.4",
|
||||
"axios": "0.21.2",
|
||||
"custom-electron-titlebar": "https://github.com/4gray/custom-electron-titlebar.git#testing",
|
||||
"custom-electron-titlebar": "4.1.0",
|
||||
"date-fns": "2.28.0",
|
||||
"electron-context-menu": "3.1.1",
|
||||
"epg-parser": "0.1.1",
|
||||
@@ -70,8 +71,8 @@
|
||||
"ngx-filter-pipe": "2.1.2",
|
||||
"ngx-indexed-db": "9.4.2",
|
||||
"ngx-uploader": "11.0.0",
|
||||
"ngx-whats-new": "0.1.0",
|
||||
"rxjs": "7.5.1",
|
||||
"ngx-whats-new": "0.2.0",
|
||||
"rxjs": "7.5.6",
|
||||
"semver": "7.3.5",
|
||||
"uuid": "8.3.2",
|
||||
"video.js": "7.17.0",
|
||||
@@ -81,32 +82,33 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "13.0.0",
|
||||
"@angular-devkit/build-angular": "13.2.0",
|
||||
"@angular-eslint/builder": "13.0.1",
|
||||
"@angular-eslint/eslint-plugin": "13.0.1",
|
||||
"@angular-eslint/eslint-plugin-template": "13.0.1",
|
||||
"@angular-eslint/schematics": "13.0.1",
|
||||
"@angular-eslint/template-parser": "13.0.1",
|
||||
"@angular/cli": "13.0.2",
|
||||
"@angular/common": "13.0.2",
|
||||
"@angular/compiler": "13.0.2",
|
||||
"@angular/compiler-cli": "13.0.2",
|
||||
"@angular/core": "13.0.2",
|
||||
"@angular/forms": "13.0.2",
|
||||
"@angular/language-service": "13.0.2",
|
||||
"@angular/platform-browser": "13.0.2",
|
||||
"@angular/platform-browser-dynamic": "13.0.2",
|
||||
"@angular/router": "13.0.2",
|
||||
"@angular/service-worker": "13.0.2",
|
||||
"@angular-devkit/build-angular": "13.3.8",
|
||||
"@angular-eslint/builder": "13.3.0",
|
||||
"@angular-eslint/eslint-plugin": "13.3.0",
|
||||
"@angular-eslint/eslint-plugin-template": "13.3.0",
|
||||
"@angular-eslint/schematics": "13.3.0",
|
||||
"@angular-eslint/template-parser": "13.3.0",
|
||||
"@angular/cli": "13.3.8",
|
||||
"@angular/common": "13.3.11",
|
||||
"@angular/compiler": "13.3.11",
|
||||
"@angular/compiler-cli": "13.3.11",
|
||||
"@angular/core": "13.3.11",
|
||||
"@angular/forms": "13.3.11",
|
||||
"@angular/language-service": "13.3.11",
|
||||
"@angular/platform-browser": "13.3.11",
|
||||
"@angular/platform-browser-dynamic": "13.3.11",
|
||||
"@angular/router": "13.3.11",
|
||||
"@angular/service-worker": "13.3.11",
|
||||
"@commitlint/cli": "16.0.1",
|
||||
"@commitlint/config-conventional": "16.0.0",
|
||||
"@ngx-translate/core": "13.0.0",
|
||||
"@ngx-translate/http-loader": "6.0.0",
|
||||
"@playwright/test": "1.21.1",
|
||||
"@semantic-release/changelog": "6.0.1",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@semantic-release/npm": "9.0.0",
|
||||
"@types/chai": "4.3.0",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/jest": "28.1.5",
|
||||
"@types/mocha": "9.0.0",
|
||||
"@types/node": "16.11.7",
|
||||
"@types/video.js": "7.3.29",
|
||||
@@ -123,20 +125,24 @@
|
||||
"eslint": "7.32.0",
|
||||
"eslint-plugin-import": "2.25.2",
|
||||
"husky": "5.0.9",
|
||||
"jest": "27.5.1",
|
||||
"jest-preset-angular": "11.1.1",
|
||||
"ng-mocks": "12.5.1",
|
||||
"jest": "28.1.3",
|
||||
"jest-preset-angular": "12.2.0",
|
||||
"ng-mocks": "14.0.2",
|
||||
"node-polyfill-webpack-plugin": "1.1.4",
|
||||
"npm-run-all": "4.1.5",
|
||||
"playwright": "1.21.1",
|
||||
"playwright-core": "1.21.1",
|
||||
"prettier": "2.5.1",
|
||||
"semantic-release": "19.0.2",
|
||||
"stylelint": "14.2.0",
|
||||
"stylelint-config-standard": "24.0.0",
|
||||
"stylelint-scss": "4.1.0",
|
||||
"ts-jest": "28.0.6",
|
||||
"ts-node": "8.10.2",
|
||||
"tslib": "2.3.0",
|
||||
"typescript": "4.4.4",
|
||||
"wait-on": "6.0.0"
|
||||
"wait-on": "6.0.0",
|
||||
"xvfb-maybe": "0.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
"outDir": "../out-tsc/app",
|
||||
"module": "es2020",
|
||||
"baseUrl": "",
|
||||
"types": []
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["**/*.spec.ts", "**/*.stub.ts"],
|
||||
"exclude": ["**/*.spec.ts", "**/*.stub.ts", "**/*.worker.ts"],
|
||||
"angularCompilerOptions": {
|
||||
"fullTemplateTypeCheck": true,
|
||||
"strictInjectionParameters": true,
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"lib": ["es2017", "es2016", "es2015", "dom"],
|
||||
"resolveJsonModule": true,
|
||||
"strict": false
|
||||
"strict": false,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
||||
"include": ["src/**/*.d.ts"],
|
||||
|
||||
+17
-28
@@ -1,31 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"node"
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es5",
|
||||
"types": ["node"],
|
||||
"lib": ["es2017", "es2016", "es2015", "dom"]
|
||||
},
|
||||
"files": [
|
||||
"electron/api.ts",
|
||||
"electron/epg-worker.ts",
|
||||
"electron/main.ts",
|
||||
"electron/menu.ts",
|
||||
"electron/preload.ts"
|
||||
],
|
||||
"lib": [
|
||||
"es2017",
|
||||
"es2016",
|
||||
"es2015",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"api.ts",
|
||||
"epg-worker.ts",
|
||||
"main.ts",
|
||||
"menu.ts",
|
||||
"preload.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.spec.ts",
|
||||
"**/*.stub.ts"
|
||||
]
|
||||
"exclude": ["node_modules", "**/*.spec.ts", "**/*.stub.ts"]
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/spec",
|
||||
"baseUrl": "./",
|
||||
"types": ["jest", "node"],
|
||||
"types": ["jest", "node", "electron"],
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/worker",
|
||||
"lib": ["es2018", "webworker", "dom"],
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*.worker.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user