From 0f1662bbe8e3844aa36fd77279cb7974940d8a88 Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Tue, 14 Dec 2021 07:55:40 +0000 Subject: [PATCH] refactor: add pwa service --- src/app/app.component.spec.ts | 16 +++--- src/app/app.component.ts | 30 ++++------- src/app/app.module.ts | 21 +++++++- src/app/home/home.component.spec.ts | 18 +++---- src/app/home/home.component.ts | 28 +++++------ .../playlist-info.component.spec.ts | 4 +- .../playlist-info/playlist-info.component.ts | 2 +- .../epg-list/epg-list.component.spec.ts | 12 ++--- .../components/epg-list/epg-list.component.ts | 6 +-- src/app/services/data.service.ts | 9 ++++ src/app/services/electron.service.stub.ts | 8 +-- src/app/services/electron.service.ts | 50 +++++++++++++------ src/app/services/pwa.service.ts | 30 +++++++++++ src/app/state/channel.store.ts | 17 +++---- 14 files changed, 156 insertions(+), 95 deletions(-) create mode 100644 src/app/services/data.service.ts create mode 100644 src/app/services/pwa.service.ts diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 9da315e03..e2fddadae 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -106,17 +106,15 @@ describe('AppComponent', () => { describe('Test ipc listeners and commands', () => { it('should set IPC listeners', () => { - spyOn(electronService.ipcRenderer, 'on'); + 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'); + spyOn(electronService, 'removeAllListeners'); component.ngOnDestroy(); - expect( - electronService.ipcRenderer.removeAllListeners - ).toHaveBeenCalledTimes(4); + expect(electronService.removeAllListeners).toHaveBeenCalledTimes(4); }); it('should navigate to the provided route', inject( @@ -245,7 +243,7 @@ describe('AppComponent', () => { 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); }); @@ -259,7 +257,7 @@ describe('AppComponent', () => { 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); }); @@ -273,7 +271,7 @@ describe('AppComponent', () => { 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); }); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4479625ff..8278e6911 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,7 +1,5 @@ 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'; @@ -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,7 +61,7 @@ export class AppComponent { */ constructor( private channelStore: ChannelStore, - private electronService: ElectronService, + private electronService: DataService, private ngZone: NgZone, private router: Router, private translate: TranslateService, @@ -78,7 +70,7 @@ export class AppComponent { private storage: StorageMap, private whatsNewService: WhatsNewService ) { - if ( + /* if ( (this.electronService.remote.process.platform === 'linux' || this.electronService.remote.process.platform === 'win32') && this.electronService.remote.process.argv.length > 2 @@ -90,12 +82,12 @@ 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, }); } - } + } */ } /** @@ -114,7 +106,7 @@ export class AppComponent { */ setRendererListeners(): void { this.commandsList.forEach((command) => - this.electronService.ipcRenderer.on(command.id, () => + this.electronService.listenOn(command.id, () => this.ngZone.run(() => command.callback()) ) ); @@ -131,7 +123,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 +221,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); } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 112eda87b..3a25becf0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -14,12 +14,25 @@ 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'; // AoT requires an exported function for factories export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { return new TranslateHttpLoader(http, './assets/i18n/', '.json'); } +function isElectron(): boolean { + return !!(window && window.process && window.process.type); +} + +export function AuthenticationFactory() { + if (isElectron()) { + return new ElectronService(); + } + return new PwaService(); +} + @NgModule({ declarations: [AppComponent], imports: [ @@ -37,6 +50,12 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { }, }), ], + providers: [ + { + provide: DataService, + useFactory: AuthenticationFactory, + }, + ], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/src/app/home/home.component.spec.ts b/src/app/home/home.component.spec.ts index fdf0f3904..ab0ac6cb4 100644 --- a/src/app/home/home.component.spec.ts +++ b/src/app/home/home.component.spec.ts @@ -83,7 +83,7 @@ describe('HomeComponent', () => { const playlistId = '12345'; spyOn(electronService.ipcRenderer, 'send'); component.removePlaylist(playlistId); - expect(electronService.ipcRenderer.send).toHaveBeenCalledWith( + expect(electronService.sendIpcEvent).toHaveBeenCalledWith( 'playlist-remove-by-id', { id: playlistId } ); @@ -96,7 +96,7 @@ describe('HomeComponent', () => { } as PlaylistMeta; spyOn(electronService.ipcRenderer, 'send'); component.refreshPlaylist(playlistMeta); - expect(electronService.ipcRenderer.send).toHaveBeenCalledWith( + expect(electronService.sendIpcEvent).toHaveBeenCalledWith( PLAYLIST_UPDATE, { id: playlistMeta._id, filePath: playlistMeta.filePath } ); @@ -106,7 +106,7 @@ describe('HomeComponent', () => { const playlistId = '6789'; spyOn(electronService.ipcRenderer, 'send'); component.getPlaylist(playlistId); - expect(electronService.ipcRenderer.send).toHaveBeenCalledWith( + expect(electronService.sendIpcEvent).toHaveBeenCalledWith( 'playlist-by-id', { id: playlistId, @@ -119,7 +119,7 @@ describe('HomeComponent', () => { const playlistUrl = 'http://test.com/' + playlistTitle; spyOn(electronService.ipcRenderer, 'send'); component.sendPlaylistsUrl(playlistUrl); - expect(electronService.ipcRenderer.send).toHaveBeenCalledWith( + expect(electronService.sendIpcEvent).toHaveBeenCalledWith( 'parse-playlist-by-url', { title: playlistTitle, @@ -141,7 +141,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 } ); @@ -184,10 +184,10 @@ describe('HomeComponent', () => { }); it('should remove all ipc listeners on destroy', () => { - spyOn(electronService.ipcRenderer, 'removeAllListeners'); + spyOn(electronService, 'removeAllListeners'); component.ngOnDestroy(); - expect( - electronService.ipcRenderer.removeAllListeners - ).toHaveBeenCalledTimes(component.commandsList.length); + expect(electronService.removeAllListeners).toHaveBeenCalledTimes( + component.commandsList.length + ); }); }); diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 971f2def0..5b17ecc65 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -14,6 +14,7 @@ import { 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< @@ -60,7 +61,7 @@ export class HomeComponent { this.snackBar.open('Done! Playlist was removed.', null, { duration: 2000, }); - this.electronService.ipcRenderer.send('playlists-all'); + // this.electronService.sendIpcEvent('playlists-all'); }, }, { @@ -90,14 +91,14 @@ export class HomeComponent { constructor( private channelStore: ChannelStore, private dialogService: DialogService, - private electronService: ElectronService, + private electronService: DataService, private ngZone: NgZone, private router: Router, private snackBar: MatSnackBar, private translate: TranslateService ) { // get all playlists - this.electronService.ipcRenderer.send('playlists-all'); + this.electronService.sendIpcEvent('playlists-all'); // set all renderer listeners this.setRendererListeners(); } @@ -107,12 +108,9 @@ export class HomeComponent { */ setRendererListeners(): void { this.commandsList.forEach((command) => { - this.electronService.ipcRenderer.on( - command.id, - (event, response) => { - this.ngZone.run(() => command.execute(response)); - } - ); + this.electronService.listenOn(command.id, (event, response) => { + this.ngZone.run(() => command.execute(response)); + }); }); } @@ -135,7 +133,7 @@ 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, @@ -156,7 +154,7 @@ export class HomeComponent { */ sendPlaylistsUrl(playlistUrl: string): void { this.isLoading = true; - this.electronService.ipcRenderer.send('parse-playlist-by-url', { + this.electronService.sendIpcEvent('parse-playlist-by-url', { title: this.getLastUrlSegment(playlistUrl), url: playlistUrl, }); @@ -201,7 +199,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 +209,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 +220,7 @@ export class HomeComponent { * @param playlistId playlist id */ getPlaylist(playlistId: string): void { - this.electronService.ipcRenderer.send('playlist-by-id', { + this.electronService.sendIpcEvent('playlist-by-id', { id: playlistId, }); } @@ -255,7 +253,7 @@ export class HomeComponent { */ ngOnDestroy(): void { this.commandsList.forEach((command) => - this.electronService.ipcRenderer.removeAllListeners(command.id) + this.electronService.removeAllListeners(command.id) ); } } diff --git a/src/app/home/recent-playlists/playlist-info/playlist-info.component.spec.ts b/src/app/home/recent-playlists/playlist-info/playlist-info.component.spec.ts index 8fcae30ba..4f430d5ea 100644 --- a/src/app/home/recent-playlists/playlist-info/playlist-info.component.spec.ts +++ b/src/app/home/recent-playlists/playlist-info/playlist-info.component.spec.ts @@ -57,8 +57,8 @@ describe('PlaylistInfoComponent', () => { const playlistToSave = { _id: 'a12345', title: 'Playlist' } as Playlist; spyOn(electronService.ipcRenderer, 'send'); 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 ); diff --git a/src/app/home/recent-playlists/playlist-info/playlist-info.component.ts b/src/app/home/recent-playlists/playlist-info/playlist-info.component.ts index 472d91f79..d4b19a0df 100644 --- a/src/app/home/recent-playlists/playlist-info/playlist-info.component.ts +++ b/src/app/home/recent-playlists/playlist-info/playlist-info.component.ts @@ -76,6 +76,6 @@ export class PlaylistInfoComponent { saveChanges( data: Pick ): void { - this.electronService.ipcRenderer.send(PLAYLIST_SAVE_DETAILS, data); + this.electronService.sendIpcEvent(PLAYLIST_SAVE_DETAILS, data); } } diff --git a/src/app/player/components/epg-list/epg-list.component.spec.ts b/src/app/player/components/epg-list/epg-list.component.spec.ts index 8f45dda88..861fab1d2 100644 --- a/src/app/player/components/epg-list/epg-list.component.spec.ts +++ b/src/app/player/components/epg-list/epg-list.component.spec.ts @@ -148,14 +148,12 @@ describe('EpgListComponent', () => { }); it('should remove ipc listeners on destroy', () => { - spyOn(electronService.ipcRenderer, 'removeAllListeners'); + 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', () => { diff --git a/src/app/player/components/epg-list/epg-list.component.ts b/src/app/player/components/epg-list/epg-list.component.ts index c381fc743..865a4df2b 100644 --- a/src/app/player/components/epg-list/epg-list.component.ts +++ b/src/app/player/components/epg-list/epg-list.component.ts @@ -61,7 +61,7 @@ export class EpgListComponent { private electronService: ElectronService, private ngZone: NgZone ) { - this.electronService.ipcRenderer.on( + this.electronService.listenOn( EPG_GET_PROGRAM_DONE, (event, response) => { this.ngZone.run(() => this.handleEpgData(response)); @@ -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); } } diff --git a/src/app/services/data.service.ts b/src/app/services/data.service.ts new file mode 100644 index 000000000..2a7a5c54d --- /dev/null +++ b/src/app/services/data.service.ts @@ -0,0 +1,9 @@ +export abstract class DataService { + abstract getAppVersion(): string; + abstract sendIpcEvent(type: string, payload?: unknown): void; + abstract removeAllListeners(type: string): void; + abstract listenOn( + command: string, + callback: (...args: any[]) => void + ): void; +} diff --git a/src/app/services/electron.service.stub.ts b/src/app/services/electron.service.stub.ts index 0e496b3be..ebe6db40d 100644 --- a/src/app/services/electron.service.stub.ts +++ b/src/app/services/electron.service.stub.ts @@ -11,14 +11,14 @@ export class ElectronServiceStub { }, app: { getVersion: jest.fn(() => '1.0.0'), - } + }, }; getAppVersion() { return this.remote.app.getVersion(); } - sendIpcEvent() { - this.ipcRenderer.send(); + sendIpcEvent() { + this.sendIpcEvent(); } -} \ No newline at end of file +} diff --git a/src/app/services/electron.service.ts b/src/app/services/electron.service.ts index d6309ce43..499a18e51 100644 --- a/src/app/services/electron.service.ts +++ b/src/app/services/electron.service.ts @@ -1,33 +1,43 @@ import { Injectable } from '@angular/core'; +import { DataService } from './data.service'; // 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 { ipcRenderer, remote /* webFrame */ } from 'electron'; +/* import * as childProcess from 'child_process'; +import * as fs from 'fs'; */ +// import { Titlebar, Color } from 'custom-electron-titlebar'; @Injectable({ providedIn: 'root', }) -export class ElectronService { - ipcRenderer: typeof ipcRenderer; - webFrame: typeof webFrame; - remote: typeof remote; - childProcess: typeof childProcess; - fs: typeof fs; +export class ElectronService extends DataService { + private ipcRenderer: typeof ipcRenderer; + // webFrame: typeof webFrame; + private remote: typeof remote; + // childProcess: typeof childProcess; + // fs: typeof fs; get isElectron(): boolean { return !!(window && window.process && window.process.type); } constructor() { + super(); // 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'); + /* this.webFrame = window.require('electron').webFrame */ // need to test whether remote is available + /* this.childProcess = window.require('child_process'); + this.fs = window.require('fs'); */ + + // create custom title bar + /* new Titlebar({ + backgroundColor: Color.fromHex('#000'), + itemBackgroundColor: Color.fromHex('#222'), + enableMnemonics: true, + }); */ } } @@ -43,7 +53,19 @@ export class ElectronService { * @param type event type * @param payload data payload */ - sendIpcEvent(type: string, payload: unknown): void { - this.ipcRenderer.send(type, payload); + sendIpcEvent(type: string, payload?: unknown): void { + this.sendIpcEvent(type, payload); + } + + /** + * Removes all IPC listeners + * @param type command name + */ + removeAllListeners(type: string): void { + this.ipcRenderer.removeAllListeners(type); + } + + listenOn(command: string, callback: (...args: any[]) => void): void { + this.ipcRenderer.on(command, callback); } } diff --git a/src/app/services/pwa.service.ts b/src/app/services/pwa.service.ts new file mode 100644 index 000000000..272b9be75 --- /dev/null +++ b/src/app/services/pwa.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; +import { DataService } from './data.service'; + +@Injectable({ + providedIn: 'root', +}) +export class PwaService extends DataService { + constructor() { + super(); + console.log('PWA Service'); + } + + getAppVersion(): string { + return '1.0.0'; + //throw new Error('Method not implemented.'); + } + + sendIpcEvent(type: string, payload?: unknown): void { + console.log(type, payload); + // throw new Error('Method not implemented.'); + } + + removeAllListeners(type: string) { + console.log(type + ' listeners removed'); + } + + listenOn(command: string, callback: (...args: any[]) => void): void { + console.log('listen on ' + command); + } +} diff --git a/src/app/state/channel.store.ts b/src/app/state/channel.store.ts index 4673b6db7..6cc530663 100644 --- a/src/app/state/channel.store.ts +++ b/src/app/state/channel.store.ts @@ -48,7 +48,7 @@ export class ChannelStore extends EntityStore { } else { favorites = [...store.favorites, channel.id]; } - this.electronService.ipcRenderer.send('update-favorites', { + this.electronService.sendIpcEvent('update-favorites', { id: store.playlistId, favorites, }); @@ -63,17 +63,14 @@ export class ChannelStore extends EntityStore { setActiveChannel(channel: Channel): void { this.update((store) => { if (store.epgAvailable) { - this.electronService.ipcRenderer.send(EPG_GET_PROGRAM, { + this.electronService.sendIpcEvent(EPG_GET_PROGRAM, { channelName: channel.name, }); 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 +114,7 @@ export class ChannelStore extends EntityStore { 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, }); }