refactor: code improvements

This commit is contained in:
4gray committed 2023-02-22 23:22:04 +01:00
1 parent 76311e1adb
commit 02192a73be
14 files changed
+126 -135

No files matched your search

+1 -1
View File
@@ -99,7 +99,7 @@ function registerGlobalShortcuts() {
*/
function createEpgWorkerWindow() {
const window = new BrowserWindow({
show: true,
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
+5
View File
@@ -7,6 +7,11 @@ const routes: Routes = [
loadChildren: () =>
import('./home/home.module').then((m) => m.HomeModule),
},
{
path: 'playlists',
loadChildren: () =>
import('./player/player.module').then((m) => m.PlayerModule),
},
{
path: 'playlists/:id',
loadChildren: () =>
+6 -12
View File
@@ -18,10 +18,7 @@ import { UrlUploadComponent } from '../home/url-upload/url-upload.component';
import { DataService } from '../services/data.service';
import { ElectronServiceStub } from '../services/electron.service.stub';
import { HeaderComponent } from '../shared/components/header/header.component';
import {
PLAYLIST_PARSE,
PLAYLIST_PARSE_BY_URL,
} from './../../../shared/ipc-commands';
import { PLAYLIST_PARSE_BY_URL } from './../../../shared/ipc-commands';
import { HomeComponent } from './home.component';
class MatSnackBarStub {
@@ -106,7 +103,7 @@ describe('HomeComponent', () => {
});
it('should send an event to the main process to parse a playlist', () => {
jest.spyOn(electronService, 'sendIpcEvent');
jest.spyOn(mockStore, 'dispatch');
const title = 'my-list.m3u';
const path = '/home/user/iptv/' + title;
const playlistContent = 'test';
@@ -118,10 +115,7 @@ describe('HomeComponent', () => {
target: { result: playlistContent },
} as unknown as Event;
component.handlePlaylist({ file, uploadEvent });
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
PLAYLIST_PARSE,
{ title, playlist: [playlistContent], path }
);
expect(mockStore.dispatch).toHaveBeenCalledTimes(1);
});
it('should set IPC event listeners', () => {
@@ -140,11 +134,11 @@ describe('HomeComponent', () => {
it('should navigate to the player view', () => {
jest.spyOn(router, 'navigateByUrl');
component.navigateToPlayer();
const playlistId = 'some-id';
component.navigateToPlayer(playlistId);
expect(router.navigateByUrl).toHaveBeenCalledTimes(1);
expect(router.navigateByUrl).toHaveBeenCalledWith(
'/iptv',
expect.anything()
`/playlists/${playlistId}`
);
});
@@ -54,7 +54,7 @@
{{ 'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate }}
</mat-hint>
</mat-form-field>
<ng-container *ngIf="playlist.url || playlist.filePath">
<ng-container *ngIf="isElectron && (playlist.url || playlist.filePath)">
<mat-checkbox class="full-width" formControlName="autoRefresh">
{{ 'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE' | translate }}
</mat-checkbox>
@@ -1,5 +1,4 @@
import { MatCheckboxModule } from '@angular/material/checkbox';
import { PLAYLIST_SAVE_DETAILS } from './../../../../../shared/ipc-commands';
/* eslint-disable @typescript-eslint/unbound-method */
import { DatePipe } from '@angular/common';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
@@ -10,8 +9,12 @@ import {
} from '@angular/forms';
import { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { Actions } from '@ngrx/effects';
import { provideMockActions } from '@ngrx/effects/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockModule, MockPipe } from 'ng-mocks';
import { Observable } from 'rxjs';
import { DataService } from '../../../services/data.service';
import { ElectronServiceStub } from '../../../services/electron.service.stub';
import { Playlist } from './../../../../../shared/playlist.interface';
@@ -21,6 +24,8 @@ describe('PlaylistInfoComponent', () => {
let component: PlaylistInfoComponent;
let fixture: ComponentFixture<PlaylistInfoComponent>;
let electronService: DataService;
let mockStore: MockStore;
const actions$ = new Observable<Actions>();
beforeEach(
waitForAsync(() => {
@@ -38,6 +43,8 @@ describe('PlaylistInfoComponent', () => {
{ provide: MAT_DIALOG_DATA, useValue: {} },
{ provide: DataService, useClass: ElectronServiceStub },
UntypedFormBuilder,
provideMockStore(),
provideMockActions(actions$),
],
}).compileComponents();
})
@@ -47,6 +54,7 @@ describe('PlaylistInfoComponent', () => {
fixture = TestBed.createComponent(PlaylistInfoComponent);
component = fixture.componentInstance;
electronService = TestBed.inject(DataService);
mockStore = TestBed.inject(MockStore);
fixture.detectChanges();
});
@@ -54,14 +62,10 @@ describe('PlaylistInfoComponent', () => {
expect(component).toBeTruthy();
});
it('should send an event to the main process after save', () => {
it('should dispatch an event to save changes in the store', () => {
const playlistToSave = { _id: 'a12345', title: 'Playlist' } as Playlist;
jest.spyOn(electronService, 'sendIpcEvent');
jest.spyOn(mockStore, 'dispatch');
component.saveChanges(playlistToSave);
expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(1);
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
PLAYLIST_SAVE_DETAILS,
playlistToSave
);
expect(mockStore.dispatch).toHaveBeenCalledTimes(1);
});
});
@@ -36,7 +36,7 @@ import * as PlaylistActions from '../../../state/actions';
})
export class PlaylistInfoComponent {
/** Flag that returns true if application runs in electron-based environment */
isElectron = this.electronService.isElectron;
isElectron = this.dataService.isElectron;
/** Playlist object */
playlist: Playlist;
@@ -44,17 +44,10 @@ export class PlaylistInfoComponent {
/** Form group with playlist details */
playlistDetails: UntypedFormGroup;
/**
* Creates an instance of the component and injects the selected playlist from the parent component
* @param datePipe
* @param formBuilder
* @param electronService
* @param playlist playlist object to show
*/
constructor(
private datePipe: DatePipe,
private formBuilder: UntypedFormBuilder,
private electronService: DataService,
private dataService: DataService,
@Inject(MAT_DIALOG_DATA) playlist: Playlist,
private store: Store
) {
@@ -93,10 +86,6 @@ export class PlaylistInfoComponent {
});
}
/**
* Saves updated playlist information
* @param data updated form data
*/
saveChanges(playlist: PlaylistMeta): void {
this.store.dispatch(PlaylistActions.updatePlaylistMeta({ playlist }));
}
@@ -4,17 +4,19 @@ import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { Actions } from '@ngrx/effects';
import { provideMockActions } from '@ngrx/effects/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { MockModule, MockPipe, MockProvider } from 'ng-mocks';
import {
PLAYLIST_GET_BY_ID,
PLAYLIST_REMOVE_BY_ID,
PLAYLIST_UPDATE,
} from '../../../../shared/ipc-commands';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Observable } from 'rxjs';
import { PLAYLIST_UPDATE } from '../../../../shared/ipc-commands';
import { DataService } from '../../services/data.service';
import { DialogService } from '../../services/dialog.service';
import { ElectronServiceStub } from '../../services/electron.service.stub';
import { PlaylistMeta } from '../../shared/playlist-meta.type';
import { initialPlaylistMetaState } from '../../state/playlists.state';
import { RecentPlaylistsComponent } from './recent-playlists.component';
describe('RecentPlaylistsComponent', () => {
@@ -23,6 +25,8 @@ describe('RecentPlaylistsComponent', () => {
let electronService: DataService;
let dialog: MatDialog;
let dialogService: DialogService;
let mockStore: MockStore;
const actions$ = new Observable<Actions>();
beforeEach(
waitForAsync(() => {
@@ -36,12 +40,15 @@ describe('RecentPlaylistsComponent', () => {
MockModule(MatListModule),
MockModule(MatIconModule),
MockModule(MatTooltipModule),
MockModule(NgxSkeletonLoaderModule),
],
providers: [
{ provide: DataService, useClass: ElectronServiceStub },
MockProvider(TranslateService),
MockProvider(DialogService),
MatSnackBar,
provideMockStore(),
provideMockActions(actions$),
],
}).compileComponents();
})
@@ -50,10 +57,13 @@ describe('RecentPlaylistsComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(RecentPlaylistsComponent);
component = fixture.componentInstance;
component.playlists = [];
dialog = TestBed.inject(MatDialog);
electronService = TestBed.inject(DataService);
dialogService = TestBed.inject(DialogService);
mockStore = TestBed.inject(MockStore);
mockStore.setState({
playlistState: { playlists: initialPlaylistMetaState },
});
fixture.detectChanges();
});
@@ -78,9 +88,9 @@ describe('RecentPlaylistsComponent', () => {
distance: { x: 0, y: 0 },
dropPoint: { x: 0, y: 0 },
} as any;
jest.spyOn(electronService, 'sendIpcEvent');
component.drop(event);
expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(1);
jest.spyOn(mockStore, 'dispatch');
component.drop(event, []);
expect(mockStore.dispatch).toHaveBeenCalledTimes(1);
});
it('should open the confirmation dialog on remove icon click', () => {
@@ -92,36 +102,33 @@ describe('RecentPlaylistsComponent', () => {
it('should send an event to the main process to remove a playlist', () => {
const playlistId = '12345';
jest.spyOn(electronService, 'sendIpcEvent');
jest.spyOn(mockStore, 'dispatch');
component.removePlaylist(playlistId);
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
PLAYLIST_REMOVE_BY_ID,
{ id: playlistId }
);
expect(mockStore.dispatch).toHaveBeenCalledTimes(1);
});
it('should send an event to the main process to refresh a playlist', () => {
const playlistMeta: PlaylistMeta = {
id: 'iptv1',
title: 'iptv',
filePath: '/home/user/lists/iptv.m3u',
} as PlaylistMeta;
} as unknown as PlaylistMeta;
jest.spyOn(electronService, 'sendIpcEvent');
component.refreshPlaylist(playlistMeta);
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
PLAYLIST_UPDATE,
{ id: playlistMeta._id, filePath: playlistMeta.filePath }
{
id: playlistMeta._id,
filePath: playlistMeta.filePath,
title: playlistMeta.title,
}
);
});
it('should send an event to the main process to get a playlist', () => {
const playlistId = '6789';
jest.spyOn(electronService, 'sendIpcEvent');
jest.spyOn(component.playlistClicked, 'emit');
component.getPlaylist(playlistId);
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
PLAYLIST_GET_BY_ID,
{
id: playlistId,
}
);
expect(component.playlistClicked.emit).toHaveBeenCalledTimes(1);
});
});
@@ -46,11 +46,11 @@ export class RecentPlaylistsComponent implements OnDestroy {
new IpcCommand(
PLAYLIST_UPDATE_RESPONSE,
(response: { message: string; playlist: Playlist }) => {
console.log(response.playlist);
this.snackBar.open(response.message, null, { duration: 2000 });
this.store.dispatch(
PlaylistActions.updatePlaylist({
playlistId: '',
// TODO: check if this is correct
playlistId: response.playlist._id,
playlist: response.playlist,
})
);
@@ -142,10 +142,6 @@ export class RecentPlaylistsComponent implements OnDestroy {
);
}
/**
* Requests playlist by id
* @param playlistId playlist id
*/
getPlaylist(playlistId: string): void {
this.router.navigate(['playlists', playlistId]);
this.playlistClicked.emit(playlistId);
@@ -13,8 +13,9 @@ import { RouterTestingModule } from '@angular/router/testing';
import { Actions } from '@ngrx/effects';
import { provideMockActions } from '@ngrx/effects/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { TranslatePipe } from '@ngx-translate/core';
import { MockModule, MockPipes } from 'ng-mocks';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { MockModule, MockPipes, MockProviders } from 'ng-mocks';
import { NgxIndexedDBService } from 'ngx-indexed-db';
import { Observable } from 'rxjs';
import * as MOCKED_PLAYLIST from '../../../../mocks/playlist.json';
import { DataService } from '../../../services/data.service';
@@ -44,6 +45,7 @@ describe('ChannelListContainerComponent', () => {
{ provide: DataService, useClass: ElectronServiceStub },
provideMockStore(),
provideMockActions(actions$),
MockProviders(NgxIndexedDBService, TranslateService),
],
imports: [
MockModule(MatSnackBarModule),
@@ -73,8 +75,6 @@ describe('ChannelListContainerComponent', () => {
mockStore.setState({
playlistState: {
channels,
favorites: [MOCKED_PLAYLIST.playlist.items[0].url],
playlistId: '',
active: undefined,
},
});
@@ -158,9 +158,10 @@ describe('ChannelListContainerComponent', () => {
new MouseEvent('click')
);
fixture.detectChanges();
expect(mockStore.dispatch).toHaveBeenCalledWith(
{channel: component._channelList[0], type: expect.stringContaining('favorites')},
);
expect(mockStore.dispatch).toHaveBeenCalledWith({
channel: component._channelList[0],
type: expect.stringContaining('favorites'),
});
expect(mockStore.dispatch).toHaveBeenCalledTimes(1);
});
});
@@ -9,12 +9,10 @@ import { ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { TranslatePipe } from '@ngx-translate/core';
import { MockComponent, MockModule, MockPipe } from 'ng-mocks';
import * as MOCKED_PLAYLIST from '../../../../mocks/playlist.json';
import { MockComponent, MockModule, MockPipe, MockProviders } from 'ng-mocks';
import { DataService } from '../../../services/data.service';
import { ElectronServiceStub } from '../../../services/electron.service.stub';
import { VideoPlayer } from '../../../settings/settings.interface';
import { createChannel } from '../../../shared/channel.model';
import { ChannelListContainerComponent } from '../channel-list-container/channel-list-container.component';
import { EpgListComponent } from '../epg-list/epg-list.component';
import { HtmlVideoPlayerComponent } from '../html-video-player/html-video-player.component';
@@ -24,7 +22,10 @@ import { VideoPlayerComponent } from './video-player.component';
import { Actions } from '@ngrx/effects';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable } from 'rxjs';
import { NgxIndexedDBService } from 'ngx-indexed-db';
import { Observable, of } from 'rxjs';
import { PlaylistsService } from '../../../services/playlists.service';
import { initialState } from '../../../state/state';
class MatSnackBarStub {
open(): void {}
@@ -56,6 +57,7 @@ describe('VideoPlayerComponent', () => {
{
provide: ActivatedRoute,
useValue: {
params: of({ id: '1' }),
snapshot: {
queryParams: {
url: 'https://iptvnator/list.m3u',
@@ -65,6 +67,7 @@ describe('VideoPlayerComponent', () => {
},
provideMockStore(),
provideMockActions(actions$),
MockProviders(NgxIndexedDBService, PlaylistsService),
],
imports: [
MockModule(MatSidenavModule),
@@ -83,34 +86,16 @@ describe('VideoPlayerComponent', () => {
component = fixture.componentInstance;
mockStore = TestBed.inject(MockStore);
// set channels
channels = MOCKED_PLAYLIST.playlist.items.map((element) =>
createChannel(element)
);
mockStore.setState({ playlistState: {channels} });
});
it('should create and init component', () => {
expect(component).toBeTruthy();
jest.spyOn(component, 'applySettings');
mockStore.setState({
playlistState: initialState,
});
fixture.detectChanges();
expect(component.applySettings).toHaveBeenCalledTimes(1);
});
it('should check default component settings', () => {
fixture.detectChanges();
expect(component.playerSettings).toEqual({
player: VideoPlayer.VideoJs,
showCaptions: false,
});
});
it('should update store after channel was faved', () => {
jest.spyOn(mockStore, 'dispatch');
const [firstChannel] = channels;
component.addToFavorites(firstChannel);
expect(mockStore.dispatch).toHaveBeenCalledTimes(1);
expect(mockStore.dispatch).toHaveBeenCalledWith({channel: firstChannel, type: expect.stringContaining('favorite')});
});
});
@@ -11,7 +11,13 @@ import {
import { ActivatedRoute } from '@angular/router';
import { Store } from '@ngrx/store';
import { StorageMap } from '@ngx-pwa/local-storage';
import { filter, Observable, skipWhile, switchMap } from 'rxjs';
import {
combineLatestWith,
filter,
Observable,
skipWhile,
switchMap,
} from 'rxjs';
import { Channel } from '../../../../../shared/channel.interface';
import {
PLAYLIST_PARSE_BY_URL,
@@ -25,6 +31,7 @@ import { STORE_KEY } from '../../../shared/enums/store-keys.enum';
import * as PlaylistActions from '../../../state/actions';
import {
selectActive,
selectChannels,
selectCurrentEpgProgram,
selectPlaylistTitle,
} from '../../../state/selectors';
@@ -69,11 +76,19 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
{
id: PLAYLIST_PARSE_RESPONSE,
execute: (response: { payload: Playlist }): void => {
this.store.dispatch(
PlaylistActions.addPlaylist({
playlist: response.payload,
})
);
if (response.payload.isTemporary) {
this.store.dispatch(
PlaylistActions.setChannels({
channels: response.payload.playlist.items,
})
);
} else {
this.store.dispatch(
PlaylistActions.addPlaylist({
playlist: response.payload,
})
);
}
this.sidebarView = 'CHANNELS';
},
},
@@ -107,11 +122,18 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
this.getPlaylistUrlAsParam();
this.channels$ = this.activatedRoute.params.pipe(
switchMap((params) => {
this.store.dispatch(
PlaylistActions.setActivePlaylist({ playlistId: params.id })
);
return this.playlistsService.getPlaylistChannels(params.id);
combineLatestWith(this.activatedRoute.queryParams),
switchMap(([params, queryParams]) => {
if (params.id) {
this.store.dispatch(
PlaylistActions.setActivePlaylist({
playlistId: params.id,
})
);
return this.playlistsService.getPlaylistChannels(params.id);
} else if (queryParams.url) {
return this.store.select(selectChannels);
}
})
);
}
+7 -14
View File
@@ -6,7 +6,7 @@ import { SwUpdate } from '@angular/service-worker';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { NgxIndexedDBService } from 'ngx-indexed-db';
import { catchError, of, switchMap, throwError } from 'rxjs';
import { catchError, throwError } from 'rxjs';
import {
ERROR,
PLAYLIST_PARSE_BY_URL,
@@ -15,7 +15,6 @@ import {
} from '../../../shared/ipc-commands';
import { Playlist } from '../../../shared/playlist.interface';
import { AppConfig } from '../../environments/environment';
import { DbStores } from '../indexed-db.config';
import * as PlaylistActions from '../state/actions';
import { DataService } from './data.service';
import { PlaylistsService } from './playlists.service';
@@ -96,13 +95,12 @@ export class PwaService extends DataService {
playlistId: payload.id,
})
);
console.log('playlist was updated...');
// TODO: show notification
/* window.postMessage({
type: PLAYLIST_UPDATE_RESPONSE,
message: `Success! The playlist was successfully updated.`,
}); */
this.snackBar.open(
`Success! The playlist was successfully updated.`,
null,
{ duration: 2000 }
);
});
}
}
@@ -114,11 +112,6 @@ export class PwaService extends DataService {
fetchFromUrl(payload: Partial<Playlist>): void {
this.getPlaylistFromUrl(payload.url)
.pipe(
switchMap((response) =>
payload.isTemporary
? of(response)
: this.dbService.add(DbStores.Playlists, response)
),
catchError((error) => {
window.postMessage({
type: ERROR,
@@ -131,7 +124,7 @@ export class PwaService extends DataService {
.subscribe((response: any) => {
window.postMessage({
type: PLAYLIST_PARSE_RESPONSE,
payload: response,
payload: { ...response, isTemporary: payload.isTemporary },
});
});
}
+3 -3
View File
@@ -90,9 +90,9 @@ export const setEpgAvailableFlag = createAction(
props<{ value: boolean }>()
);
export const setPlaylist = createAction(
`${STORE_KEY} Set playlist as active`,
props<{ playlist: Playlist }>()
export const setChannels = createAction(
`${STORE_KEY} Set channels`,
props<{ channels: Channel[] }>()
);
export const updatePlaylistPositions = createAction(
+6 -11
View File
@@ -47,17 +47,6 @@ export const playlistReducer = createReducer(
epgAvailable: action.value,
})
),
/* on(PlaylistActions.setPlaylist, (state, action): PlaylistState => {
const { playlist } = action;
return {
...state,
channels: playlist?.playlist.items.map((element) =>
createChannel(element)
),
active: undefined,
};
}), */
on(PlaylistActions.updateFavorites, (state, action): PlaylistState => {
let favorites;
const { channel } = action;
@@ -167,6 +156,12 @@ export const playlistReducer = createReducer(
state.playlists
),
};
}),
on(PlaylistActions.setChannels, (state, action): PlaylistState => {
return {
...state,
channels: action.channels,
};
})
);