diff --git a/menu.ts b/menu.ts index d37e50e49..55ad87241 100644 --- a/menu.ts +++ b/menu.ts @@ -1,6 +1,6 @@ import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron'; import * as path from 'path'; -import { SHOW_WHATS_NEW } 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 { @@ -48,14 +48,14 @@ export class AppMenu { { label: 'Add playlist', click: () => - this.window.webContents.send('add-playlist-view'), + this.window.webContents.send(VIEW_ADD_PLAYLIST), }, { type: 'separator', }, { label: 'Settings', - click: () => this.window.webContents.send('settings-view'), + click: () => this.window.webContents.send(VIEW_SETTINGS), }, { type: 'separator', diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 846c3243a..fada1603a 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,12 +1,30 @@ import { NgxWhatsNewModule } from 'ngx-whats-new'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { + ComponentFixture, + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; -import { TranslateModule, TranslatePipe } from '@ngx-translate/core'; +import { + TranslateModule, + TranslatePipe, + TranslateService, +} from '@ngx-translate/core'; import { ElectronService } from './services/electron.service'; import { ElectronServiceStub } from './services/electron.service.stub'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MockModule, MockPipe } from 'ng-mocks'; +import { of } from 'rxjs'; +import { WhatsNewService } from './services/whats-new.service'; +import { Theme } from './settings/theme.enum'; +import { SettingsService } from './services/settings.service'; +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'; +import { IpcCommand } from '../../shared/ipc-command.class'; jest.mock('custom-electron-titlebar', () => { return { @@ -24,6 +42,14 @@ class MatSnackBarStub { } describe('AppComponent', () => { + let component: AppComponent; + let electronService: ElectronService; + let fixture: ComponentFixture; + let settingsService: SettingsService; + let translateService: TranslateService; + let whatsNewService: WhatsNewService; + const defaultLanguage = 'en'; + beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ @@ -31,6 +57,9 @@ describe('AppComponent', () => { providers: [ { provide: ElectronService, useClass: ElectronServiceStub }, { provide: MatSnackBar, useClass: MatSnackBarStub }, + { provide: WhatsNewService, useClass: WhatsNewServiceStub }, + TranslateService, + SettingsService, // TODO: stub ], imports: [ MockModule(MatSnackBarModule), @@ -42,12 +71,212 @@ describe('AppComponent', () => { }) ); - it( - 'should create the app', - waitForAsync(() => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app).toBeTruthy(); - }) - ); + beforeEach(() => { + electronService = TestBed.inject(ElectronService); + fixture = TestBed.createComponent(AppComponent); + settingsService = TestBed.inject(SettingsService); + translateService = TestBed.inject(TranslateService); + whatsNewService = TestBed.inject(WhatsNewService); + component = fixture.componentInstance; + component.modals = []; + fixture.detectChanges(); + }); + + it('should create the component and set default language', () => { + spyOn(translateService, 'setDefaultLang'); + spyOn(component, 'setRendererListeners'); + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + expect(component.commandsList.length).toEqual(5); + expect(IpcCommand).toHaveBeenCalledTimes(5); + }); + + it('should init component', () => { + spyOn(translateService, 'setDefaultLang'); + spyOn(component, 'setRendererListeners'); + spyOn(component, 'initSettings'); + spyOn(component, 'handleWhatsNewDialog'); + component.ngOnInit(); + expect(translateService.setDefaultLang).toHaveBeenCalledWith( + defaultLanguage + ); + expect(component.setRendererListeners).toHaveBeenCalledTimes(1); + expect(component.initSettings).toHaveBeenCalledTimes(1); + expect(component.handleWhatsNewDialog).toHaveBeenCalledTimes(1); + }); + + describe('Test ipc listeners and commands', () => { + it('should set IPC listeners', () => { + spyOn(electronService.ipcRenderer, 'on'); + component.setRendererListeners(); + expect(electronService.ipcRenderer.on).toHaveBeenCalledTimes(5); + }); + + it('should remove all ipc listeners on destroy', () => { + spyOn(electronService.ipcRenderer, 'removeAllListeners'); + component.ngOnDestroy(); + expect( + electronService.ipcRenderer.removeAllListeners + ).toHaveBeenCalledTimes(4); + }); + + it('should navigate to the provided route', inject( + [Router], + (router: Router) => { + const route = '/add-playlists'; + spyOn(router, 'navigateByUrl'); + component.navigateToRoute(route); + expect(router.navigateByUrl).toHaveBeenCalledTimes(1); + expect(router.navigateByUrl).toHaveBeenCalledWith( + route, + expect.anything() + ); + } + )); + + it('should show a notification on epg error', inject( + [MatSnackBar], + (snackbar: MatSnackBar) => { + spyOn(snackbar, 'open'); + component.onEpgError(); + expect(snackbar.open).toHaveBeenCalledTimes(1); + } + )); + + it('should handle epg download success', inject( + [MatSnackBar, ChannelStore], + (snackbar: MatSnackBar, channelStore: ChannelStore) => { + spyOn(snackbar, 'open'); + spyOn(channelStore, 'setEpgAvailableFlag'); + component.onEpgFetchDone(); + expect(snackbar.open).toHaveBeenCalledTimes(1); + expect(channelStore.setEpgAvailableFlag).toHaveBeenCalledWith( + true + ); + } + )); + + it('show show whats new dialog', () => { + spyOn(whatsNewService, 'getModalsByVersion'); + spyOn(component, 'setDialogVisibility'); + component.showWhatsNewDialog(); + expect(whatsNewService.getModalsByVersion).toHaveBeenCalledTimes(1); + expect(component.setDialogVisibility).toHaveBeenCalledWith(true); + }); + }); + + 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'); + + component.handleWhatsNewDialog(); + expect(spyOnSettingsGet).toHaveBeenCalled(); + expect(whatsNewService.getModalsByVersion).toHaveBeenCalled(); + expect( + whatsNewService.changeDialogVisibleState + ).toHaveBeenCalledWith(true); + }); + + 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'); + + component.handleWhatsNewDialog(); + expect(spyOnSettingsGet).toHaveBeenCalled(); + expect(whatsNewService.getModalsByVersion).toHaveBeenCalledTimes(0); + expect( + whatsNewService.changeDialogVisibleState + ).toHaveBeenCalledTimes(0); + }); + + it('should change the visibility of the whats new dialog', () => { + spyOn(whatsNewService, 'changeDialogVisibleState'); + component.modals = [{}, {}]; + const visibilityFlag = true; + component.setDialogVisibility(true); + + expect( + whatsNewService.changeDialogVisibleState + ).toHaveBeenCalledWith(visibilityFlag); + expect( + whatsNewService.changeDialogVisibleState + ).toHaveBeenCalledTimes(1); + }); + + it('should not change the visibility of the whats new dialog', () => { + spyOn(whatsNewService, 'changeDialogVisibleState'); + component.modals = []; + component.setDialogVisibility(true); + expect( + whatsNewService.changeDialogVisibleState + ).toHaveBeenCalledTimes(0); + }); + }); + + describe('Set initial settings', () => { + const theme = Theme.DarkTheme; + const language = 'es'; + const epgUrl = 'http://localhost/epg.xml'; + + beforeEach(() => { + spyOn(electronService.ipcRenderer, 'send'); + spyOn(settingsService, 'changeTheme'); + spyOn(component, 'handleWhatsNewDialog'); + spyOn(translateService, 'use'); + }); + + it('should get and init settings (all settings are defined)', () => { + const spyOnSettingsGet = spyOn( + settingsService, + 'getValueFromLocalStorage' + ).and.returnValue(of({ theme, epgUrl, language })); + + component.initSettings(); + + expect(spyOnSettingsGet).toHaveBeenCalledWith(STORE_KEY.Settings); + expect(settingsService.changeTheme).toHaveBeenCalledWith(theme); + expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(1); + expect(translateService.use).toHaveBeenCalledWith(language); + }); + + it('should get and init settings (nothing is defined)', () => { + const spyOnSettingsGet = spyOn( + settingsService, + 'getValueFromLocalStorage' + ).and.returnValue(of()); + + component.initSettings(); + + expect(spyOnSettingsGet).toHaveBeenCalledWith(STORE_KEY.Settings); + expect(settingsService.changeTheme).toHaveBeenCalledTimes(0); + expect(electronService.ipcRenderer.send).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 })); + + component.initSettings(); + + expect(spyOnSettingsGet).toHaveBeenCalledWith(STORE_KEY.Settings); + expect(settingsService.changeTheme).toHaveBeenCalledWith(theme); + expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(0); + expect(translateService.use).toHaveBeenCalledWith(defaultLanguage); + }); + }); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 045b6ade2..fd922b64d 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,7 +3,7 @@ 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 } from '@angular/material/snack-bar'; +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'; @@ -12,11 +12,15 @@ import { EPG_FETCH, EPG_FETCH_DONE, SHOW_WHATS_NEW, + VIEW_ADD_PLAYLIST, + VIEW_SETTINGS, } from '../../shared/ipc-commands'; +import { IpcCommand } from '../../shared/ipc-command.class'; import { SettingsService } from './services/settings.service'; 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({ @@ -25,6 +29,9 @@ new Titlebar({ enableMnemonics: true, }); +/** + * AppComponent + */ @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -32,10 +39,30 @@ new Titlebar({ export class AppComponent { /** Visibility flag of the "what is new" modal dialog */ isDialogVisible$ = this.whatsNewService.dialogState$; + /** Dialog options */ options = this.whatsNewService.options; + /** Modals to show for the updated version of the application */ - modals: ModalWindow[]; + modals: ModalWindow[] = []; + + /** Default options for epg snackbar notifications */ + epgSnackBarOptions: MatSnackBarConfig = { + verticalPosition: 'bottom', + horizontalPosition: 'right', + }; + + /** List of ipc commands with function mapping */ + commandsList = [ + new IpcCommand(VIEW_ADD_PLAYLIST, () => this.navigateToRoute('/')), + new IpcCommand(VIEW_SETTINGS, () => this.navigateToRoute('/settings')), + new IpcCommand(EPG_FETCH_DONE, () => this.onEpgFetchDone), + new IpcCommand(EPG_ERROR, () => this.onEpgError), + new IpcCommand(SHOW_WHATS_NEW, () => this.showWhatsNewDialog), + ]; + + /** Default language as fallback */ + DEFAULT_LANG = 'en'; /** * Creates an instance of AppComponent @@ -51,10 +78,6 @@ export class AppComponent { private storage: StorageMap, private whatsNewService: WhatsNewService ) { - this.translate.setDefaultLang('en'); - - this.setRendererListeners(); - if ( (this.electronService.remote.process.platform === 'linux' || this.electronService.remote.process.platform === 'win32') && @@ -76,76 +99,53 @@ export class AppComponent { } /** - * Initializes all necessary listeners for the events from the renderer process + * Starts all the functions to initialize the component */ - setRendererListeners(): void { - this.electronService.ipcRenderer.on('add-playlist-view', () => { - this.ngZone.run(() => { - this.router.navigateByUrl('/', { skipLocationChange: true }); - }); - }); + ngOnInit(): void { + this.translate.setDefaultLang(this.DEFAULT_LANG); - this.electronService.ipcRenderer.on('settings-view', () => { - this.ngZone.run(() => { - this.router.navigateByUrl('/settings', { - skipLocationChange: true, - }); - }); - }); - - this.electronService.ipcRenderer.on(EPG_FETCH_DONE, () => { - this.ngZone.run(() => { - this.channelStore.setEpgAvailableFlag(true); - this.snackBar.open('EPG was successfully downloaded!', null, { - duration: 2000, - verticalPosition: 'bottom', - horizontalPosition: 'right', - }); - }); - }); - - this.electronService.ipcRenderer.on(EPG_ERROR, () => { - this.snackBar.open('EPG Error: something went wrong...', null, { - duration: 2000, - verticalPosition: 'bottom', - horizontalPosition: 'right', - }); - }); - - this.electronService.ipcRenderer.on(SHOW_WHATS_NEW, () => { - this.ngZone.run(() => { - this.modals = this.whatsNewService.getModalsByVersion( - this.electronService.getAppVersion() - ); - this.setDialogVisibility(true); - }); - }); + this.setRendererListeners(); + this.initSettings(); + this.handleWhatsNewDialog(); } /** - * Subscribes for app settings on component init + * Initializes all necessary listeners for the events from the renderer process */ - ngOnInit(): void { - this.storage.get('settings').subscribe((settings: Settings) => { - if (settings && Object.keys(settings).length > 0) { - this.translate.use(settings.language ?? 'en'); - if (settings.epgUrl) { - this.electronService.ipcRenderer.send(EPG_FETCH, { - url: settings.epgUrl, - }); - this.snackBar.open('Fetch EPG data...', 'Close', { - verticalPosition: 'bottom', - horizontalPosition: 'right', - }); - } + setRendererListeners(): void { + this.commandsList.forEach((command) => + this.electronService.ipcRenderer.on(command.id, () => + this.ngZone.run(() => command.callback()) + ) + ); + } - if (settings.theme) { - this.settingsService.changeTheme(settings.theme); - } - } - }); + /** + * Reads the settings object from local storage and initializes the + * application based on them + */ + initSettings(): void { + this.settingsService + .getValueFromLocalStorage(STORE_KEY.Settings) + .subscribe((settings: Settings) => { + if (settings && Object.keys(settings).length > 0) { + this.translate.use(settings.language ?? this.DEFAULT_LANG); + if (settings.epgUrl) { + this.electronService.ipcRenderer.send(EPG_FETCH, { + url: settings.epgUrl, + }); + this.snackBar.open( + this.translate.instant('FETCH_EPG'), + this.translate.instant('CLOSE'), + this.epgSnackBarOptions + ); + } - this.handleWhatsNewDialog(); + if (settings.theme) { + this.settingsService.changeTheme(settings.theme); + } + } + }); } /** @@ -153,23 +153,23 @@ export class AppComponent { */ handleWhatsNewDialog(): void { const actualVersion = this.electronService.getAppVersion(); - this.storage.get('version').subscribe((version) => { - const isNewVersion = semver.gt(actualVersion, version || '0.0.0'); - if (!version || isNewVersion) { - this.modals = - this.whatsNewService.getModalsByVersion(actualVersion); - this.setDialogVisibility(true); - } - this.setVersion(actualVersion); - }); - } - - /** - * Updates the version of the application in localstorage - * @param version actual version of the application - */ - setVersion(version: string): void { - this.storage.set('version', version).subscribe(() => {}); + this.settingsService + .getValueFromLocalStorage(STORE_KEY.Version) + .subscribe((version) => { + const isNewVersion = semver.gt( + actualVersion, + version || '0.0.0' + ); + if (!version || isNewVersion) { + this.modals = + this.whatsNewService.getModalsByVersion(actualVersion); + this.setDialogVisibility(true); + } + this.settingsService.setValueToLocalStorage( + STORE_KEY.Version, + actualVersion + ); + }); } /** @@ -182,15 +182,56 @@ export class AppComponent { } } + /** + * Navigate to the specified route + * @param route route to navigate to + */ + navigateToRoute(route: string) { + this.router.navigateByUrl(route, { skipLocationChange: true }); + } + + /** + * Handles the event when the EPG fetching is done + */ + onEpgFetchDone(): void { + this.channelStore.setEpgAvailableFlag(true); + this.snackBar.open( + this.translate.instant('EPG.DOWNLOAD_SUCCESS'), + null, + { + ...this.epgSnackBarOptions, + duration: 2000, + } + ); + } + + /** + * Handles epg error + */ + onEpgError(): void { + this.snackBar.open(this.translate.instant('EPG.ERROR'), null, { + ...this.epgSnackBarOptions, + duration: 2000, + }); + } + + /** + * Shows the "what is new" dialog + */ + showWhatsNewDialog(): void { + this.modals = this.whatsNewService.getModalsByVersion( + this.electronService.getAppVersion() + ); + this.setDialogVisibility(true); + } + /** * 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( - 'add-playlist-view' - ); - this.electronService.ipcRenderer.removeAllListeners('settings-view'); + this.electronService.ipcRenderer.removeAllListeners(VIEW_ADD_PLAYLIST); + this.electronService.ipcRenderer.removeAllListeners(VIEW_SETTINGS); } } diff --git a/src/app/player/components/video-player/video-player.component.ts b/src/app/player/components/video-player/video-player.component.ts index 28c8ce1ee..5073e4e3d 100644 --- a/src/app/player/components/video-player/video-player.component.ts +++ b/src/app/player/components/video-player/video-player.component.ts @@ -5,8 +5,9 @@ import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { MatSidenav } from '@angular/material/sidenav'; import { StorageMap } from '@ngx-pwa/local-storage'; -import { Settings, SETTINGS_STORE_KEY, VideoPlayer } from '../../../settings/settings.interface'; +import { Settings, VideoPlayer } from '../../../settings/settings.interface'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { STORE_KEY } from '../../../shared/enums/store-keys.enum'; @Component({ selector: 'app-video-player', @@ -74,7 +75,7 @@ export class VideoPlayerComponent implements OnInit { * Reads the app configuration from the browsers storage and applies the settings in the current component */ applySettings(): void { - this.storage.get(SETTINGS_STORE_KEY).subscribe((settings: Settings) => { + this.storage.get(STORE_KEY.Settings).subscribe((settings: Settings) => { if (settings && Object.keys(settings).length > 0) { this.playerSettings = { player: settings.player, diff --git a/src/app/services/electron.service.stub.ts b/src/app/services/electron.service.stub.ts index c84c3f82f..0e496b3be 100644 --- a/src/app/services/electron.service.stub.ts +++ b/src/app/services/electron.service.stub.ts @@ -10,12 +10,12 @@ export class ElectronServiceStub { argv: [0, 1], }, app: { - getVersion: jest.fn(), + getVersion: jest.fn(() => '1.0.0'), } }; getAppVersion() { - this.remote.app.getVersion(); + return this.remote.app.getVersion(); } sendIpcEvent() { diff --git a/src/app/services/settings.service.spec.ts b/src/app/services/settings.service.spec.ts new file mode 100644 index 000000000..ec8f01f1f --- /dev/null +++ b/src/app/services/settings.service.spec.ts @@ -0,0 +1,73 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { StorageMap } from '@ngx-pwa/local-storage'; +import { of } from 'rxjs'; +import { Theme } from '../settings/theme.enum'; +import { STORE_KEY } from '../shared/enums/store-keys.enum'; +import { SettingsService } from './settings.service'; + +describe('Service: Settings', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [SettingsService], + }); + }); + + it('should create a service instance', inject( + [SettingsService], + (service: SettingsService) => { + expect(service).toBeTruthy(); + } + )); + + it('should set key/value pair', inject( + [SettingsService, StorageMap], + (service: SettingsService, storage: StorageMap) => { + const version = '2.1.0'; + spyOn(storage, 'set').and.returnValue(of([])); + service.setValueToLocalStorage(STORE_KEY.Version, version); + expect(storage.set).toHaveBeenCalledWith( + STORE_KEY.Version, + version + ); + } + )); + + it('should get value from the local storage', inject( + [SettingsService, StorageMap], + (service: SettingsService, storage: StorageMap) => { + const version = '2.1.0'; + service.setValueToLocalStorage(STORE_KEY.Version, version); + service + .getValueFromLocalStorage(STORE_KEY.Version) + .subscribe((value) => { + expect(value).toBe(version); + }); + } + )); + + describe('Test theme switch', () => { + let spyOnAdd, spyOnRemove; + beforeEach(() => { + spyOnAdd = spyOn(document.body.classList, 'add'); + spyOnRemove = spyOn(document.body.classList, 'remove'); + }); + + it('should switch to the dark theme', inject( + [SettingsService], + (service: SettingsService) => { + service.changeTheme(Theme.DarkTheme); + expect(spyOnRemove).toHaveBeenCalledTimes(0); + expect(spyOnAdd).toHaveBeenCalledTimes(1); + } + )); + + it('should switch to the light theme', inject( + [SettingsService], + (service: SettingsService) => { + service.changeTheme(Theme.LightTheme); + expect(spyOnRemove).toHaveBeenCalledTimes(1); + expect(spyOnAdd).toHaveBeenCalledTimes(0); + } + )); + }); +}); diff --git a/src/app/services/settings.service.ts b/src/app/services/settings.service.ts index 643302dc9..f02d3f10f 100644 --- a/src/app/services/settings.service.ts +++ b/src/app/services/settings.service.ts @@ -1,10 +1,15 @@ import { Theme } from './../settings/theme.enum'; import { Injectable } from '@angular/core'; +import { StorageMap } from '@ngx-pwa/local-storage'; +import { STORE_KEY } from '../shared/enums/store-keys.enum'; @Injectable({ providedIn: 'root', }) export class SettingsService { + /** Creates an instance of SettingsService */ + constructor(private storage: StorageMap) {} + /** * Changes the visual theme of the application * @param selectedTheme theme to set @@ -16,4 +21,22 @@ export class SettingsService { document.body.classList.add('dark-theme'); } } + + /** + * Returns the value of the given key from the local storage + * @param key key to get + * @returns returns the value of the given key + */ + getValueFromLocalStorage(key: STORE_KEY) { + return this.storage.get(key); + } + + /** + * Sets the given key/value pair in the local storage + * @param key key to set + * @param value value to set + */ + setValueToLocalStorage(key: STORE_KEY, value: string) { + this.storage.set(key, value).subscribe(() => {}); + } } diff --git a/src/app/services/whats-new.service.stub.ts b/src/app/services/whats-new.service.stub.ts new file mode 100644 index 000000000..c8f05b035 --- /dev/null +++ b/src/app/services/whats-new.service.stub.ts @@ -0,0 +1,6 @@ +export class WhatsNewServiceStub { + changeDialogVisibleState(): void {} + getModalsByVersion(): any { + return []; + } +} diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index 89cb4bc6d..dc8fac85c 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -5,7 +5,8 @@ import { StorageMap } from '@ngx-pwa/local-storage'; import { Router } from '@angular/router'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Observable, Subscription } from 'rxjs'; -import { Settings, SETTINGS_STORE_KEY, VideoPlayer } from './settings.interface'; +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'; @@ -59,7 +60,7 @@ export class SettingsComponent implements OnInit { themeEnum = Theme; /** - * Creates an instance of SettingsComponent and injects + * Creates an instance of SettingsComponent and injects * required dependencies into the component * @param channelQuery * @param electronService @@ -94,50 +95,54 @@ export class SettingsComponent implements OnInit { } /** - * Reads the config object from the browsers + * Reads the config object from the browsers * storage (indexed db) */ ngOnInit(): void { - this.storage.get(SETTINGS_STORE_KEY).subscribe((settings: Settings) => { - if (settings) { - this.settingsForm.setValue({ - player: settings.player - ? settings.player - : VideoPlayer.VideoJs, - epgUrl: settings.epgUrl ? settings.epgUrl : '', - language: settings.language - ? settings.language - : Language.ENGLISH, - showCaptions: settings.showCaptions - ? settings.showCaptions - : false, - theme: settings.theme - ? settings.theme - : Theme.LightTheme, - }); - } - }); + this.settingsService + .getValueFromLocalStorage(STORE_KEY.Settings) + .subscribe((settings: Settings) => { + if (settings) { + this.settingsForm.setValue({ + player: settings.player + ? settings.player + : VideoPlayer.VideoJs, + epgUrl: settings.epgUrl ? settings.epgUrl : '', + language: settings.language + ? settings.language + : Language.ENGLISH, + showCaptions: settings.showCaptions + ? settings.showCaptions + : false, + theme: settings.theme + ? settings.theme + : Theme.LightTheme, + }); + } + }); } /** - * Checks whether the latest version of the application + * Checks whether the latest version of the application * is used and updates the version message in the * settings UI */ checkAppVersion(): void { this.http - .get(PACKAGE_JSON_URL) - .pipe(catchError(err => { - console.error(err); - throw new Error(err); - })) - .subscribe((response: { version: string }) => { - this.showVersionInformation(response.version); - }); + .get(PACKAGE_JSON_URL) + .pipe( + catchError((err) => { + console.error(err); + throw new Error(err); + }) + ) + .subscribe((response: { version: string }) => { + this.showVersionInformation(response.version); + }); } /** - * Updates the message in settings UI about the used + * Updates the message in settings UI about the used * version of the app * @param currentVersion current version of the application */ @@ -158,17 +163,14 @@ export class SettingsComponent implements OnInit { } /** - * Compares actual with latest version of the - * application + * Compares actual with latest version of the + * application * @param latestVersion latest version * @returns returns true if an update is available */ isCurrentVersionOutdated(latestVersion: string): boolean { this.version = this.electronService.getAppVersion(); - return semver.lt( - this.version, - latestVersion - ); + return semver.lt(this.version, latestVersion); } /** @@ -177,7 +179,7 @@ export class SettingsComponent implements OnInit { */ onSubmit(): void { this.storage - .set(SETTINGS_STORE_KEY, this.settingsForm.value) + .set(STORE_KEY.Settings, this.settingsForm.value) .subscribe(() => { this.settingsForm.markAsPristine(); // check whether the epg url was changed or not @@ -185,9 +187,7 @@ export class SettingsComponent implements OnInit { this.fetchEpg(); } this.translate.use(this.settingsForm.value.language); - this.settingsService.changeTheme( - this.settingsForm.value.theme - ); + this.settingsService.changeTheme(this.settingsForm.value.theme); this.snackBar.open( this.translate.instant('SETTINGS.SETTINGS_SAVED'), null, diff --git a/src/app/settings/settings.interface.ts b/src/app/settings/settings.interface.ts index e21d8c478..c5fb94edb 100644 --- a/src/app/settings/settings.interface.ts +++ b/src/app/settings/settings.interface.ts @@ -1,11 +1,9 @@ import { Language } from './language.enum'; import { Theme } from './theme.enum'; -/** Settings key in storage */ -export const SETTINGS_STORE_KEY = 'settings'; - /** * Contains all types of supported video players + * TODO: extract to separate file */ export enum VideoPlayer { VideoJs = 'videojs',