mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
Merge pull request #107 from 4gray/pwa
PWA support and dependency update
This commit is contained in:
66 files changed
+12046
-17427
No files matched your search
@@ -9,8 +9,8 @@ FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
# [Optional] Uncomment if you want to install an additional version of node using nvm
|
||||
# ARG EXTRA_NODE_VERSION=10
|
||||
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
||||
ARG EXTRA_NODE_VERSION=16.10
|
||||
RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
||||
|
||||
# [Optional] Uncomment if you want to install more global node packages
|
||||
# RUN su node -c "npm install -g <your-package-list -here>"
|
||||
+51
-51
@@ -1,62 +1,62 @@
|
||||
name: Build and release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- electron
|
||||
pull_request:
|
||||
branches:
|
||||
- electron
|
||||
push:
|
||||
branches:
|
||||
- electron
|
||||
pull_request:
|
||||
branches:
|
||||
- electron
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "16.x"
|
||||
- run: git config --global url."https://${{ secrets.PAT }}@github.com/".insteadOf ssh://git@github.com/
|
||||
- run: npm ci
|
||||
- run: npm test -- --coverage
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
- run: xvfb-run --auto-servernum npm run e2e
|
||||
- run: npm run build:prod
|
||||
env:
|
||||
CI: true
|
||||
release:
|
||||
needs: [build-and-test]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '16.x'
|
||||
- run: git config --global url."https://${{ secrets.PAT }}@github.com/".insteadOf ssh://git@github.com/
|
||||
- run: npm ci
|
||||
- run: npm test -- --coverage
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
- run: xvfb-run --auto-servernum npm run e2e
|
||||
- run: npm run build:prod
|
||||
env:
|
||||
CI: true
|
||||
release:
|
||||
needs: [build-and-test]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v1
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "16.x"
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '16.x'
|
||||
|
||||
- name: Build Electron app
|
||||
uses: samuelmeuli/action-electron-builder@v1
|
||||
with:
|
||||
# GitHub token, automatically provided to the action
|
||||
# (No need to define this secret in the repo settings)
|
||||
github_token: ${{ secrets.github_token }}
|
||||
- name: Build Electron app
|
||||
uses: samuelmeuli/action-electron-builder@v1
|
||||
with:
|
||||
# GitHub token, automatically provided to the action
|
||||
# (No need to define this secret in the repo settings)
|
||||
github_token: ${{ secrets.github_token }}
|
||||
|
||||
# If the commit is tagged with a version (e.g. "v1.0.0"),
|
||||
# release the app after building
|
||||
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dev-release
|
||||
path: ./release/iptvnator*
|
||||
# If the commit is tagged with a version (e.g. "v1.0.0"),
|
||||
# release the app after building
|
||||
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dev-release
|
||||
path: ./release/iptvnator*
|
||||
@@ -16,6 +16,7 @@ src/**/*.js
|
||||
!src/karma.conf.js
|
||||
*.js.map
|
||||
data.db
|
||||
preload.js
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
@@ -45,6 +46,7 @@ npm-debug.log
|
||||
testem.log
|
||||
/typings
|
||||
package-lock.json
|
||||
.angular/*
|
||||
|
||||
# e2e
|
||||
/e2e/*.js
|
||||
|
||||
+4
-1
@@ -24,7 +24,10 @@
|
||||
"ignoreAtRules": ["function", "if", "each", "include", "mixin"]
|
||||
}
|
||||
],
|
||||
"value-keyword-case": null
|
||||
"value-keyword-case": null,
|
||||
"string-quotes": "single",
|
||||
"value-no-vendor-prefix": null,
|
||||
"property-no-vendor-prefix": null
|
||||
},
|
||||
"plugins": ["stylelint-scss"],
|
||||
"syntax": "scss"
|
||||
|
||||
+67
-27
@@ -1,37 +1,49 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"cli": {
|
||||
"defaultCollection": "@angular-eslint/schematics"
|
||||
},
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-electron": {
|
||||
"iptvnator": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-builders/custom-webpack:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"deleteOutputPath": false,
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"assets": [
|
||||
"src/assets"
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"customWebpackConfig": {
|
||||
"path": "./angular.webpack.js"
|
||||
}
|
||||
"path": "./angular.webpack.js",
|
||||
"replaceDuplicatePlugins": true
|
||||
},
|
||||
"serviceWorker": true,
|
||||
"ngswConfigPath": "ngsw-config.json"
|
||||
},
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"optimization": false,
|
||||
"outputHashing": "all",
|
||||
"outputHashing": "none",
|
||||
"sourceMap": true,
|
||||
"namedChunks": false,
|
||||
"aot": false,
|
||||
@@ -45,6 +57,22 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"optimization": false,
|
||||
"outputHashing": "none",
|
||||
"sourceMap": true,
|
||||
"namedChunks": false,
|
||||
"aot": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": false,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.web.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
@@ -66,61 +94,73 @@
|
||||
"serve": {
|
||||
"builder": "@angular-builders/custom-webpack:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "angular-electron:build"
|
||||
"browserTarget": "iptvnator:build"
|
||||
},
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"browserTarget": "angular-electron:build:dev"
|
||||
"browserTarget": "iptvnator:build:dev"
|
||||
},
|
||||
"web": {
|
||||
"browserTarget": "angular-electron:build:web"
|
||||
"browserTarget": "iptvnator:build:web"
|
||||
},
|
||||
"production": {
|
||||
"browserTarget": "angular-electron:build:production"
|
||||
"browserTarget": "iptvnator:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "angular-electron:build"
|
||||
"browserTarget": "iptvnator:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-builders/custom-webpack:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills-test.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"scripts": [],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets"
|
||||
],
|
||||
"customWebpackConfig": {
|
||||
"path": "./angular.webpack.js",
|
||||
"replaceDuplicatePlugins": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"eslintConfig": ".eslintrc.json",
|
||||
"tsConfig": [
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"angular-electron-e2e": {
|
||||
"iptvnator-e2e": {
|
||||
"root": "e2e",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"eslintConfig": ".eslintrc.json",
|
||||
"tsConfig": [
|
||||
"e2e/tsconfig.e2e.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
"lintFilePatterns": [
|
||||
"e2e/**/*.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "angular-electron",
|
||||
"defaultProject": "iptvnator",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "app",
|
||||
@@ -130,4 +170,4 @@
|
||||
"prefix": "app"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-8
@@ -1,24 +1,35 @@
|
||||
//Polyfill Node.js core modules in Webpack. This module is only needed for webpack 5+.
|
||||
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
|
||||
|
||||
/**
|
||||
* Custom angular webpack configuration
|
||||
*/
|
||||
|
||||
module.exports = (config, options) => {
|
||||
config.target = 'electron-renderer';
|
||||
if (options.customWebpackConfig.target) {
|
||||
config.target = options.customWebpackConfig.target;
|
||||
} else if (options.fileReplacements) {
|
||||
for(let fileReplacement of options.fileReplacements) {
|
||||
|
||||
if (options.fileReplacements) {
|
||||
for (let fileReplacement of options.fileReplacements) {
|
||||
if (fileReplacement.replace !== 'src/environments/environment.ts') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fileReplacementParts = fileReplacement['with'].split('.');
|
||||
if (['dev', 'prod', 'test', 'electron-renderer'].indexOf(fileReplacementParts[1]) < 0) {
|
||||
config.target = fileReplacementParts[1];
|
||||
if (
|
||||
fileReplacementParts.length > 1 &&
|
||||
['web'].indexOf(fileReplacementParts[1]) >= 0
|
||||
) {
|
||||
config.target = 'web';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
config.plugins = [
|
||||
...config.plugins,
|
||||
new NodePolyfillPlugin({
|
||||
excludeAliases: ['console'],
|
||||
}),
|
||||
];
|
||||
|
||||
return config;
|
||||
}
|
||||
};
|
||||
@@ -19,6 +19,14 @@ import {
|
||||
PLAYLIST_UPDATE,
|
||||
PLAYLIST_UPDATE_RESPONSE,
|
||||
PLAYLIST_UPDATE_POSITIONS,
|
||||
PLAYLIST_GET_ALL_RESPONSE,
|
||||
PLAYLIST_REMOVE_BY_ID_RESPONSE,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_GET_ALL,
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
PLAYLIST_GET_BY_ID,
|
||||
OPEN_FILE,
|
||||
PLAYLIST_UPDATE_FAVORITES,
|
||||
} from './shared/ipc-commands';
|
||||
|
||||
const fs = require('fs');
|
||||
@@ -42,7 +50,7 @@ export class Api {
|
||||
workerWindow: BrowserWindow;
|
||||
|
||||
constructor() {
|
||||
ipcMain.on('parse-playlist-by-url', (event, args) => {
|
||||
ipcMain.on(PLAYLIST_PARSE_BY_URL, (event, args) => {
|
||||
try {
|
||||
axios.get(args.url).then((result) => {
|
||||
const parsedPlaylist = this.convertFileStringToPlaylist(
|
||||
@@ -81,9 +89,9 @@ export class Api {
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('playlists-all', (event) => this.sendAllPlaylists(event));
|
||||
ipcMain.on(PLAYLIST_GET_ALL, (event) => this.sendAllPlaylists(event));
|
||||
|
||||
ipcMain.on('playlist-by-id', (event, args) => {
|
||||
ipcMain.on(PLAYLIST_GET_BY_ID, (event, args) => {
|
||||
db.findOne({ _id: args.id }).then((playlist) => {
|
||||
this.setUserAgent(playlist.userAgent);
|
||||
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
|
||||
@@ -92,10 +100,10 @@ export class Api {
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('playlist-remove-by-id', (event, args) => {
|
||||
ipcMain.on(PLAYLIST_REMOVE_BY_ID, (event, args) => {
|
||||
db.remove({ _id: args.id }).then((removed) => {
|
||||
if (removed) {
|
||||
event.sender.send('playlist-remove-by-id-result', {
|
||||
event.sender.send(PLAYLIST_REMOVE_BY_ID_RESPONSE, {
|
||||
message: 'playlist was removed',
|
||||
});
|
||||
}
|
||||
@@ -103,7 +111,7 @@ export class Api {
|
||||
});
|
||||
|
||||
// open playlist from file system
|
||||
ipcMain.on('open-file', (event, args) => {
|
||||
ipcMain.on(OPEN_FILE, (event, args) => {
|
||||
fs.readFile(
|
||||
args.filePath,
|
||||
'utf-8',
|
||||
@@ -131,7 +139,7 @@ export class Api {
|
||||
);
|
||||
});
|
||||
|
||||
ipcMain.on('update-favorites', (event, args) => {
|
||||
ipcMain.on(PLAYLIST_UPDATE_FAVORITES, (event, args) => {
|
||||
db.update(
|
||||
{ id: args.id },
|
||||
{ $set: { favorites: args.favorites } }
|
||||
@@ -248,7 +256,7 @@ export class Api {
|
||||
*/
|
||||
sendAllPlaylists(event: Electron.IpcMainEvent): void {
|
||||
this.getAllPlaylistsMeta().then((playlists) => {
|
||||
event.sender.send('playlist-all-result', {
|
||||
event.sender.send(PLAYLIST_GET_ALL_RESPONSE, {
|
||||
payload: playlists,
|
||||
});
|
||||
});
|
||||
@@ -318,7 +326,7 @@ export class Api {
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves playlist to the localStorage
|
||||
* Creates a playlist object
|
||||
* @param name name of the playlist
|
||||
* @param playlist playlist to save
|
||||
* @param urlOrPath absolute fs path or url of the playlist
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { SpectronClient } from 'spectron';
|
||||
|
||||
import commonSetup from './common-setup';
|
||||
|
||||
describe('angular-electron App', function () {
|
||||
describe('iptvnator App', function () {
|
||||
commonSetup.apply(this);
|
||||
|
||||
let client: SpectronClient;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
"preset": "jest-preset-angular",
|
||||
"resetMocks": true,
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/src/setup-jest.ts"
|
||||
],
|
||||
"testMatch": [
|
||||
"**/+(*.)+(spec|test).+(ts)?(x)"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"html",
|
||||
"lcov"
|
||||
]
|
||||
};
|
||||
@@ -17,6 +17,7 @@ function createWindow(): BrowserWindow {
|
||||
width: 1000,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: true,
|
||||
allowRunningInsecureContent: serve ? true : false,
|
||||
contextIsolation: false,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron';
|
||||
import * as path from 'path';
|
||||
import { SHOW_WHATS_NEW, VIEW_ADD_PLAYLIST, VIEW_SETTINGS } from './shared/ipc-commands';
|
||||
import {
|
||||
SHOW_WHATS_NEW,
|
||||
VIEW_ADD_PLAYLIST,
|
||||
VIEW_SETTINGS,
|
||||
} from './shared/ipc-commands';
|
||||
const openAboutWindow = require('about-window').default;
|
||||
|
||||
export class AppMenu {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
"name": "app",
|
||||
"installMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/favicon.ico",
|
||||
"/index.html",
|
||||
"/manifest.webmanifest",
|
||||
"/*.css",
|
||||
"/*.js"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assets",
|
||||
"installMode": "lazy",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/assets/**",
|
||||
"/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
+10945
-16903
File diff suppressed because it is too large.
Load diff
+82
-89
@@ -27,6 +27,7 @@
|
||||
"build": "npm run electron:serve-tsc && ng build --base-href ./",
|
||||
"build:dev": "npm run build -- -c dev",
|
||||
"build:prod": "npm run build -- -c production",
|
||||
"ng:build": "ng build -c production",
|
||||
"ng:serve": "ng serve -c dev",
|
||||
"ng:serve:web": "ng serve -c web -o",
|
||||
"electron:serve-tsc": "tsc -p tsconfig.serve.json",
|
||||
@@ -41,84 +42,18 @@
|
||||
"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",
|
||||
"version": "conventional-changelog -i CHANGELOG.md -p angular -s -r 0 && git add CHANGELOG.md",
|
||||
"lint": "ng lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "12.1.0",
|
||||
"@angular-devkit/build-angular": "12.1.0",
|
||||
"@angular-eslint/builder": "12.5.0",
|
||||
"@angular-eslint/eslint-plugin": "12.5.0",
|
||||
"@angular-eslint/eslint-plugin-template": "12.5.0",
|
||||
"@angular-eslint/template-parser": "12.5.0",
|
||||
"@angular/cli": "12.1.0",
|
||||
"@angular/common": "12.1.0",
|
||||
"@angular/compiler": "12.1.0",
|
||||
"@angular/compiler-cli": "12.1.0",
|
||||
"@angular/core": "12.1.0",
|
||||
"@angular/forms": "12.1.0",
|
||||
"@angular/language-service": "12.1.0",
|
||||
"@angular/platform-browser": "12.1.0",
|
||||
"@angular/platform-browser-dynamic": "12.1.0",
|
||||
"@angular/router": "12.1.0",
|
||||
"@commitlint/cli": "12.1.4",
|
||||
"@commitlint/config-conventional": "12.1.4",
|
||||
"@ngx-translate/core": "13.0.0",
|
||||
"@ngx-translate/http-loader": "6.0.0",
|
||||
"@semantic-release/changelog": "5.0.1",
|
||||
"@semantic-release/git": "9.0.0",
|
||||
"@semantic-release/npm": "7.1.1",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/jasminewd2": "2.0.8",
|
||||
"@types/jest": "26.0.14",
|
||||
"@types/mocha": "7.0.2",
|
||||
"@types/node": "12.12.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.1.0",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "5.1.0",
|
||||
"@typescript-eslint/parser": "5.1.0",
|
||||
"chai": "4.2.0",
|
||||
"conventional-changelog-cli": "2.0.34",
|
||||
"core-js": "3.6.5",
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "11.5.0",
|
||||
"electron-builder": "22.13.1",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-plugin-import": "2.25.2",
|
||||
"husky": "5.0.9",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"jest": "26.4.2",
|
||||
"jest-preset-angular": "8.3.1",
|
||||
"mocha": "9.1.3",
|
||||
"ng-mocks": "12.5.0",
|
||||
"npm-run-all": "4.1.5",
|
||||
"prettier": "2.3.0",
|
||||
"rxjs": "6.6.3",
|
||||
"semantic-release": "17.2.3",
|
||||
"spectron": "13.0.0",
|
||||
"stylelint": "13.13.1",
|
||||
"stylelint-config-standard": "22.0.0",
|
||||
"stylelint-scss": "3.19.0",
|
||||
"ts-node": "8.10.2",
|
||||
"tslib": "2.3.0",
|
||||
"typescript": "4.3.4",
|
||||
"video.js": "7.15.4",
|
||||
"wait-on": "5.2.1",
|
||||
"webdriver-manager": "12.1.8",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
"lint": "ng lint",
|
||||
"build-pwa": "ng build -c production --base-href /iptvnator --deploy-url /iptvnator/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "12.1.0",
|
||||
"@angular/cdk": "12.1.0",
|
||||
"@angular/flex-layout": "12.0.0-beta.34",
|
||||
"@angular/material": "12.1.0",
|
||||
"@angular/animations": "13.0.2",
|
||||
"@angular/cdk": "13.0.2",
|
||||
"@angular/flex-layout": "13.0.0-beta.36",
|
||||
"@angular/material": "13.0.2",
|
||||
"@datorama/akita": "5.2.6",
|
||||
"@ngx-pwa/local-storage": "12.1.0",
|
||||
"@types/video.js": "7.3.27",
|
||||
"@types/videojs-hls-quality-selector": "1.1.0",
|
||||
"@videojs/http-streaming": "2.11.1",
|
||||
"@electron/remote": "2.0.1",
|
||||
"@ngx-pwa/local-storage": "13.0.2",
|
||||
"@videojs/http-streaming": "2.13.1",
|
||||
"@yangkghjh/videojs-aspect-ratio-panel": "0.0.1",
|
||||
"about-window": "1.13.4",
|
||||
"axios": "0.21.2",
|
||||
@@ -132,24 +67,82 @@
|
||||
"nedb": "1.8.0",
|
||||
"nedb-promises-ts": "0.0.1-4",
|
||||
"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",
|
||||
"semver": "7.3.5",
|
||||
"uuid": "8.3.2",
|
||||
"video.js": "7.17.0",
|
||||
"videojs-contrib-quality-levels": "2.1.0",
|
||||
"videojs-hls-quality-selector": "1.1.4"
|
||||
"videojs-hls-quality-selector": "1.1.4",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-preset-angular",
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/src/setupJest.ts"
|
||||
],
|
||||
"testMatch": [
|
||||
"**/+(*.)+(spec|test).+(ts)?(x)"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"html",
|
||||
"lcov"
|
||||
]
|
||||
}
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "13.0.0",
|
||||
"@angular-devkit/build-angular": "13.0.4",
|
||||
"@angular-eslint/builder": "13.0.1",
|
||||
"@angular-eslint/eslint-plugin": "13.0.1",
|
||||
"@angular-eslint/eslint-plugin-template": "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",
|
||||
"@commitlint/cli": "16.0.1",
|
||||
"@commitlint/config-conventional": "16.0.0",
|
||||
"@ngx-translate/core": "13.0.0",
|
||||
"@ngx-translate/http-loader": "6.0.0",
|
||||
"@semantic-release/changelog": "6.0.1",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@semantic-release/npm": "8.0.3",
|
||||
"@types/chai": "4.3.0",
|
||||
"@types/jest": "27.0.3",
|
||||
"@types/mocha": "9.0.0",
|
||||
"@types/node": "16.11.7",
|
||||
"@types/video.js": "7.3.29",
|
||||
"@types/videojs-hls-quality-selector": "1.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.1.0",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "5.1.0",
|
||||
"@typescript-eslint/parser": "5.1.0",
|
||||
"chai": "4.3.4",
|
||||
"conventional-changelog-cli": "2.2.2",
|
||||
"core-js": "3.6.5",
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "11.0.0",
|
||||
"electron-builder": "22.13.1",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-plugin-import": "2.25.2",
|
||||
"husky": "5.0.9",
|
||||
"jest": "27.4.5",
|
||||
"jest-preset-angular": "11.0.1",
|
||||
"mocha": "9.1.3",
|
||||
"ng-mocks": "12.5.1",
|
||||
"node-polyfill-webpack-plugin": "1.1.4",
|
||||
"npm-run-all": "4.1.5",
|
||||
"prettier": "2.5.1",
|
||||
"semantic-release": "18.0.1",
|
||||
"spectron": "13.0.0",
|
||||
"stylelint": "14.2.0",
|
||||
"stylelint-config-standard": "24.0.0",
|
||||
"stylelint-scss": "4.1.0",
|
||||
"ts-node": "8.10.2",
|
||||
"tslib": "2.3.0",
|
||||
"typescript": "4.4.4",
|
||||
"wait-on": "6.0.0",
|
||||
"webdriver-manager": "12.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"chrome 91"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Color, Titlebar } from 'custom-electron-titlebar';
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
new Titlebar({
|
||||
backgroundColor: Color.fromHex('#000'),
|
||||
itemBackgroundColor: Color.fromHex('#222'),
|
||||
enableMnemonics: true,
|
||||
});
|
||||
});
|
||||
+11
-3
@@ -6,20 +6,28 @@ export const EPG_GET_PROGRAM = 'EPG:GET_PROGRAM';
|
||||
export const EPG_GET_PROGRAM_DONE = 'EPG:GET_PROGRAM_DONE';
|
||||
|
||||
// Playlist related commands
|
||||
export const PLAYLIST_GET_ALL = 'PLAYLIST:GET_ALL';
|
||||
export const PLAYLIST_GET_ALL_RESPONSE = 'PLAYLIST:GET_ALL_RESPONSE';
|
||||
export const PLAYLIST_GET_BY_ID = 'PLAYLIST:GET_BY_ID';
|
||||
export const PLAYLIST_SAVE_DETAILS = 'PLAYLIST:SAVE_DETAILS';
|
||||
export const PLAYLIST_PARSE = 'PLAYLIST:PARSE_PLAYLIST';
|
||||
export const PLAYLIST_PARSE_BY_URL = 'PLAYLIST:PARSE_PLAYLIST_BY_URL';
|
||||
export const PLAYLIST_PARSE_RESPONSE = 'PLAYLIST:PARSE_PLAYLIST_RESPONSE';
|
||||
export const PLAYLIST_UPDATE = 'PLAYLIST:UPDATE';
|
||||
export const PLAYLIST_UPDATE_RESPONSE = 'PLAYLIST:UPDATE_RESPONSE';
|
||||
export const PLAYLIST_UPDATE_POSITIONS = 'PLAYLIST:UPDATE_POSITIONS';
|
||||
export const PLAYLIST_REMOVE_BY_ID = 'PLAYLIST:REMOVE_BY_ID';
|
||||
export const PLAYLIST_REMOVE_BY_ID_RESPONSE = 'PLAYLIST:REMOVE_BY_ID_RESPONSE';
|
||||
export const PLAYLIST_UPDATE_FAVORITES = 'PLAYLIST:UPDATE_FAVORITES';
|
||||
|
||||
// General
|
||||
export const SHOW_WHATS_NEW = 'SHOW_WHATS_NEW';
|
||||
export const ERROR = 'error';
|
||||
export const ERROR = 'ERROR';
|
||||
export const OPEN_FILE = 'OPEN_FILE';
|
||||
|
||||
// Channels
|
||||
export const CHANNEL_SET_USER_AGENT = 'CHANNEL:SET_USER_AGENT';
|
||||
|
||||
// Views
|
||||
export const VIEW_SETTINGS = 'settings-view';
|
||||
export const VIEW_ADD_PLAYLIST = 'add-playlist-view';
|
||||
export const VIEW_SETTINGS = 'VIEW:SETTINGS';
|
||||
export const VIEW_ADD_PLAYLIST = 'VIEW:PLAYLISTS';
|
||||
@@ -7,15 +7,10 @@ import {
|
||||
} from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
import {
|
||||
TranslateModule,
|
||||
TranslatePipe,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import { ElectronService } from './services/electron.service';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { ElectronServiceStub } from './services/electron.service.stub';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MockModule, MockPipe } from 'ng-mocks';
|
||||
import { MockModule, MockPipe, MockProviders } from 'ng-mocks';
|
||||
import { of } from 'rxjs';
|
||||
import { WhatsNewService } from './services/whats-new.service';
|
||||
import { Theme } from './settings/theme.enum';
|
||||
@@ -24,25 +19,17 @@ import { Router } from '@angular/router';
|
||||
import { ChannelStore } from './state';
|
||||
import { STORE_KEY } from './shared/enums/store-keys.enum';
|
||||
import { WhatsNewServiceStub } from './services/whats-new.service.stub';
|
||||
|
||||
jest.mock('custom-electron-titlebar', () => {
|
||||
return {
|
||||
Titlebar: jest.fn().mockImplementation(() => {
|
||||
return {};
|
||||
}),
|
||||
Color: {
|
||||
fromHex: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
import { DataService } from './services/data.service';
|
||||
|
||||
class MatSnackBarStub {
|
||||
open(): void {}
|
||||
}
|
||||
|
||||
jest.spyOn(global.console, 'error').mockImplementation(() => {});
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let component: AppComponent;
|
||||
let electronService: ElectronService;
|
||||
let electronService: DataService;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
let settingsService: SettingsService;
|
||||
let translateService: TranslateService;
|
||||
@@ -54,24 +41,26 @@ describe('AppComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AppComponent, MockPipe(TranslatePipe)],
|
||||
providers: [
|
||||
{ provide: ElectronService, useClass: ElectronServiceStub },
|
||||
{ provide: MatSnackBar, useClass: MatSnackBarStub },
|
||||
{ provide: WhatsNewService, useClass: WhatsNewServiceStub },
|
||||
TranslateService,
|
||||
MockProviders(TranslateService),
|
||||
SettingsService, // TODO: stub
|
||||
{
|
||||
provide: DataService,
|
||||
useClass: ElectronServiceStub,
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
MockModule(MatSnackBarModule),
|
||||
MockModule(NgxWhatsNewModule),
|
||||
RouterTestingModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
electronService = TestBed.inject(ElectronService);
|
||||
electronService = TestBed.inject(DataService);
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
settingsService = TestBed.inject(SettingsService);
|
||||
translateService = TestBed.inject(TranslateService);
|
||||
@@ -82,8 +71,8 @@ describe('AppComponent', () => {
|
||||
});
|
||||
|
||||
it('should create the component and set default language', () => {
|
||||
spyOn(translateService, 'setDefaultLang');
|
||||
spyOn(component, 'setRendererListeners');
|
||||
jest.spyOn(translateService, 'setDefaultLang');
|
||||
jest.spyOn(component, 'setRendererListeners');
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
@@ -91,10 +80,10 @@ describe('AppComponent', () => {
|
||||
});
|
||||
|
||||
it('should init component', () => {
|
||||
spyOn(translateService, 'setDefaultLang');
|
||||
spyOn(component, 'setRendererListeners');
|
||||
spyOn(component, 'initSettings');
|
||||
spyOn(component, 'handleWhatsNewDialog');
|
||||
jest.spyOn(translateService, 'setDefaultLang');
|
||||
jest.spyOn(component, 'setRendererListeners');
|
||||
jest.spyOn(component, 'initSettings');
|
||||
jest.spyOn(component, 'handleWhatsNewDialog');
|
||||
component.ngOnInit();
|
||||
expect(translateService.setDefaultLang).toHaveBeenCalledWith(
|
||||
defaultLanguage
|
||||
@@ -106,24 +95,22 @@ describe('AppComponent', () => {
|
||||
|
||||
describe('Test ipc listeners and commands', () => {
|
||||
it('should set IPC listeners', () => {
|
||||
spyOn(electronService.ipcRenderer, 'on');
|
||||
jest.spyOn(electronService, 'listenOn');
|
||||
component.setRendererListeners();
|
||||
expect(electronService.ipcRenderer.on).toHaveBeenCalledTimes(5);
|
||||
expect(electronService.listenOn).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it('should remove all ipc listeners on destroy', () => {
|
||||
spyOn(electronService.ipcRenderer, 'removeAllListeners');
|
||||
jest.spyOn(electronService, 'removeAllListeners');
|
||||
component.ngOnDestroy();
|
||||
expect(
|
||||
electronService.ipcRenderer.removeAllListeners
|
||||
).toHaveBeenCalledTimes(4);
|
||||
expect(electronService.removeAllListeners).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('should navigate to the provided route', inject(
|
||||
[Router],
|
||||
(router: Router) => {
|
||||
const route = '/add-playlists';
|
||||
spyOn(router, 'navigateByUrl');
|
||||
jest.spyOn(router, 'navigateByUrl');
|
||||
component.navigateToRoute(route);
|
||||
expect(router.navigateByUrl).toHaveBeenCalledTimes(1);
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith(
|
||||
@@ -136,7 +123,7 @@ describe('AppComponent', () => {
|
||||
it('should show a notification on epg error', inject(
|
||||
[MatSnackBar],
|
||||
(snackbar: MatSnackBar) => {
|
||||
spyOn(snackbar, 'open');
|
||||
jest.spyOn(snackbar, 'open');
|
||||
component.onEpgError();
|
||||
expect(snackbar.open).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
@@ -145,8 +132,8 @@ describe('AppComponent', () => {
|
||||
it('should handle epg download success', inject(
|
||||
[MatSnackBar, ChannelStore],
|
||||
(snackbar: MatSnackBar, channelStore: ChannelStore) => {
|
||||
spyOn(snackbar, 'open');
|
||||
spyOn(channelStore, 'setEpgAvailableFlag');
|
||||
jest.spyOn(snackbar, 'open');
|
||||
jest.spyOn(channelStore, 'setEpgAvailableFlag');
|
||||
component.onEpgFetchDone();
|
||||
expect(snackbar.open).toHaveBeenCalledTimes(1);
|
||||
expect(channelStore.setEpgAvailableFlag).toHaveBeenCalledWith(
|
||||
@@ -156,8 +143,8 @@ describe('AppComponent', () => {
|
||||
));
|
||||
|
||||
it('show show whats new dialog', () => {
|
||||
spyOn(whatsNewService, 'getModalsByVersion');
|
||||
spyOn(component, 'setDialogVisibility');
|
||||
jest.spyOn(whatsNewService, 'getModalsByVersion');
|
||||
jest.spyOn(component, 'setDialogVisibility');
|
||||
component.showWhatsNewDialog();
|
||||
expect(whatsNewService.getModalsByVersion).toHaveBeenCalledTimes(1);
|
||||
expect(component.setDialogVisibility).toHaveBeenCalledWith(true);
|
||||
@@ -167,12 +154,13 @@ describe('AppComponent', () => {
|
||||
describe('Test version handling', () => {
|
||||
it('should get actual app version which is outdated and show updates dialog', () => {
|
||||
const currentAppVersion = '0.0.1';
|
||||
const spyOnSettingsGet = spyOn(
|
||||
settingsService,
|
||||
'getValueFromLocalStorage'
|
||||
).and.returnValue(of(currentAppVersion));
|
||||
spyOn(whatsNewService, 'getModalsByVersion').and.returnValue([{}]);
|
||||
spyOn(whatsNewService, 'changeDialogVisibleState');
|
||||
const spyOnSettingsGet = jest
|
||||
.spyOn(settingsService, 'getValueFromLocalStorage')
|
||||
.mockReturnValue(of(currentAppVersion));
|
||||
jest.spyOn(whatsNewService, 'getModalsByVersion').mockReturnValue([
|
||||
{},
|
||||
]);
|
||||
jest.spyOn(whatsNewService, 'changeDialogVisibleState');
|
||||
|
||||
component.handleWhatsNewDialog();
|
||||
expect(spyOnSettingsGet).toHaveBeenCalled();
|
||||
@@ -184,12 +172,13 @@ describe('AppComponent', () => {
|
||||
|
||||
it('should get actual app version which is not outdated and do not shop updates dialog', () => {
|
||||
const currentAppVersion = '1.0.0';
|
||||
const spyOnSettingsGet = spyOn(
|
||||
settingsService,
|
||||
'getValueFromLocalStorage'
|
||||
).and.returnValue(of(currentAppVersion));
|
||||
spyOn(whatsNewService, 'getModalsByVersion').and.returnValue([{}]);
|
||||
spyOn(whatsNewService, 'changeDialogVisibleState');
|
||||
const spyOnSettingsGet = jest
|
||||
.spyOn(settingsService, 'getValueFromLocalStorage')
|
||||
.mockReturnValue(of(currentAppVersion));
|
||||
jest.spyOn(whatsNewService, 'getModalsByVersion').mockReturnValue([
|
||||
{},
|
||||
]);
|
||||
jest.spyOn(whatsNewService, 'changeDialogVisibleState');
|
||||
|
||||
component.handleWhatsNewDialog();
|
||||
expect(spyOnSettingsGet).toHaveBeenCalled();
|
||||
@@ -200,7 +189,7 @@ describe('AppComponent', () => {
|
||||
});
|
||||
|
||||
it('should change the visibility of the whats new dialog', () => {
|
||||
spyOn(whatsNewService, 'changeDialogVisibleState');
|
||||
jest.spyOn(whatsNewService, 'changeDialogVisibleState');
|
||||
component.modals = [{}, {}];
|
||||
const visibilityFlag = true;
|
||||
component.setDialogVisibility(true);
|
||||
@@ -214,7 +203,7 @@ describe('AppComponent', () => {
|
||||
});
|
||||
|
||||
it('should not change the visibility of the whats new dialog', () => {
|
||||
spyOn(whatsNewService, 'changeDialogVisibleState');
|
||||
jest.spyOn(whatsNewService, 'changeDialogVisibleState');
|
||||
component.modals = [];
|
||||
component.setDialogVisibility(true);
|
||||
expect(
|
||||
@@ -229,51 +218,48 @@ describe('AppComponent', () => {
|
||||
const epgUrl = 'http://localhost/epg.xml';
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(electronService.ipcRenderer, 'send');
|
||||
spyOn(settingsService, 'changeTheme');
|
||||
spyOn(component, 'handleWhatsNewDialog');
|
||||
spyOn(translateService, 'use');
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
jest.spyOn(settingsService, 'changeTheme');
|
||||
jest.spyOn(component, 'handleWhatsNewDialog');
|
||||
jest.spyOn(translateService, 'use');
|
||||
});
|
||||
|
||||
it('should get and init settings (all settings are defined)', () => {
|
||||
const spyOnSettingsGet = spyOn(
|
||||
settingsService,
|
||||
'getValueFromLocalStorage'
|
||||
).and.returnValue(of({ theme, epgUrl, language }));
|
||||
const spyOnSettingsGet = jest
|
||||
.spyOn(settingsService, 'getValueFromLocalStorage')
|
||||
.mockReturnValue(of({ theme, epgUrl, language }));
|
||||
|
||||
component.initSettings();
|
||||
|
||||
expect(spyOnSettingsGet).toHaveBeenCalledWith(STORE_KEY.Settings);
|
||||
expect(settingsService.changeTheme).toHaveBeenCalledWith(theme);
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(1);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(1);
|
||||
expect(translateService.use).toHaveBeenCalledWith(language);
|
||||
});
|
||||
|
||||
it('should get and init settings (nothing is defined)', () => {
|
||||
const spyOnSettingsGet = spyOn(
|
||||
settingsService,
|
||||
'getValueFromLocalStorage'
|
||||
).and.returnValue(of());
|
||||
const spyOnSettingsGet = jest
|
||||
.spyOn(settingsService, 'getValueFromLocalStorage')
|
||||
.mockReturnValue(of());
|
||||
|
||||
component.initSettings();
|
||||
|
||||
expect(spyOnSettingsGet).toHaveBeenCalledWith(STORE_KEY.Settings);
|
||||
expect(settingsService.changeTheme).toHaveBeenCalledTimes(0);
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(0);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(0);
|
||||
expect(translateService.use).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should get and init settings (only theme is defined)', () => {
|
||||
const spyOnSettingsGet = spyOn(
|
||||
settingsService,
|
||||
'getValueFromLocalStorage'
|
||||
).and.returnValue(of({ theme }));
|
||||
const spyOnSettingsGet = jest
|
||||
.spyOn(settingsService, 'getValueFromLocalStorage')
|
||||
.mockReturnValue(of({ theme }));
|
||||
|
||||
component.initSettings();
|
||||
|
||||
expect(spyOnSettingsGet).toHaveBeenCalledWith(STORE_KEY.Settings);
|
||||
expect(settingsService.changeTheme).toHaveBeenCalledWith(theme);
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(0);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(0);
|
||||
expect(translateService.use).toHaveBeenCalledWith(defaultLanguage);
|
||||
});
|
||||
});
|
||||
|
||||
+19
-25
@@ -1,16 +1,14 @@
|
||||
import { Component, NgZone } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Titlebar, Color } from 'custom-electron-titlebar';
|
||||
import { ElectronService } from './services/electron.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
|
||||
import { ChannelStore } from './state';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { Settings } from './settings/settings.interface';
|
||||
import {
|
||||
EPG_ERROR,
|
||||
EPG_FETCH,
|
||||
EPG_FETCH_DONE,
|
||||
OPEN_FILE,
|
||||
SHOW_WHATS_NEW,
|
||||
VIEW_ADD_PLAYLIST,
|
||||
VIEW_SETTINGS,
|
||||
@@ -21,13 +19,7 @@ import { WhatsNewService } from './services/whats-new.service';
|
||||
import * as semver from 'semver';
|
||||
import { ModalWindow } from 'ngx-whats-new/lib/modal-window.interface';
|
||||
import { STORE_KEY } from './shared/enums/store-keys.enum';
|
||||
|
||||
// create custom title bar
|
||||
new Titlebar({
|
||||
backgroundColor: Color.fromHex('#000'),
|
||||
itemBackgroundColor: Color.fromHex('#222'),
|
||||
enableMnemonics: true,
|
||||
});
|
||||
import { DataService } from './services/data.service';
|
||||
|
||||
/**
|
||||
* AppComponent
|
||||
@@ -69,18 +61,18 @@ export class AppComponent {
|
||||
*/
|
||||
constructor(
|
||||
private channelStore: ChannelStore,
|
||||
private electronService: ElectronService,
|
||||
private electronService: DataService,
|
||||
private ngZone: NgZone,
|
||||
private router: Router,
|
||||
private translate: TranslateService,
|
||||
private settingsService: SettingsService,
|
||||
private snackBar: MatSnackBar,
|
||||
private storage: StorageMap,
|
||||
private whatsNewService: WhatsNewService
|
||||
) {
|
||||
if (
|
||||
(this.electronService.remote.process.platform === 'linux' ||
|
||||
this.electronService.remote.process.platform === 'win32') &&
|
||||
((this.electronService.isElectron &&
|
||||
this.electronService?.remote?.process.platform === 'linux') ||
|
||||
this.electronService?.remote?.process.platform === 'win32') &&
|
||||
this.electronService.remote.process.argv.length > 2
|
||||
) {
|
||||
const filePath = this.electronService.remote.process.argv.find(
|
||||
@@ -90,7 +82,7 @@ export class AppComponent {
|
||||
if (filePath) {
|
||||
const filePathsArray = filePath.split('/');
|
||||
const fileName = filePathsArray[filePathsArray.length - 1];
|
||||
this.electronService.ipcRenderer.send('open-file', {
|
||||
this.electronService.sendIpcEvent(OPEN_FILE, {
|
||||
filePath,
|
||||
fileName,
|
||||
});
|
||||
@@ -113,11 +105,13 @@ export class AppComponent {
|
||||
* Initializes all necessary listeners for the events from the renderer process
|
||||
*/
|
||||
setRendererListeners(): void {
|
||||
this.commandsList.forEach((command) =>
|
||||
this.electronService.ipcRenderer.on(command.id, () =>
|
||||
this.ngZone.run(() => command.callback())
|
||||
)
|
||||
);
|
||||
if (this.electronService.isElectron) {
|
||||
this.commandsList.forEach((command) =>
|
||||
this.electronService.listenOn(command.id, () =>
|
||||
this.ngZone.run(() => command.callback())
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,7 +125,7 @@ export class AppComponent {
|
||||
if (settings && Object.keys(settings).length > 0) {
|
||||
this.translate.use(settings.language ?? this.DEFAULT_LANG);
|
||||
if (settings.epgUrl) {
|
||||
this.electronService.ipcRenderer.send(EPG_FETCH, {
|
||||
this.electronService.sendIpcEvent(EPG_FETCH, {
|
||||
url: settings.epgUrl,
|
||||
});
|
||||
this.snackBar.open(
|
||||
@@ -229,9 +223,9 @@ export class AppComponent {
|
||||
* Removes all ipc command listeners on component destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.electronService.ipcRenderer.removeAllListeners(EPG_FETCH_DONE);
|
||||
this.electronService.ipcRenderer.removeAllListeners(EPG_ERROR);
|
||||
this.electronService.ipcRenderer.removeAllListeners(VIEW_ADD_PLAYLIST);
|
||||
this.electronService.ipcRenderer.removeAllListeners(VIEW_SETTINGS);
|
||||
this.electronService.removeAllListeners(EPG_FETCH_DONE);
|
||||
this.electronService.removeAllListeners(EPG_ERROR);
|
||||
this.electronService.removeAllListeners(VIEW_ADD_PLAYLIST);
|
||||
this.electronService.removeAllListeners(VIEW_SETTINGS);
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,38 @@ import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { ElectronService } from './services/electron.service';
|
||||
import { PwaService } from './services/pwa.service';
|
||||
import { DataService } from './services/data.service';
|
||||
import { NgxIndexedDBModule, NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { AppConfig } from '../environments/environment';
|
||||
import { dbConfig } from './indexed-db.config';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
|
||||
// AoT requires an exported function for factories
|
||||
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the application runs in the electron based environment.
|
||||
*/
|
||||
function isElectron() {
|
||||
return !!(window && window.process && (window.process as any).type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally imports the necessary service based on the current environment
|
||||
* @param dbService indexed db service
|
||||
* @returns
|
||||
*/
|
||||
export function DataFactory(dbService: NgxIndexedDBService, http: HttpClient) {
|
||||
if (isElectron()) {
|
||||
return new ElectronService();
|
||||
}
|
||||
return new PwaService(dbService, http);
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
@@ -29,6 +55,10 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
HttpClientModule,
|
||||
NgxWhatsNewModule,
|
||||
SharedModule,
|
||||
AppConfig.environment === 'WEB'
|
||||
? NgxIndexedDBModule.forRoot(dbConfig)
|
||||
: [],
|
||||
NgxIndexedDBModule.forRoot(dbConfig),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
@@ -36,6 +66,19 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
deps: [HttpClient],
|
||||
},
|
||||
}),
|
||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||
enabled: AppConfig.production,
|
||||
// Register the ServiceWorker as soon as the app is stable
|
||||
// or after 30 seconds (whichever comes first).
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: DataService,
|
||||
useFactory: DataFactory,
|
||||
deps: [NgxIndexedDBService, HttpClient],
|
||||
},
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
|
||||
@@ -14,7 +14,10 @@ describe('FileUploadComponent', () => {
|
||||
waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [FileUploadComponent, MockPipe(TranslatePipe)],
|
||||
imports: [MockModule(MatIconModule), NgxUploaderModule],
|
||||
imports: [
|
||||
MockModule(MatIconModule),
|
||||
MockModule(NgxUploaderModule),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: TranslateService,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { UploadFile } from 'ngx-uploader';
|
||||
import {
|
||||
PLAYLIST_GET_BY_ID,
|
||||
PLAYLIST_PARSE,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
PLAYLIST_UPDATE,
|
||||
} from './../../../shared/ipc-commands';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
@@ -16,20 +19,28 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { ElectronService } from '../services/electron.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { DialogService } from '../services/dialog.service';
|
||||
import { ElectronServiceStub } from '../services/electron.service.stub';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { NgxIndexedDBModule, NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
class MatSnackBarStub {
|
||||
open(): void {}
|
||||
}
|
||||
|
||||
class NgxIndexedDBServiceStub {
|
||||
getAll() {
|
||||
return of([] as any);
|
||||
}
|
||||
}
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
let dialogService: DialogService;
|
||||
let fixture: ComponentFixture<HomeComponent>;
|
||||
let electronService: ElectronService;
|
||||
let electronService: DataService;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -48,12 +59,17 @@ describe('HomeComponent', () => {
|
||||
MockModule(MatProgressBarModule),
|
||||
MockModule(MatSnackBarModule),
|
||||
MockModule(RouterTestingModule),
|
||||
MockModule(NgxIndexedDBModule),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatSnackBar, useClass: MatSnackBarStub },
|
||||
{ provide: ElectronService, useClass: ElectronServiceStub },
|
||||
{ provide: DataService, useClass: ElectronServiceStub },
|
||||
MockProvider(DialogService),
|
||||
MockProvider(TranslateService),
|
||||
{
|
||||
provide: NgxIndexedDBService,
|
||||
useClass: NgxIndexedDBServiceStub,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
@@ -61,10 +77,10 @@ describe('HomeComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
component = fixture.componentInstance;
|
||||
electronService = TestBed.inject(ElectronService);
|
||||
electronService = TestBed.inject(DataService);
|
||||
dialogService = TestBed.inject(DialogService);
|
||||
router = TestBed.inject(Router);
|
||||
TestBed.inject(ElectronService);
|
||||
TestBed.inject(NgxIndexedDBService);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -74,17 +90,17 @@ describe('HomeComponent', () => {
|
||||
|
||||
it('should open the confirmation dialog on remove icon click', () => {
|
||||
const playlistId = '12345';
|
||||
spyOn(dialogService, 'openConfirmDialog');
|
||||
jest.spyOn(dialogService, 'openConfirmDialog');
|
||||
component.removeClicked(playlistId);
|
||||
expect(dialogService.openConfirmDialog).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should send an event to the main process to remove a playlist', () => {
|
||||
const playlistId = '12345';
|
||||
spyOn(electronService.ipcRenderer, 'send');
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.removePlaylist(playlistId);
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledWith(
|
||||
'playlist-remove-by-id',
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
{ id: playlistId }
|
||||
);
|
||||
});
|
||||
@@ -94,9 +110,9 @@ describe('HomeComponent', () => {
|
||||
_id: 'iptv1',
|
||||
filePath: '/home/user/lists/iptv.m3u',
|
||||
} as PlaylistMeta;
|
||||
spyOn(electronService.ipcRenderer, 'send');
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.refreshPlaylist(playlistMeta);
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledWith(
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
PLAYLIST_UPDATE,
|
||||
{ id: playlistMeta._id, filePath: playlistMeta.filePath }
|
||||
);
|
||||
@@ -104,10 +120,10 @@ describe('HomeComponent', () => {
|
||||
|
||||
it('should send an event to the main process to get a playlist', () => {
|
||||
const playlistId = '6789';
|
||||
spyOn(electronService.ipcRenderer, 'send');
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.getPlaylist(playlistId);
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledWith(
|
||||
'playlist-by-id',
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
PLAYLIST_GET_BY_ID,
|
||||
{
|
||||
id: playlistId,
|
||||
}
|
||||
@@ -117,10 +133,10 @@ describe('HomeComponent', () => {
|
||||
it('should send an event to the main process to get a playlist by URL', () => {
|
||||
const playlistTitle = 'playlist.m3u';
|
||||
const playlistUrl = 'http://test.com/' + playlistTitle;
|
||||
spyOn(electronService.ipcRenderer, 'send');
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.sendPlaylistsUrl(playlistUrl);
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledWith(
|
||||
'parse-playlist-by-url',
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
{
|
||||
title: playlistTitle,
|
||||
url: playlistUrl,
|
||||
@@ -129,7 +145,7 @@ describe('HomeComponent', () => {
|
||||
});
|
||||
|
||||
it('should send an event to the main process to parse a playlist', () => {
|
||||
spyOn(electronService.ipcRenderer, 'send');
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
const title = 'my-list.m3u';
|
||||
const path = '/home/user/iptv/' + title;
|
||||
const playlistContent = 'test';
|
||||
@@ -141,7 +157,7 @@ describe('HomeComponent', () => {
|
||||
target: { result: playlistContent },
|
||||
} as unknown as Event;
|
||||
component.handlePlaylist({ file, uploadEvent });
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledWith(
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
PLAYLIST_PARSE,
|
||||
{ title, playlist: [playlistContent], path }
|
||||
);
|
||||
@@ -160,21 +176,21 @@ describe('HomeComponent', () => {
|
||||
});
|
||||
|
||||
it('should set IPC event listeners', () => {
|
||||
spyOn(electronService.ipcRenderer, 'on');
|
||||
jest.spyOn(electronService, 'listenOn');
|
||||
component.setRendererListeners();
|
||||
expect(electronService.ipcRenderer.on).toHaveBeenCalledTimes(
|
||||
expect(electronService.listenOn).toHaveBeenCalledTimes(
|
||||
component.commandsList.length
|
||||
);
|
||||
});
|
||||
|
||||
it('should send notification on file reject', () => {
|
||||
spyOn(component, 'showNotification');
|
||||
jest.spyOn(component, 'showNotification');
|
||||
component.rejectFile('wrong-file.txt');
|
||||
expect(component.showNotification).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should navigate to the player view', () => {
|
||||
spyOn(router, 'navigateByUrl');
|
||||
jest.spyOn(router, 'navigateByUrl');
|
||||
component.navigateToPlayer();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledTimes(1);
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith(
|
||||
@@ -184,10 +200,10 @@ describe('HomeComponent', () => {
|
||||
});
|
||||
|
||||
it('should remove all ipc listeners on destroy', () => {
|
||||
spyOn(electronService.ipcRenderer, 'removeAllListeners');
|
||||
jest.spyOn(electronService, 'removeAllListeners');
|
||||
component.ngOnDestroy();
|
||||
expect(
|
||||
electronService.ipcRenderer.removeAllListeners
|
||||
).toHaveBeenCalledTimes(component.commandsList.length);
|
||||
expect(electronService.removeAllListeners).toHaveBeenCalledTimes(
|
||||
component.commandsList.length
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -5,15 +5,21 @@ import { ChannelStore, createChannel } from '../state';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Playlist } from '../../../shared/playlist.interface';
|
||||
import { ElectronService } from '../services/electron.service';
|
||||
import {
|
||||
ERROR,
|
||||
PLAYLIST_GET_ALL,
|
||||
PLAYLIST_GET_ALL_RESPONSE,
|
||||
PLAYLIST_GET_BY_ID,
|
||||
PLAYLIST_PARSE,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
PLAYLIST_REMOVE_BY_ID_RESPONSE,
|
||||
PLAYLIST_UPDATE,
|
||||
PLAYLIST_UPDATE_RESPONSE,
|
||||
} from '../../../shared/ipc-commands';
|
||||
import { DialogService } from './../services/dialog.service';
|
||||
import { DataService } from '../services/data.service';
|
||||
|
||||
/** Type to describe meta data of a playlist */
|
||||
export type PlaylistMeta = Pick<
|
||||
@@ -49,18 +55,18 @@ export class HomeComponent {
|
||||
this.setPlaylist(response.payload),
|
||||
},
|
||||
{
|
||||
id: 'playlist-all-result',
|
||||
id: PLAYLIST_GET_ALL_RESPONSE,
|
||||
execute: (response: { payload: Partial<PlaylistMeta[]> }): void => {
|
||||
this.playlists = response.payload;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'playlist-remove-by-id-result',
|
||||
id: PLAYLIST_REMOVE_BY_ID_RESPONSE,
|
||||
execute: (): void => {
|
||||
this.snackBar.open('Done! Playlist was removed.', null, {
|
||||
duration: 2000,
|
||||
});
|
||||
this.electronService.ipcRenderer.send('playlists-all');
|
||||
this.electronService.sendIpcEvent(PLAYLIST_GET_ALL);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -82,22 +88,24 @@ export class HomeComponent {
|
||||
/**
|
||||
* Creates an instanceof HomeComponent
|
||||
* @param channelStore channels store
|
||||
* @param dialogService dialog service
|
||||
* @param electronService electron service
|
||||
* @param ngZone angulars ngZone module
|
||||
* @param router angulars router
|
||||
* @param snackBar snackbars with notification messages
|
||||
* @param ngZone angular ngZone module
|
||||
* @param router angular router
|
||||
* @param snackBar snackbar for notification messages
|
||||
* @param translate translate service
|
||||
*/
|
||||
constructor(
|
||||
private electronService: DataService,
|
||||
private channelStore: ChannelStore,
|
||||
private dialogService: DialogService,
|
||||
private electronService: ElectronService,
|
||||
private ngZone: NgZone,
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar,
|
||||
private translate: TranslateService
|
||||
) {
|
||||
// get all playlists
|
||||
this.electronService.ipcRenderer.send('playlists-all');
|
||||
this.electronService.sendIpcEvent(PLAYLIST_GET_ALL);
|
||||
// set all renderer listeners
|
||||
this.setRendererListeners();
|
||||
}
|
||||
@@ -107,12 +115,17 @@ export class HomeComponent {
|
||||
*/
|
||||
setRendererListeners(): void {
|
||||
this.commandsList.forEach((command) => {
|
||||
this.electronService.ipcRenderer.on(
|
||||
command.id,
|
||||
(event, response) => {
|
||||
this.ngZone.run(() => command.execute(response));
|
||||
}
|
||||
);
|
||||
if (this.electronService.isElectron) {
|
||||
this.electronService.listenOn(command.id, (event, response) =>
|
||||
this.ngZone.run(() => command.execute(response))
|
||||
);
|
||||
} else {
|
||||
this.electronService.listenOn(command.id, (response) => {
|
||||
if (response.data.type === command.id) {
|
||||
command.execute(response.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,10 +148,10 @@ export class HomeComponent {
|
||||
this.isLoading = true;
|
||||
const result = (payload.uploadEvent.target as FileReader).result;
|
||||
const array = (result as string).split('\n');
|
||||
this.electronService.ipcRenderer.send(PLAYLIST_PARSE, {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_PARSE, {
|
||||
title: payload.file.name,
|
||||
playlist: array,
|
||||
path: payload.file.nativeFile.path,
|
||||
path: (payload.file.nativeFile as any).path,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,7 +169,7 @@ export class HomeComponent {
|
||||
*/
|
||||
sendPlaylistsUrl(playlistUrl: string): void {
|
||||
this.isLoading = true;
|
||||
this.electronService.ipcRenderer.send('parse-playlist-by-url', {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_PARSE_BY_URL, {
|
||||
title: this.getLastUrlSegment(playlistUrl),
|
||||
url: playlistUrl,
|
||||
});
|
||||
@@ -201,7 +214,7 @@ export class HomeComponent {
|
||||
* @param playlistId playlist id to remove
|
||||
*/
|
||||
removePlaylist(playlistId: string): void {
|
||||
this.electronService.ipcRenderer.send('playlist-remove-by-id', {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_REMOVE_BY_ID, {
|
||||
id: playlistId,
|
||||
});
|
||||
}
|
||||
@@ -211,7 +224,7 @@ export class HomeComponent {
|
||||
* @param item playlist to update
|
||||
*/
|
||||
refreshPlaylist(item: PlaylistMeta): void {
|
||||
this.electronService.ipcRenderer.send(PLAYLIST_UPDATE, {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_UPDATE, {
|
||||
id: item._id,
|
||||
...(item.url ? { url: item.url } : { filePath: item.filePath }),
|
||||
});
|
||||
@@ -222,7 +235,7 @@ export class HomeComponent {
|
||||
* @param playlistId playlist id
|
||||
*/
|
||||
getPlaylist(playlistId: string): void {
|
||||
this.electronService.ipcRenderer.send('playlist-by-id', {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_GET_BY_ID, {
|
||||
id: playlistId,
|
||||
});
|
||||
}
|
||||
@@ -255,7 +268,7 @@ export class HomeComponent {
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.commandsList.forEach((command) =>
|
||||
this.electronService.ipcRenderer.removeAllListeners(command.id)
|
||||
this.electronService.removeAllListeners(command.id)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="count" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width mb-10">
|
||||
<mat-form-field class="full-width mb-10" *ngIf="isElectron">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.USER_AGENT' | translate
|
||||
}}</mat-label>
|
||||
@@ -54,18 +54,19 @@
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate }}
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-checkbox
|
||||
*ngIf="playlist.url || playlist.filePath"
|
||||
class="full-width"
|
||||
formControlName="autoRefresh"
|
||||
>
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE' | translate }}
|
||||
</mat-checkbox>
|
||||
<p>
|
||||
{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE_DESCRIPTION' | translate
|
||||
}}
|
||||
</p>
|
||||
<ng-container *ngIf="playlist.url || playlist.filePath">
|
||||
<mat-checkbox
|
||||
class="full-width"
|
||||
formControlName="autoRefresh"
|
||||
>
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE' | translate }}
|
||||
</mat-checkbox>
|
||||
<p>
|
||||
{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE_DESCRIPTION' | translate
|
||||
}}
|
||||
</p>
|
||||
</ng-container>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { PLAYLIST_SAVE_DETAILS } from './../../../../../shared/ipc-commands';
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { ElectronServiceStub } from '../../../services/electron.service.stub';
|
||||
import { ElectronService } from './../../../services/electron.service';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { MockModule, MockPipe } from 'ng-mocks';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
@@ -16,7 +16,7 @@ import { Playlist } from './../../../../../shared/playlist.interface';
|
||||
describe('PlaylistInfoComponent', () => {
|
||||
let component: PlaylistInfoComponent;
|
||||
let fixture: ComponentFixture<PlaylistInfoComponent>;
|
||||
let electronService: ElectronService;
|
||||
let electronService: DataService;
|
||||
|
||||
beforeEach(
|
||||
waitForAsync(() => {
|
||||
@@ -35,7 +35,7 @@ describe('PlaylistInfoComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{ provide: MAT_DIALOG_DATA, useValue: {} },
|
||||
{ provide: ElectronService, useClass: ElectronServiceStub },
|
||||
{ provide: DataService, useClass: ElectronServiceStub },
|
||||
FormBuilder,
|
||||
],
|
||||
}).compileComponents();
|
||||
@@ -45,7 +45,7 @@ describe('PlaylistInfoComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PlaylistInfoComponent);
|
||||
component = fixture.componentInstance;
|
||||
electronService = TestBed.inject(ElectronService);
|
||||
electronService = TestBed.inject(DataService);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -55,10 +55,10 @@ describe('PlaylistInfoComponent', () => {
|
||||
|
||||
it('should send an event to the main process after save', () => {
|
||||
const playlistToSave = { _id: 'a12345', title: 'Playlist' } as Playlist;
|
||||
spyOn(electronService.ipcRenderer, 'send');
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.saveChanges(playlistToSave);
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(1);
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledWith(
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(1);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
PLAYLIST_SAVE_DETAILS,
|
||||
playlistToSave
|
||||
);
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
} from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { PLAYLIST_SAVE_DETAILS } from '../../../../../shared/ipc-commands';
|
||||
import { ElectronService } from '../../../services/electron.service';
|
||||
import { Playlist } from '../../../../../shared/playlist.interface';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-playlist-info',
|
||||
@@ -18,6 +18,9 @@ import { Playlist } from '../../../../../shared/playlist.interface';
|
||||
providers: [DatePipe],
|
||||
})
|
||||
export class PlaylistInfoComponent {
|
||||
/** Flag that returns true if application runs in electron-based environment */
|
||||
isElectron = this.electronService.isElectron;
|
||||
|
||||
/** Playlist object */
|
||||
playlist: Playlist;
|
||||
|
||||
@@ -34,7 +37,7 @@ export class PlaylistInfoComponent {
|
||||
constructor(
|
||||
private datePipe: DatePipe,
|
||||
private formBuilder: FormBuilder,
|
||||
private electronService: ElectronService,
|
||||
private electronService: DataService,
|
||||
@Inject(MAT_DIALOG_DATA) playlist: Playlist
|
||||
) {
|
||||
this.playlist = playlist;
|
||||
@@ -76,6 +79,6 @@ export class PlaylistInfoComponent {
|
||||
saveChanges(
|
||||
data: Pick<Playlist, '_id' | 'title' | 'userAgent' | 'autoRefresh'>
|
||||
): void {
|
||||
this.electronService.ipcRenderer.send(PLAYLIST_SAVE_DETAILS, data);
|
||||
this.electronService.sendIpcEvent(PLAYLIST_SAVE_DETAILS, data);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ mat-nav-list {
|
||||
.cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 5px 5px -3px rgb(0 0 0 / 20%), 0 8px 10px 1px rgb(0 0 0 / 14%), 0 3px 14px 2px rgb(0 0 0 / 12%);
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { MockModule, MockPipe, MockProvider } from 'ng-mocks';
|
||||
import { ElectronService } from '../../services/electron.service';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||
import { RecentPlaylistsComponent } from './recent-playlists.component';
|
||||
import { PlaylistMeta } from '../home.component';
|
||||
@@ -13,7 +13,7 @@ import { ElectronServiceStub } from '../../services/electron.service.stub';
|
||||
describe('RecentPlaylistsComponent', () => {
|
||||
let component: RecentPlaylistsComponent;
|
||||
let fixture: ComponentFixture<RecentPlaylistsComponent>;
|
||||
let electronService: ElectronService;
|
||||
let electronService: DataService;
|
||||
let dialog: MatDialog;
|
||||
|
||||
beforeEach(
|
||||
@@ -30,7 +30,7 @@ describe('RecentPlaylistsComponent', () => {
|
||||
MockModule(MatTooltipModule),
|
||||
],
|
||||
providers: [
|
||||
{ provide: ElectronService, useClass: ElectronServiceStub },
|
||||
{ provide: DataService, useClass: ElectronServiceStub },
|
||||
MockProvider(TranslateService),
|
||||
],
|
||||
}).compileComponents();
|
||||
@@ -42,8 +42,7 @@ describe('RecentPlaylistsComponent', () => {
|
||||
component = fixture.componentInstance;
|
||||
component.playlists = [];
|
||||
dialog = TestBed.inject(MatDialog);
|
||||
electronService = TestBed.inject(ElectronService);
|
||||
TestBed.inject(ElectronService);
|
||||
electronService = TestBed.inject(DataService);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -52,7 +51,7 @@ describe('RecentPlaylistsComponent', () => {
|
||||
});
|
||||
|
||||
it('should open the info dialog', () => {
|
||||
spyOn(dialog, 'open');
|
||||
jest.spyOn(dialog, 'open');
|
||||
component.openInfoDialog({} as PlaylistMeta);
|
||||
expect(dialog.open).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -68,7 +67,7 @@ describe('RecentPlaylistsComponent', () => {
|
||||
distance: { x: 0, y: 0 },
|
||||
dropPoint: { x: 0, y: 0 },
|
||||
};
|
||||
spyOn(electronService, 'sendIpcEvent');
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.drop(event);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
import { PlaylistInfoComponent } from './playlist-info/playlist-info.component';
|
||||
import { PlaylistMeta } from './../home.component';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { ElectronService } from '../../services/electron.service';
|
||||
import { DataService } from '../../services/data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recent-playlists',
|
||||
@@ -31,7 +31,7 @@ export class RecentPlaylistsComponent {
|
||||
*/
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private electronService: ElectronService
|
||||
private electronService: DataService
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,11 +39,10 @@ describe('UrlUploadComponent', () => {
|
||||
it(
|
||||
'submit form with playlist url',
|
||||
waitForAsync(() => {
|
||||
spyOn(component.urlAdded, 'emit');
|
||||
jest.spyOn(component.urlAdded, 'emit');
|
||||
const TEST_URL = 'http://example.org/playlist.m3u';
|
||||
const submitButton = fixture.debugElement.nativeElement.querySelector(
|
||||
'button'
|
||||
);
|
||||
const submitButton =
|
||||
fixture.debugElement.nativeElement.querySelector('button');
|
||||
|
||||
// test input field validation
|
||||
expect(submitButton.disabled).toBeTruthy();
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { DBConfig } from 'ngx-indexed-db';
|
||||
|
||||
/**
|
||||
* Contains names of the database stores
|
||||
*/
|
||||
export enum DbStores {
|
||||
Playlists = 'playlists',
|
||||
}
|
||||
|
||||
/** Defines db tables and schema */
|
||||
export const dbConfig: DBConfig = {
|
||||
name: 'iptvnator',
|
||||
version: 1,
|
||||
objectStoresMeta: [
|
||||
{
|
||||
store: DbStores.Playlists,
|
||||
storeConfig: { keyPath: '_id', autoIncrement: false },
|
||||
storeSchema: [
|
||||
{
|
||||
name: '_id',
|
||||
keypath: '_id',
|
||||
options: { unique: false },
|
||||
},
|
||||
{
|
||||
name: 'filename',
|
||||
keypath: 'filename',
|
||||
options: { unique: false },
|
||||
},
|
||||
{ name: 'title', keypath: 'title', options: { unique: false } },
|
||||
{ name: 'count', keypath: 'count', options: { unique: false } },
|
||||
{
|
||||
name: 'playlist',
|
||||
keypath: 'playlist',
|
||||
options: { unique: false },
|
||||
},
|
||||
{
|
||||
name: 'importDate',
|
||||
keypath: 'importDate',
|
||||
options: { unique: false },
|
||||
},
|
||||
{
|
||||
name: 'lastUsage',
|
||||
keypath: 'lastUsage',
|
||||
options: { unique: false },
|
||||
},
|
||||
{
|
||||
name: 'favorites',
|
||||
keypath: 'favorites',
|
||||
options: { unique: false },
|
||||
},
|
||||
{
|
||||
name: 'autoRefresh',
|
||||
keypath: 'autoRefresh',
|
||||
options: { unique: false },
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
keypath: 'url',
|
||||
options: { unique: false },
|
||||
},
|
||||
{
|
||||
name: 'filePath',
|
||||
keypath: 'filePath',
|
||||
options: { unique: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
+1
-1
@@ -69,7 +69,7 @@
|
||||
appearance: none;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCA5NiA5NiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTYgOTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBvbHlnb24gZmlsbD0iI2VlZSIgcG9pbnRzPSI5NiwxNCA4MiwwIDQ4LDM0IDE0LDAgMCwxNCAzNCw0OCAwLDgyIDE0LDk2IDQ4LDYyIDgyLDk2IDk2LDgyIDYyLDQ4ICIvPg0KPC9zdmc+DQo=);
|
||||
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCA5NiA5NiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTYgOTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBvbHlnb24gZmlsbD0iI2VlZSIgcG9pbnRzPSI5NiwxNCA4MiwwIDQ4LDM0IDE0LDAgMCwxNCAzNCw0OCAwLDgyIDE0LDk2IDQ4LDYyIDgyLDk2IDk2LDgyIDYyLDQ4ICIvPg0KPC9zdmc+DQo=');
|
||||
background-size: 10px 10px;
|
||||
color: #fff;
|
||||
}
|
||||
+23
-24
@@ -1,26 +1,26 @@
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ChannelListContainerComponent } from './channel-list-container.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { MockModule, MockPipes } from 'ng-mocks';
|
||||
import { FilterPipe } from 'ngx-filter-pipe';
|
||||
import * as MOCKED_PLAYLIST from '../../../../mocks/playlist.json';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { ElectronServiceStub } from '../../../services/electron.service.stub';
|
||||
import { createChannel } from '../../../state';
|
||||
import { ChannelQuery } from '../../../state/channel.query';
|
||||
import { ChannelStore } from '../../../state/channel.store';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MockModule, MockPipe } from 'ng-mocks';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { FilterPipeModule } from 'ngx-filter-pipe';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { createChannel } from '../../../state';
|
||||
import * as MOCKED_PLAYLIST from '../../../../mocks/playlist.json';
|
||||
import { ElectronService } from '../../../services/electron.service';
|
||||
import { ElectronServiceStub } from '../../../services/electron.service.stub';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { ChannelListContainerComponent } from './channel-list-container.component';
|
||||
|
||||
class MatSnackBarStub {
|
||||
open(): void {}
|
||||
@@ -35,12 +35,12 @@ describe('ChannelListContainerComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
ChannelListContainerComponent,
|
||||
MockPipe(TranslatePipe),
|
||||
MockPipes(TranslatePipe, FilterPipe),
|
||||
],
|
||||
providers: [
|
||||
ChannelQuery,
|
||||
{ provide: MatSnackBar, useClass: MatSnackBarStub },
|
||||
{ provide: ElectronService, useClass: ElectronServiceStub },
|
||||
{ provide: DataService, useClass: ElectronServiceStub },
|
||||
],
|
||||
imports: [
|
||||
MockModule(MatSnackBarModule),
|
||||
@@ -53,7 +53,6 @@ describe('ChannelListContainerComponent', () => {
|
||||
MockModule(MatExpansionModule),
|
||||
FormsModule,
|
||||
RouterTestingModule,
|
||||
FilterPipeModule,
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
@@ -146,14 +145,14 @@ describe('ChannelListContainerComponent', () => {
|
||||
});
|
||||
|
||||
it('should update store after channel was selected', () => {
|
||||
spyOn(store, 'update');
|
||||
jest.spyOn(store, 'update');
|
||||
component.selectChannel(component._channelList[0]);
|
||||
fixture.detectChanges();
|
||||
expect(store.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should update store after channel was favorited', () => {
|
||||
spyOn(store, 'updateFavorite');
|
||||
jest.spyOn(store, 'updateFavorite');
|
||||
component.toggleFavoriteChannel(
|
||||
component._channelList[0],
|
||||
new MouseEvent('click')
|
||||
|
||||
@@ -20,8 +20,7 @@ const EPG_PROGRAM_ITEM = {
|
||||
desc: [
|
||||
{
|
||||
lang: 'en',
|
||||
value:
|
||||
"Jordan's Queen Rania has made job creation a priority to help curb the staggering unemployment rates among youths in the Middle East.",
|
||||
value: "Jordan's Queen Rania has made job creation a priority to help curb the staggering unemployment rates among youths in the Middle East.",
|
||||
},
|
||||
],
|
||||
date: ['20080711'],
|
||||
@@ -93,7 +92,7 @@ describe('EpgListItemComponent', () => {
|
||||
});
|
||||
|
||||
it('should trigger the function to open the details dialog', () => {
|
||||
spyOn(dialog, 'open');
|
||||
jest.spyOn(dialog, 'open');
|
||||
component.showDescription({} as EpgProgram);
|
||||
expect(dialog.open).toHaveBeenCalledTimes(1);
|
||||
expect(dialog.open).toHaveBeenCalledWith(EpgItemDescriptionComponent, {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { EpgListComponent, EpgData } from './epg-list.component';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { ElectronService } from '../../../services/electron.service';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { ElectronServiceStub } from '../../../services/electron.service.stub';
|
||||
import * as moment from 'moment';
|
||||
import { EPG_GET_PROGRAM_DONE } from '../../../../../shared/ipc-commands';
|
||||
@@ -18,7 +18,7 @@ import { EpgListItemComponent } from './epg-list-item/epg-list-item.component';
|
||||
describe('EpgListComponent', () => {
|
||||
let component: EpgListComponent;
|
||||
let fixture: ComponentFixture<EpgListComponent>;
|
||||
let electronService: ElectronService;
|
||||
let electronService: DataService;
|
||||
let channelStore: ChannelStore;
|
||||
|
||||
const MOCKED_PROGRAMS = {
|
||||
@@ -100,7 +100,7 @@ describe('EpgListComponent', () => {
|
||||
MockModule(MatDialogModule),
|
||||
],
|
||||
providers: [
|
||||
{ provide: ElectronService, useClass: ElectronServiceStub },
|
||||
{ provide: DataService, useClass: ElectronServiceStub },
|
||||
MockProvider(MatDialog),
|
||||
],
|
||||
}).compileComponents();
|
||||
@@ -110,7 +110,7 @@ describe('EpgListComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EpgListComponent);
|
||||
component = fixture.componentInstance;
|
||||
electronService = TestBed.inject(ElectronService);
|
||||
electronService = TestBed.inject(DataService);
|
||||
channelStore = TestBed.inject(ChannelStore);
|
||||
channelStore.setActiveChannel({
|
||||
id: '',
|
||||
@@ -148,18 +148,16 @@ describe('EpgListComponent', () => {
|
||||
});
|
||||
|
||||
it('should remove ipc listeners on destroy', () => {
|
||||
spyOn(electronService.ipcRenderer, 'removeAllListeners');
|
||||
jest.spyOn(electronService, 'removeAllListeners');
|
||||
component.ngOnDestroy();
|
||||
expect(
|
||||
electronService.ipcRenderer.removeAllListeners
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
electronService.ipcRenderer.removeAllListeners
|
||||
).toHaveBeenCalledWith(EPG_GET_PROGRAM_DONE);
|
||||
expect(electronService.removeAllListeners).toHaveBeenCalledTimes(1);
|
||||
expect(electronService.removeAllListeners).toHaveBeenCalledWith(
|
||||
EPG_GET_PROGRAM_DONE
|
||||
);
|
||||
});
|
||||
|
||||
it('should set epg program as active', () => {
|
||||
spyOn(channelStore, 'setActiveEpgProgram');
|
||||
jest.spyOn(channelStore, 'setActiveEpgProgram');
|
||||
component.setEpgProgram(MOCKED_PROGRAMS.items[0], false, true);
|
||||
expect(channelStore.setActiveEpgProgram).toHaveBeenCalledTimes(1);
|
||||
expect(channelStore.setActiveEpgProgram).toHaveBeenCalledWith(
|
||||
@@ -168,7 +166,7 @@ describe('EpgListComponent', () => {
|
||||
});
|
||||
|
||||
it('should reset active epg program', () => {
|
||||
spyOn(channelStore, 'resetActiveEpgProgram');
|
||||
jest.spyOn(channelStore, 'resetActiveEpgProgram');
|
||||
component.setEpgProgram(MOCKED_PROGRAMS.items[0], true);
|
||||
expect(channelStore.resetActiveEpgProgram).toHaveBeenCalledTimes(1);
|
||||
component.setEpgProgram(MOCKED_PROGRAMS.items[0], true, true);
|
||||
|
||||
@@ -2,11 +2,11 @@ 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 { ElectronService } from '../../../services/electron.service';
|
||||
import { EPG_GET_PROGRAM_DONE } from '../../../../../shared/ipc-commands';
|
||||
import { ChannelQuery, ChannelStore } from '../../../state';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
|
||||
export interface EpgData {
|
||||
channel: EpgChannel;
|
||||
@@ -58,10 +58,10 @@ export class EpgListComponent {
|
||||
constructor(
|
||||
private channelQuery: ChannelQuery,
|
||||
private channelStore: ChannelStore,
|
||||
private electronService: ElectronService,
|
||||
private electronService: DataService,
|
||||
private ngZone: NgZone
|
||||
) {
|
||||
this.electronService.ipcRenderer.on(
|
||||
this.electronService.listenOn(
|
||||
EPG_GET_PROGRAM_DONE,
|
||||
(event, response) => {
|
||||
this.ngZone.run(() => this.handleEpgData(response));
|
||||
@@ -94,7 +94,7 @@ export class EpgListComponent {
|
||||
* @param programs
|
||||
*/
|
||||
handleEpgData(programs: { payload: EpgData }): void {
|
||||
if (programs.payload?.items?.length > 0) {
|
||||
if (programs?.payload?.items?.length > 0) {
|
||||
this.programs = programs;
|
||||
this.timeNow = moment(Date.now()).format(DATE_TIME_FORMAT);
|
||||
this.dateToday = moment(Date.now()).format(DATE_FORMAT);
|
||||
@@ -183,8 +183,6 @@ export class EpgListComponent {
|
||||
* Removes all ipc renderer listeners after destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.electronService.ipcRenderer.removeAllListeners(
|
||||
EPG_GET_PROGRAM_DONE
|
||||
);
|
||||
this.electronService.removeAllListeners(EPG_GET_PROGRAM_DONE);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { MockPipe } from 'ng-mocks';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { SimpleChange } from '@angular/core';
|
||||
|
||||
import { HtmlVideoPlayerComponent } from './html-video-player.component';
|
||||
|
||||
describe('HtmlVideoPlayerComponent', () => {
|
||||
@@ -40,13 +38,15 @@ describe('HtmlVideoPlayerComponent', () => {
|
||||
});
|
||||
|
||||
it('should call play channel function after input changes', () => {
|
||||
spyOn(component, 'playChannel');
|
||||
jest.spyOn(component, 'playChannel');
|
||||
jest.spyOn(global.console, 'error').mockImplementation(() => {});
|
||||
component.ngOnChanges({
|
||||
channel: new SimpleChange(null, TEST_CHANNEL, true),
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.playChannel).toBeCalledTimes(1);
|
||||
expect(component.playChannel).toBeCalledWith(TEST_CHANNEL);
|
||||
expect(component.playChannel).toHaveBeenCalledTimes(1);
|
||||
expect(component.playChannel).toHaveBeenCalledWith(TEST_CHANNEL);
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -49,7 +49,7 @@ export class HtmlVideoPlayerComponent implements OnChanges, OnDestroy {
|
||||
playChannel(channel: Channel): void {
|
||||
if (this.hls) this.hls.destroy();
|
||||
const url = channel.url + channel.epgParams;
|
||||
if (Hls.isSupported()) {
|
||||
if (Hls && Hls.isSupported()) {
|
||||
console.log('... switching channel to ', channel.name, url);
|
||||
this.hls = new Hls();
|
||||
this.hls.attachMedia(this.videoPlayer.nativeElement);
|
||||
|
||||
@@ -17,6 +17,8 @@ import { createChannel } from '../../../state';
|
||||
import { HtmlVideoPlayerComponent } from '../html-video-player/html-video-player.component';
|
||||
import { EpgListComponent } from '../epg-list/epg-list.component';
|
||||
import { VideoPlayer } from '../../../settings/settings.interface';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { ElectronServiceStub } from '../../../services/electron.service.stub';
|
||||
|
||||
class MatSnackBarStub {
|
||||
open(): void {}
|
||||
@@ -43,6 +45,7 @@ describe('VideoPlayerComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatSnackBar, useClass: MatSnackBarStub },
|
||||
{ provide: DataService, useClass: ElectronServiceStub },
|
||||
],
|
||||
imports: [
|
||||
MockModule(MatSidenavModule),
|
||||
@@ -67,9 +70,9 @@ describe('VideoPlayerComponent', () => {
|
||||
|
||||
it('should create and init component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
spyOn(component, 'applySettings');
|
||||
jest.spyOn(component, 'applySettings');
|
||||
fixture.detectChanges();
|
||||
expect(component.applySettings).toBeCalledTimes(1);
|
||||
expect(component.applySettings).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should check default component settings', () => {
|
||||
@@ -81,11 +84,11 @@ describe('VideoPlayerComponent', () => {
|
||||
});
|
||||
|
||||
it('should update store after channel was faved', () => {
|
||||
spyOn(store, 'updateFavorite');
|
||||
jest.spyOn(store, 'updateFavorite');
|
||||
const [firstChannel] = channels;
|
||||
component.addToFavorites(firstChannel);
|
||||
fixture.detectChanges();
|
||||
expect(store.updateFavorite).toBeCalledTimes(1);
|
||||
expect(store.updateFavorite).toBeCalledWith(firstChannel);
|
||||
//fixture.detectChanges();
|
||||
expect(store.updateFavorite).toHaveBeenCalledTimes(1);
|
||||
expect(store.updateFavorite).toHaveBeenCalledWith(firstChannel);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
export abstract class DataService {
|
||||
get isElectron(): boolean {
|
||||
return !!(window && window.process && (window.process as any).type);
|
||||
}
|
||||
get remote() {
|
||||
return this.isElectron ? window.require('electron').remote : null;
|
||||
}
|
||||
get ipcRenderer(): typeof ipcRenderer {
|
||||
return this.isElectron ? window.require('electron').ipcRenderer : null;
|
||||
}
|
||||
abstract getAppVersion(): string;
|
||||
abstract sendIpcEvent(type: string, payload?: unknown): void;
|
||||
abstract removeAllListeners(type: string): void;
|
||||
abstract listenOn(
|
||||
command: string,
|
||||
callback: (...args: any[]) => void
|
||||
): void;
|
||||
}
|
||||
@@ -25,7 +25,9 @@ describe('Service: Dialog', () => {
|
||||
it('should open a confirm dialog', inject(
|
||||
[MatDialog, DialogService],
|
||||
(dialog: MatDialog, service: DialogService) => {
|
||||
spyOn(dialog, 'open').and.returnValue({ afterClosed: () => EMPTY });
|
||||
jest.spyOn(dialog, 'open').mockReturnValue({
|
||||
afterClosed: () => EMPTY,
|
||||
} as any);
|
||||
service.openConfirmDialog({
|
||||
title: 'Remove dialog',
|
||||
message: 'Message',
|
||||
|
||||
@@ -11,14 +11,18 @@ export class ElectronServiceStub {
|
||||
},
|
||||
app: {
|
||||
getVersion: jest.fn(() => '1.0.0'),
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
isElectron = true;
|
||||
|
||||
removeAllListeners(type: string) {}
|
||||
|
||||
getAppVersion() {
|
||||
return this.remote.app.getVersion();
|
||||
}
|
||||
|
||||
sendIpcEvent() {
|
||||
this.ipcRenderer.send();
|
||||
}
|
||||
}
|
||||
sendIpcEvent() {}
|
||||
|
||||
listenOn() {}
|
||||
}
|
||||
@@ -1,34 +1,16 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
// If you import a module but never use any of the imported values other than as TypeScript types,
|
||||
// the resulting javascript file will look as if you never imported the module at all.
|
||||
import { ipcRenderer, webFrame, remote } from 'electron';
|
||||
import * as childProcess from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import { DataService } from './data.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ElectronService {
|
||||
ipcRenderer: typeof ipcRenderer;
|
||||
webFrame: typeof webFrame;
|
||||
remote: typeof remote;
|
||||
childProcess: typeof childProcess;
|
||||
fs: typeof fs;
|
||||
|
||||
get isElectron(): boolean {
|
||||
return !!(window && window.process && window.process.type);
|
||||
}
|
||||
|
||||
export class ElectronService extends DataService {
|
||||
/**
|
||||
* Creates an instance of ElectronService.
|
||||
*/
|
||||
constructor() {
|
||||
// Conditional imports
|
||||
if (this.isElectron) {
|
||||
this.ipcRenderer = window.require('electron').ipcRenderer;
|
||||
this.webFrame = window.require('electron').webFrame;
|
||||
this.remote = window.require('electron').remote;
|
||||
this.childProcess = window.require('child_process');
|
||||
this.fs = window.require('fs');
|
||||
}
|
||||
super();
|
||||
console.log('Electron service initialized...');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +25,24 @@ export class ElectronService {
|
||||
* @param type event type
|
||||
* @param payload data payload
|
||||
*/
|
||||
sendIpcEvent(type: string, payload: unknown): void {
|
||||
sendIpcEvent(type: string, payload?: unknown): void {
|
||||
this.ipcRenderer.send(type, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all IPC listeners
|
||||
* @param type command name
|
||||
*/
|
||||
removeAllListeners(type: string): void {
|
||||
this.ipcRenderer.removeAllListeners(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens on an IPC event from the main process
|
||||
* @param command command name
|
||||
* @param callback callback function
|
||||
*/
|
||||
listenOn(command: string, callback: (...args: any[]) => void): void {
|
||||
this.ipcRenderer.on(command, callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { guid } from '@datorama/akita';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import { NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { switchMap } from 'rxjs';
|
||||
import {
|
||||
PLAYLIST_GET_ALL,
|
||||
PLAYLIST_GET_ALL_RESPONSE,
|
||||
PLAYLIST_GET_BY_ID,
|
||||
PLAYLIST_PARSE,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
PLAYLIST_REMOVE_BY_ID_RESPONSE,
|
||||
PLAYLIST_SAVE_DETAILS,
|
||||
PLAYLIST_UPDATE,
|
||||
PLAYLIST_UPDATE_FAVORITES,
|
||||
} from '../../../shared/ipc-commands';
|
||||
import {
|
||||
Playlist,
|
||||
PlaylistUpdateState,
|
||||
} from '../../../shared/playlist.interface';
|
||||
import { AppConfig } from '../../environments/environment';
|
||||
import { ParsedPlaylist } from '../../typings';
|
||||
import { DbStores } from '../indexed-db.config';
|
||||
import { DataService } from './data.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PwaService extends DataService {
|
||||
/**
|
||||
* Creates an instance of PwaService.
|
||||
* @param dbService database service
|
||||
* @param http angular http client
|
||||
*/
|
||||
constructor(
|
||||
private dbService: NgxIndexedDBService,
|
||||
private http: HttpClient
|
||||
) {
|
||||
super();
|
||||
console.log('PWA service initialized...');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current version of the app
|
||||
*/
|
||||
getAppVersion(): string {
|
||||
return AppConfig.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming IPC commands
|
||||
* @param type ipc command type
|
||||
* @param payload payload
|
||||
*/
|
||||
sendIpcEvent(type: string, payload?: any): void {
|
||||
if (type === PLAYLIST_PARSE) {
|
||||
const parsedPlaylist = this.parsePlaylist(payload.playlist);
|
||||
const playlistObject = this.createPlaylistObject(
|
||||
payload.title,
|
||||
parsedPlaylist,
|
||||
payload.path,
|
||||
'FILE'
|
||||
);
|
||||
|
||||
// save to db
|
||||
this.dbService
|
||||
.add(DbStores.Playlists, playlistObject)
|
||||
.subscribe(() => {
|
||||
console.log('playlist was saved to db...');
|
||||
});
|
||||
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: playlistObject,
|
||||
});
|
||||
} else if (type === PLAYLIST_GET_BY_ID) {
|
||||
this.dbService
|
||||
.getByIndex(DbStores.Playlists, '_id', payload.id)
|
||||
.subscribe((playlist) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: playlist,
|
||||
});
|
||||
});
|
||||
} else if (type === PLAYLIST_REMOVE_BY_ID) {
|
||||
this.dbService
|
||||
.delete(DbStores.Playlists, payload.id)
|
||||
.subscribe((playlist) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_REMOVE_BY_ID_RESPONSE,
|
||||
payload: playlist,
|
||||
});
|
||||
});
|
||||
} else if (type === PLAYLIST_GET_ALL) {
|
||||
this.dbService.getAll(DbStores.Playlists).subscribe((playlists) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_GET_ALL_RESPONSE,
|
||||
payload: playlists,
|
||||
});
|
||||
});
|
||||
} else if (type === PLAYLIST_SAVE_DETAILS) {
|
||||
this.dbService
|
||||
.getByID(DbStores.Playlists, payload._id)
|
||||
.pipe(
|
||||
switchMap((playlist: Playlist) => {
|
||||
return this.dbService.update(DbStores.Playlists, {
|
||||
...playlist,
|
||||
title: payload.title,
|
||||
});
|
||||
})
|
||||
)
|
||||
.subscribe((playlists) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_GET_ALL_RESPONSE,
|
||||
payload: playlists,
|
||||
});
|
||||
});
|
||||
} else if (type === PLAYLIST_UPDATE_FAVORITES) {
|
||||
this.dbService
|
||||
.getByID(DbStores.Playlists, payload.id)
|
||||
.pipe(
|
||||
switchMap((playlist: Playlist) => {
|
||||
return this.dbService.update(DbStores.Playlists, {
|
||||
...playlist,
|
||||
favorites: payload.favorites,
|
||||
});
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
console.log('favorites were updated...');
|
||||
});
|
||||
} else if (type === PLAYLIST_PARSE_BY_URL) {
|
||||
this.fetchFromUrl(payload);
|
||||
} else if (type === PLAYLIST_UPDATE) {
|
||||
this.http
|
||||
.get(payload.url, { responseType: 'text' })
|
||||
.subscribe((response: any) => {
|
||||
const refreshedPlaylist =
|
||||
this.convertFileStringToPlaylist(response);
|
||||
|
||||
this.dbService
|
||||
.getByID(DbStores.Playlists, payload.id)
|
||||
.pipe(
|
||||
switchMap((currentPlaylist: Playlist) => {
|
||||
return this.dbService.update(
|
||||
DbStores.Playlists,
|
||||
{
|
||||
...currentPlaylist,
|
||||
...refreshedPlaylist,
|
||||
count: refreshedPlaylist.items.length,
|
||||
updateDate: Date.now(),
|
||||
updateState:
|
||||
PlaylistUpdateState.UPDATED,
|
||||
}
|
||||
);
|
||||
})
|
||||
)
|
||||
.subscribe((playlists) => {
|
||||
console.log('playlist was updated...');
|
||||
window.postMessage({
|
||||
type: PLAYLIST_GET_ALL_RESPONSE,
|
||||
payload: playlists,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches playlist from the specified url
|
||||
* @param payload playlist payload
|
||||
*/
|
||||
fetchFromUrl(payload: Partial<Playlist>): void {
|
||||
this.http
|
||||
.get(payload.url, { responseType: 'text' })
|
||||
.subscribe((response: any) => {
|
||||
const parsedPlaylist =
|
||||
this.convertFileStringToPlaylist(response);
|
||||
const playlistObject = this.createPlaylistObject(
|
||||
payload.title,
|
||||
parsedPlaylist,
|
||||
payload.url,
|
||||
'URL'
|
||||
);
|
||||
|
||||
// save to db
|
||||
this.dbService
|
||||
.add(DbStores.Playlists, playlistObject)
|
||||
.subscribe(() => {
|
||||
console.log('playlist was added...');
|
||||
});
|
||||
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: playlistObject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts file string to playlist object
|
||||
* @param m3uString
|
||||
*/
|
||||
convertFileStringToPlaylist(m3uString: string): ParsedPlaylist {
|
||||
return this.parsePlaylist(m3uString.split('\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves playlist to the localStorage
|
||||
* @param name name of the playlist
|
||||
* @param playlist playlist to save
|
||||
* @param urlOrPath absolute fs path or url of the playlist
|
||||
* @param uploadType upload type - by file or via an url
|
||||
*/
|
||||
createPlaylistObject(
|
||||
name: string,
|
||||
playlist: ParsedPlaylist,
|
||||
urlOrPath?: string,
|
||||
uploadType?: 'URL' | 'FILE'
|
||||
): Playlist {
|
||||
const id = guid();
|
||||
return {
|
||||
id,
|
||||
_id: id,
|
||||
filename: name,
|
||||
title: name,
|
||||
count: playlist.items.length,
|
||||
playlist: {
|
||||
...playlist,
|
||||
items: playlist.items.map((item) => ({
|
||||
id: guid(),
|
||||
...item,
|
||||
})),
|
||||
},
|
||||
importDate: new Date().toISOString(),
|
||||
lastUsage: new Date().toISOString(),
|
||||
favorites: [],
|
||||
autoRefresh: false,
|
||||
...(uploadType === 'URL' ? { url: urlOrPath } : {}),
|
||||
...(uploadType === 'FILE' ? { filePath: urlOrPath } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses string based array to playlist object
|
||||
* @param m3uArray m3u playlist as array with strings
|
||||
*/
|
||||
parsePlaylist(m3uArray: string[]): ParsedPlaylist {
|
||||
const playlistAsString = m3uArray.join('\n');
|
||||
return parse(playlistAsString);
|
||||
}
|
||||
|
||||
removeAllListeners(): void {
|
||||
// not implemented
|
||||
}
|
||||
|
||||
listenOn(command: string, callback: (...args: any[]) => void): void {
|
||||
window.addEventListener('message', callback);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ describe('Service: Settings', () => {
|
||||
[SettingsService, StorageMap],
|
||||
(service: SettingsService, storage: StorageMap) => {
|
||||
const version = '2.1.0';
|
||||
spyOn(storage, 'set').and.returnValue(of([]));
|
||||
jest.spyOn(storage, 'set').mockReturnValue(of([] as any));
|
||||
service.setValueToLocalStorage(STORE_KEY.Version, version);
|
||||
expect(storage.set).toHaveBeenCalledWith(
|
||||
STORE_KEY.Version,
|
||||
@@ -48,10 +48,14 @@ describe('Service: Settings', () => {
|
||||
describe('Test theme switch', () => {
|
||||
let spyOnAdd, spyOnRemove;
|
||||
beforeEach(() => {
|
||||
spyOnAdd = spyOn(document.body.classList, 'add');
|
||||
spyOnRemove = spyOn(document.body.classList, 'remove');
|
||||
spyOnAdd = jest.spyOn(document.body.classList, 'add');
|
||||
spyOnRemove = jest.spyOn(document.body.classList, 'remove');
|
||||
});
|
||||
|
||||
/* afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
}); */
|
||||
|
||||
it('should switch to the dark theme', inject(
|
||||
[SettingsService],
|
||||
(service: SettingsService) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('Service: WhatsNew', () => {
|
||||
it('should change the state', inject(
|
||||
[WhatsNewService],
|
||||
(service: WhatsNewService) => {
|
||||
spyOn(service.dialogState$, 'next');
|
||||
jest.spyOn(service.dialogState$, 'next');
|
||||
service.changeDialogVisibleState(true);
|
||||
expect(service.dialogState$.next).toHaveBeenCalledTimes(1);
|
||||
expect(service.dialogState$.next).toHaveBeenCalledWith(true);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<mat-list>
|
||||
<mat-list-item>
|
||||
<mat-list-item *ngIf="isElectron">
|
||||
<div matLine fxLayout="row">
|
||||
<p fxFlex>{{ 'SETTINGS.EPG_URL_LABEL' | translate }}</p>
|
||||
<p fxFlex fxLayoutAlign="start center">
|
||||
@@ -138,6 +138,7 @@
|
||||
</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-card-actions>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { ElectronService } from './../services/electron.service';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { ElectronServiceStub } from '../services/electron.service.stub';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -47,7 +47,7 @@ const DEFAULT_SETTINGS = {
|
||||
describe('SettingsComponent', () => {
|
||||
let component: SettingsComponent;
|
||||
let fixture: ComponentFixture<SettingsComponent>;
|
||||
let electronService: ElectronService;
|
||||
let electronService: DataService;
|
||||
let router: Router;
|
||||
let storage: StorageMap;
|
||||
let translate: TranslateService;
|
||||
@@ -67,7 +67,7 @@ describe('SettingsComponent', () => {
|
||||
provide: TranslateService,
|
||||
useClass: TranslateServiceStub,
|
||||
},
|
||||
{ provide: ElectronService, useClass: ElectronServiceStub },
|
||||
{ provide: DataService, useClass: ElectronServiceStub },
|
||||
{
|
||||
provide: Router,
|
||||
useClass: MockRouter,
|
||||
@@ -93,7 +93,7 @@ describe('SettingsComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SettingsComponent);
|
||||
electronService = TestBed.inject(ElectronService);
|
||||
electronService = TestBed.inject(DataService);
|
||||
storage = TestBed.inject(StorageMap);
|
||||
router = TestBed.inject(Router);
|
||||
translate = TestBed.inject(TranslateService);
|
||||
@@ -110,12 +110,12 @@ describe('SettingsComponent', () => {
|
||||
let spyOnStorageGet;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOnStorageGet = spyOn(storage, 'get');
|
||||
spyOnStorageGet = jest.spyOn(storage, 'get');
|
||||
});
|
||||
|
||||
it('should init default settings if previous config was not saved', () => {
|
||||
spyOnStorageGet.and.returnValue(of(null));
|
||||
spyOn(component.settingsForm, 'setValue');
|
||||
spyOnStorageGet.mockReturnValue(of(null));
|
||||
jest.spyOn(component.settingsForm, 'setValue');
|
||||
component.ngOnInit();
|
||||
expect(storage.get).toHaveBeenCalled();
|
||||
expect(component.settingsForm.setValue).toHaveBeenCalledTimes(0);
|
||||
@@ -123,14 +123,14 @@ describe('SettingsComponent', () => {
|
||||
});
|
||||
|
||||
it('should call set value function if custom config exists', () => {
|
||||
spyOnStorageGet.and.returnValue(of(settings));
|
||||
spyOn(component.settingsForm, 'setValue');
|
||||
spyOnStorageGet.mockReturnValue(of(settings));
|
||||
jest.spyOn(component.settingsForm, 'setValue');
|
||||
component.ngOnInit();
|
||||
expect(component.settingsForm.setValue).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should get and apply custom settings', () => {
|
||||
spyOnStorageGet.and.returnValue(of(settings));
|
||||
spyOnStorageGet.mockReturnValue(of(settings));
|
||||
component.ngOnInit();
|
||||
expect(storage.get).toHaveBeenCalled();
|
||||
expect(component.settingsForm.value).toEqual({
|
||||
@@ -145,7 +145,7 @@ describe('SettingsComponent', () => {
|
||||
const currentVersion = '0.1.0';
|
||||
|
||||
it('should return true if version is outdated', () => {
|
||||
spyOn(electronService, 'getAppVersion').and.returnValue(
|
||||
jest.spyOn(electronService, 'getAppVersion').mockReturnValue(
|
||||
currentVersion
|
||||
);
|
||||
const isOutdated =
|
||||
@@ -154,8 +154,8 @@ describe('SettingsComponent', () => {
|
||||
});
|
||||
|
||||
it('should update notification message if version is outdated', () => {
|
||||
spyOn(translate, 'instant');
|
||||
spyOn(electronService, 'getAppVersion').and.returnValue(
|
||||
jest.spyOn(translate, 'instant');
|
||||
jest.spyOn(electronService, 'getAppVersion').mockReturnValue(
|
||||
currentVersion
|
||||
);
|
||||
component.showVersionInformation(currentVersion);
|
||||
@@ -165,7 +165,7 @@ describe('SettingsComponent', () => {
|
||||
});
|
||||
|
||||
it('should send epg fetch command', () => {
|
||||
spyOn(electronService, 'sendIpcEvent');
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.fetchEpg();
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(EPG_FETCH, {
|
||||
url: '',
|
||||
@@ -173,13 +173,13 @@ describe('SettingsComponent', () => {
|
||||
});
|
||||
|
||||
it('should navigate back to home page', () => {
|
||||
spyOn(router, 'navigateByUrl');
|
||||
jest.spyOn(router, 'navigateByUrl');
|
||||
component.backToHome();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should save settings on submit', () => {
|
||||
spyOn(storage, 'set').and.returnValue(of([]));
|
||||
jest.spyOn(storage, 'set').mockReturnValue(of([] as any));
|
||||
component.onSubmit();
|
||||
expect(storage.set).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -9,13 +9,13 @@ import { STORE_KEY } from '../shared/enums/store-keys.enum';
|
||||
import { Settings, VideoPlayer } from './settings.interface';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import * as semver from 'semver';
|
||||
import { ElectronService } from '../services/electron.service';
|
||||
import { ChannelQuery } from '../state';
|
||||
import { EPG_FETCH } from '../../../shared/ipc-commands';
|
||||
import { Language } from './language.enum';
|
||||
import { Theme } from './theme.enum';
|
||||
import { SettingsService } from './../services/settings.service';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { DataService } from '../services/data.service';
|
||||
|
||||
/** Url of the package.json file in the app repository, required to get the version of the released app */
|
||||
const PACKAGE_JSON_URL =
|
||||
@@ -56,6 +56,9 @@ export class SettingsComponent implements OnInit {
|
||||
(store) => store.epgAvailable
|
||||
);
|
||||
|
||||
/** Flag that indicates whether the app runs in electron environment */
|
||||
isElectron = this.electronService.isElectron;
|
||||
|
||||
/** All available visual themes */
|
||||
themeEnum = Theme;
|
||||
|
||||
@@ -74,7 +77,7 @@ export class SettingsComponent implements OnInit {
|
||||
*/
|
||||
constructor(
|
||||
private channelQuery: ChannelQuery,
|
||||
private electronService: ElectronService,
|
||||
private electronService: DataService,
|
||||
private formBuilder: FormBuilder,
|
||||
private http: HttpClient,
|
||||
private router: Router,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';
|
||||
import * as moment from 'moment';
|
||||
import {
|
||||
CHANNEL_SET_USER_AGENT,
|
||||
EPG_GET_PROGRAM,
|
||||
PLAYLIST_UPDATE_FAVORITES,
|
||||
} from '../../../shared/ipc-commands';
|
||||
import { ElectronService } from '../services/electron.service';
|
||||
import { Channel } from './channel.model';
|
||||
import * as moment from 'moment';
|
||||
import { EpgProgram } from '../player/models/epg-program.model';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { Channel } from './channel.model';
|
||||
|
||||
export interface ChannelState extends EntityState<Channel> {
|
||||
active: Channel;
|
||||
@@ -24,7 +25,7 @@ export class ChannelStore extends EntityStore<ChannelState> {
|
||||
* Creates an instance of ChannelStore
|
||||
* @param electronService electron service
|
||||
*/
|
||||
constructor(private electronService: ElectronService) {
|
||||
constructor(private electronService: DataService) {
|
||||
super({
|
||||
active: undefined,
|
||||
currentEpgProgram: undefined,
|
||||
@@ -48,7 +49,7 @@ export class ChannelStore extends EntityStore<ChannelState> {
|
||||
} else {
|
||||
favorites = [...store.favorites, channel.id];
|
||||
}
|
||||
this.electronService.ipcRenderer.send('update-favorites', {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_UPDATE_FAVORITES, {
|
||||
id: store.playlistId,
|
||||
favorites,
|
||||
});
|
||||
@@ -63,17 +64,14 @@ export class ChannelStore extends EntityStore<ChannelState> {
|
||||
setActiveChannel(channel: Channel): void {
|
||||
this.update((store) => {
|
||||
if (store.epgAvailable) {
|
||||
this.electronService.ipcRenderer.send(EPG_GET_PROGRAM, {
|
||||
this.electronService.sendIpcEvent(EPG_GET_PROGRAM, {
|
||||
channel,
|
||||
});
|
||||
if (channel.http['user-agent']) {
|
||||
this.electronService.ipcRenderer.send(
|
||||
CHANNEL_SET_USER_AGENT,
|
||||
{
|
||||
referer: channel.http.referrer,
|
||||
userAgent: channel.http['user-agent'],
|
||||
}
|
||||
);
|
||||
this.electronService.sendIpcEvent(CHANNEL_SET_USER_AGENT, {
|
||||
referer: channel.http.referrer,
|
||||
userAgent: channel.http['user-agent'],
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -117,7 +115,7 @@ export class ChannelStore extends EntityStore<ChannelState> {
|
||||
setEpgAvailableFlag(value: boolean): void {
|
||||
this.update((store) => {
|
||||
if (store.active && store.active.name) {
|
||||
this.electronService.ipcRenderer.send(EPG_GET_PROGRAM, {
|
||||
this.electronService.sendIpcEvent(EPG_GET_PROGRAM, {
|
||||
channelName: store.active.name,
|
||||
});
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -6,4 +6,5 @@
|
||||
export const AppConfig = {
|
||||
production: false,
|
||||
environment: 'DEV',
|
||||
version: require('../../package.json').version,
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export const AppConfig = {
|
||||
production: true,
|
||||
environment: 'PROD',
|
||||
version: require('../../package.json').version,
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export const AppConfig = {
|
||||
production: false,
|
||||
environment: 'LOCAL',
|
||||
version: require('../../package.json').version,
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export const AppConfig = {
|
||||
production: false,
|
||||
environment: 'WEB',
|
||||
version: require('../../package.json').version,
|
||||
};
|
||||
@@ -6,7 +6,14 @@
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<link rel="apple-touch-icon" href="assets/icons/apple-touch-icon.png"/>
|
||||
<link rel="icon" type="image/x-icon" href="assets/icons/favicon.ico" />
|
||||
<script>
|
||||
if (global === undefined) {
|
||||
var global = window;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="mat-app-background">
|
||||
<app-root></app-root>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "iptvnator",
|
||||
"description": "IPTVnator is a tool to watch and manage your IPTV playlists.",
|
||||
"short_name": "iptvnator",
|
||||
"theme_color": "#000",
|
||||
"background_color": "#fafafa",
|
||||
"display": "standalone",
|
||||
"scope": "./",
|
||||
"start_url": "./",
|
||||
"icons": [
|
||||
{
|
||||
"src": "assets/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/android-chrome-maskable-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/android-chrome-maskable-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
import 'core-js/es/reflect';
|
||||
import 'zone.js';
|
||||
+1
-12
@@ -18,16 +18,6 @@
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
@@ -55,8 +45,7 @@
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js'; // Included with Angular CLI.
|
||||
|
||||
import 'zone.js'; // Included with Angular CLI.
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import 'jest-preset-angular/setup-jest';
|
||||
@@ -1 +0,0 @@
|
||||
import 'jest-preset-angular';
|
||||
+2
-2
@@ -49,11 +49,11 @@ textarea,
|
||||
.dark-theme {
|
||||
::-webkit-scrollbar {
|
||||
overflow-y: scroll;
|
||||
background: rgb(100, 100, 100);
|
||||
background: rgb(100 100 100);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(60, 60, 60);
|
||||
background-color: rgb(60 60 60);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -20,7 +20,8 @@
|
||||
"api.ts",
|
||||
"epg-worker.ts",
|
||||
"main.ts",
|
||||
"menu.ts"
|
||||
"menu.ts",
|
||||
"preload.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
Reference in new issue
Block a user