refactor: switch from akita to ngrx

This commit is contained in:
4gray committed 2022-12-06 23:52:41 +01:00
1 parent 9b557690c1
commit 466bbade53
25 files changed
+2058 -4187

No files matched your search

+13 -3
View File
@@ -35,9 +35,15 @@
4,
{
"SwitchCase": 1,
"CallExpression": { "arguments": "first" },
"FunctionExpression": { "parameters": "first" },
"FunctionDeclaration": { "parameters": "first" },
"CallExpression": {
"arguments": "first"
},
"FunctionExpression": {
"parameters": "first"
},
"FunctionDeclaration": {
"parameters": "first"
},
"ignoredNodes": ["TSTypeParameterInstantiation"]
}
],
@@ -66,6 +72,10 @@
"@angular-eslint/template/banana-in-box": "error",
"@angular-eslint/template/no-negated-async": "error"
}
},
{
"files": ["*.ts"],
"extends": ["plugin:@ngrx/recommended"]
}
]
}
+1617 -3893
View File
File diff suppressed because it is too large. Load diff
+17 -14
View File
@@ -51,18 +51,20 @@
"@angular/cdk": "14.0.5",
"@angular/flex-layout": "14.0.0-beta.40",
"@angular/material": "14.0.5",
"@datorama/akita": "7.1.1",
"@electron/remote": "2.0.8",
"@ngrx/effects": "14.3.2",
"@ngrx/store": "14.3.2",
"@ngrx/store-devtools": "14.3.2",
"@ngx-pwa/local-storage": "13.0.5",
"@videojs/http-streaming": "2.13.1",
"@videojs/http-streaming": "2.15.0",
"@yangkghjh/videojs-aspect-ratio-panel": "0.0.1",
"about-window": "1.15.2",
"axios": "0.21.2",
"custom-electron-titlebar": "4.1.0",
"date-fns": "2.28.0",
"electron-context-menu": "3.1.1",
"date-fns": "2.29.3",
"electron-context-menu": "3.5.0",
"epg-parser": "0.1.6",
"hls.js": "1.2.1",
"hls.js": "1.2.7",
"iptv-playlist-parser": "0.8.0",
"lodash": "4.17.21",
"material-icons": "0.3.1",
@@ -70,11 +72,11 @@
"nedb-promises": "6.0.3",
"ngx-indexed-db": "9.4.2",
"ngx-whats-new": "0.3.0",
"rxjs": "7.5.6",
"semver": "7.3.5",
"rxjs": "7.5.7",
"semver": "7.3.8",
"uuid": "8.3.2",
"video.js": "7.20.2",
"videojs-contrib-quality-levels": "2.1.0",
"video.js": "7.20.3",
"videojs-contrib-quality-levels": "2.2.0",
"videojs-hls-quality-selector": "1.1.4",
"zone.js": "~0.11.4"
},
@@ -99,6 +101,7 @@
"@angular/service-worker": "14.0.6",
"@commitlint/cli": "16.0.1",
"@commitlint/config-conventional": "16.0.0",
"@ngrx/eslint-plugin": "^14.3.2",
"@ngx-translate/core": "14.0.0",
"@ngx-translate/http-loader": "7.0.0",
"@playwright/test": "1.26.0",
@@ -111,17 +114,17 @@
"@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",
"@typescript-eslint/eslint-plugin": "5.40.1",
"@typescript-eslint/eslint-plugin-tslint": "5.40.1",
"@typescript-eslint/parser": "5.40.1",
"chai": "4.3.4",
"conventional-changelog-cli": "2.2.2",
"core-js": "3.6.5",
"cross-env": "7.0.3",
"electron": "20.2.0",
"electron-builder": "23.3.3",
"eslint": "7.32.0",
"eslint-plugin-import": "2.25.2",
"eslint": "8.26.0",
"eslint-plugin-import": "2.26.0",
"husky": "5.0.9",
"jest": "28.1.3",
"jest-preset-angular": "12.2.0",
+1 -3
View File
@@ -1,5 +1,3 @@
import { ID } from '@datorama/akita';
/**
* An interface that describe the possible states of the playlist update/refresh process
*/
@@ -13,7 +11,7 @@ export enum PlaylistUpdateState {
* Describes playlist interface
*/
export interface Playlist {
id: ID;
id: string;
_id: string;
title: string;
filename: string;
+10
View File
@@ -42,3 +42,13 @@ export function createFavoritesPlaylist(
filename: 'Global favorites',
};
}
export const getUUID = () =>
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
(String(1e7) + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(
Number(c) ^
(crypto.getRandomValues(new Uint8Array(1))[0] &
(15 >> (Number(c) / 4)))
).toString(16)
);
+1 -1
View File
@@ -167,7 +167,7 @@ export class AppComponent {
const actualVersion = this.electronService.getAppVersion();
this.settingsService
.getValueFromLocalStorage(STORE_KEY.Version)
.subscribe((version) => {
.subscribe((version: string) => {
const isNewVersion = semver.gt(
actualVersion,
version || '0.0.0'
+12
View File
@@ -4,6 +4,9 @@ import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ServiceWorkerModule } from '@angular/service-worker';
// NG Translate
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { NgxIndexedDBModule, NgxIndexedDBService } from 'ngx-indexed-db';
@@ -17,6 +20,8 @@ import { DataService } from './services/data.service';
import { ElectronService } from './services/electron.service';
import { PwaService } from './services/pwa.service';
import { SharedModule } from './shared/shared.module';
import { PlaylistEffects } from './state/effects';
import { playlistReducer } from './state/reducers';
// AoT requires an exported function for factories
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
@@ -65,6 +70,13 @@ export function DataFactory(dbService: NgxIndexedDBService, http: HttpClient) {
enabled: AppConfig.production && !isElectron,
registrationStrategy: 'registerWhenStable:30000',
}),
StoreModule.forRoot({}),
StoreModule.forFeature('playlistState', playlistReducer),
EffectsModule.forRoot([PlaylistEffects]),
StoreDevtoolsModule.instrument({
maxAge: 25,
logOnly: AppConfig.production,
}),
],
providers: [
{
+10 -4
View File
@@ -1,6 +1,7 @@
import { Component, NgZone } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import {
PLAYLIST_PARSE,
PLAYLIST_PARSE_BY_URL,
@@ -10,7 +11,8 @@ import {
import { Playlist } from '../../../shared/playlist.interface';
import { DataService } from '../services/data.service';
import { PlaylistMeta } from '../shared/playlist-meta.type';
import { ChannelStore } from '../state';
import { setPlaylist } from '../state/actions';
import { selectChannels } from '../state/selectors';
@Component({
selector: 'app-home',
@@ -21,6 +23,8 @@ export class HomeComponent {
/** Added playlists */
playlists: PlaylistMeta[] = [];
channels$ = this.store.select(selectChannels);
/** Loading spinner state */
isLoading = false;
@@ -29,7 +33,9 @@ export class HomeComponent {
{
id: PLAYLIST_PARSE_RESPONSE,
execute: (response: { payload: Playlist }): void => {
this.channelStore.setPlaylist(response.payload);
this.store.dispatch(
setPlaylist({ playlist: response.payload })
);
this.navigateToPlayer();
},
},
@@ -39,7 +45,7 @@ export class HomeComponent {
/**
* Creates an instanceof HomeComponent
* @param channelStore channels store
* @param store channels store
* @param electronService electron service
* @param ngZone angular ngZone module
* @param router angular router
@@ -48,7 +54,7 @@ export class HomeComponent {
constructor(
private electronService: DataService,
private ngZone: NgZone,
private channelStore: ChannelStore,
private readonly store: Store,
private router: Router,
private snackBar: MatSnackBar
) {
@@ -6,11 +6,16 @@ import {
ViewChild,
} from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store';
import * as _ from 'lodash';
import { map, Observable, skipWhile } from 'rxjs';
import { combineLatestWith, map, skipWhile } from 'rxjs';
import { Channel } from '../../../../../shared/channel.interface';
import { ChannelQuery, ChannelStore } from '../../../state';
import * as PlaylistActions from '../../../state/actions';
import {
selectChannels,
selectFavorites,
selectPlaylistId,
} from '../../../state/selectors';
@Component({
selector: 'app-channel-list-container',
templateUrl: './channel-list-container.component.html',
@@ -38,11 +43,12 @@ export class ChannelListContainerComponent {
/** Selected channel */
selected!: Channel;
/** List with favorited channels */
favorites$: Observable<Channel[]> = this.channelQuery.select((store) =>
this.channelQuery
.getAll()
.filter((channel) => store.favorites.includes(channel.id))
/** List with favorites */
favorites$ = this.store.select(selectChannels).pipe(
combineLatestWith(this.store.select(selectFavorites)),
map(([channels, favorites]) =>
channels.filter((channel) => favorites.includes(channel.id))
)
);
/** Search term for channel filter */
@@ -62,21 +68,18 @@ export class ChannelListContainerComponent {
}
/** ID of the current playlist */
playlistId$ = this.channelQuery.select().pipe(
skipWhile(
(store) => store.playlistId === '' || store.playlistId === undefined
),
map((data) => data.playlistId)
);
playlistId$ = this.store
.select(selectPlaylistId)
.pipe(
skipWhile(
(playlistId) => playlistId === '' || playlistId === undefined
)
);
/**
* Creates an instance of ChannelListContainerComponent
*/
constructor(
private channelQuery: ChannelQuery,
private channelStore: ChannelStore,
private snackBar: MatSnackBar
) {}
constructor(private readonly store: Store, private snackBar: MatSnackBar) {}
/**
* Sets clicked channel as selected and emits them to the parent component
@@ -84,7 +87,7 @@ export class ChannelListContainerComponent {
*/
selectChannel(channel: Channel): void {
this.selected = channel;
this.channelStore.setActiveChannel(channel);
this.store.dispatch(PlaylistActions.setActiveChannel({ channel }));
}
/**
@@ -95,7 +98,7 @@ export class ChannelListContainerComponent {
toggleFavoriteChannel(channel: Channel, clickEvent: MouseEvent): void {
clickEvent.stopPropagation();
this.snackBar.open('Favorites were updated!', null, { duration: 2000 });
this.channelStore.updateFavorite(channel);
this.store.dispatch(PlaylistActions.updateFavorites({ channel }));
}
/**
@@ -95,7 +95,7 @@
[item]="item"
></app-epg-list-item>
</div>
<p matLine>{{ item?.title[0]?.value }}</p>
<p matLine [innerHTML]="item?.title[0]?.value"></p>
</mat-list-option>
</ng-template>
</ng-container>
@@ -1,10 +1,16 @@
import { Component, NgZone } from '@angular/core';
import { Store } from '@ngrx/store';
import * as moment from 'moment';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { EPG_GET_PROGRAM_DONE } from '../../../../../shared/ipc-commands';
import { DataService } from '../../../services/data.service';
import { ChannelQuery, ChannelStore } from '../../../state';
import {
resetActiveEpgProgram,
setActiveEpgProgram,
setCurrentEpgProgram,
} from '../../../state/actions';
import { selectActive } from '../../../state/selectors';
import { EpgChannel } from '../../models/epg-channel.model';
import { EpgProgram } from '../../models/epg-program.model';
@@ -50,14 +56,12 @@ export class EpgListComponent {
/**
* Creates an instance of EpgListComponent
* @param channelQuery
* @param channelStore
* @param store
* @param electronService
* @param ngZone
*/
constructor(
private channelQuery: ChannelQuery,
private channelStore: ChannelStore,
private readonly store: Store,
private electronService: DataService,
private ngZone: NgZone
) {
@@ -73,20 +77,21 @@ export class EpgListComponent {
* Subscribe for values from the store on component init
*/
ngOnInit(): void {
this.timeshiftUntil$ = this.channelQuery
.select(
(store) =>
store.active?.tvg?.rec ||
store.active?.timeshift ||
store.active?.catchup?.days
this.timeshiftUntil$ = this.store.select(selectActive).pipe(
// eslint-disable-next-line @ngrx/avoid-mapping-selectors
map((active) => {
return (
active?.tvg?.rec ||
active?.timeshift ||
active?.catchup?.days
);
}),
map((value) =>
moment(Date.now())
.subtract(value, 'days')
.format(DATE_TIME_FORMAT)
)
.pipe(
map((value) =>
moment(Date.now())
.subtract(value, 'days')
.format(DATE_TIME_FORMAT)
)
);
);
}
/**
@@ -105,7 +110,7 @@ export class EpgListComponent {
} else {
this.items = [];
this.channel = null;
this.channelStore.setCurrentEpgProgram(undefined);
this.store.dispatch(setCurrentEpgProgram(undefined));
}
}
@@ -156,7 +161,7 @@ export class EpgListComponent {
this.playingNow = this.items.find(
(item) => this.timeNow >= item.start && this.timeNow <= item.stop
);
this.channelStore.setCurrentEpgProgram(this.playingNow);
this.store.dispatch(setCurrentEpgProgram({ program: this.playingNow }));
}
/**
@@ -171,10 +176,10 @@ export class EpgListComponent {
timeshift?: boolean
): void {
if (isLive) {
this.channelStore.resetActiveEpgProgram();
this.store.dispatch(resetActiveEpgProgram());
} else {
if (!timeshift) return;
this.channelStore.setActiveEpgProgram(program);
this.store.dispatch(setActiveEpgProgram({ program }));
}
this.playingNow = program;
}
@@ -12,8 +12,9 @@ import {
import { MatSidenav } from '@angular/material/sidenav';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { StorageMap } from '@ngx-pwa/local-storage';
import { filter, map, Observable, skipWhile } from 'rxjs';
import { filter, skipWhile } from 'rxjs';
import { Channel } from '../../../../../shared/channel.interface';
import {
PLAYLIST_GET_ALL,
@@ -24,9 +25,17 @@ import { Playlist } from '../../../../../shared/playlist.interface';
import { DataService } from '../../../services/data.service';
import { Settings, VideoPlayer } from '../../../settings/settings.interface';
import { STORE_KEY } from '../../../shared/enums/store-keys.enum';
import { ChannelQuery, ChannelStore } from '../../../state';
import { setPlaylist, updateFavorites } from '../../../state/actions';
import {
selectActive,
selectChannels,
selectCurrentEpgProgram,
selectFavorites,
selectIsEpgAvailable,
selectPlaylistFilename,
selectPlaylistId,
} from '../../../state/selectors';
import { MultiEpgContainerComponent } from '../multi-epg/multi-epg-container.component';
import { EpgProgram } from './../../models/epg-program.model';
/** Possible sidebar view options */
type SidebarView = 'CHANNELS' | 'PLAYLISTS';
@@ -42,27 +51,21 @@ export const COMPONENT_OVERLAY_REF = new InjectionToken(
})
export class VideoPlayerComponent implements OnInit, OnDestroy {
/** Active selected channel */
activeChannel$: Observable<Channel> = this.channelQuery
.select((state) => state.active)
activeChannel$ = this.store
.select(selectActive)
.pipe(filter((channel) => Boolean(channel)));
/** Channels list */
channels$: Observable<Channel[]> = this.channelQuery.selectAll();
channels$ = this.store.select(selectChannels);
/** EPG availability flag */
epgAvailable$: Observable<boolean> = this.channelQuery.select(
(store) => store.epgAvailable
);
epgAvailable$ = this.store.select(selectIsEpgAvailable);
/** Current epg program */
epgProgram$: Observable<EpgProgram> = this.channelQuery.select(
(store) => store.currentEpgProgram
);
epgProgram$ = this.store.select(selectCurrentEpgProgram);
/** Favorites list */
favorites$: Observable<string[]> = this.channelQuery.select(
(store) => store.favorites
);
favorites$ = this.store.select(selectFavorites);
/** Selected video player options */
playerSettings: Partial<Settings> = {
@@ -77,18 +80,18 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
@ViewChild('sidenav') sideNav: MatSidenav;
/** ID of the current playlist */
playlistId$ = this.channelQuery.select().pipe(
skipWhile(
(store) => store.playlistId === '' || store.playlistId === undefined
),
map((data) => data.playlistId)
);
playlistId$ = this.store
.select(selectPlaylistId)
.pipe(
skipWhile(
(playlistId) => playlistId === '' || playlistId === undefined
)
);
/** Title of the current playlist */
playlistTitle$ = this.channelQuery.select().pipe(
skipWhile((store) => store.playlistFilename === ''),
map((store) => store.playlistFilename)
);
playlistTitle$ = this.store
.select(selectPlaylistFilename)
.pipe(skipWhile((playlistFilename) => playlistFilename === ''));
isElectron = this.dataService.isElectron;
@@ -97,7 +100,9 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
{
id: PLAYLIST_PARSE_RESPONSE,
execute: (response: { payload: Playlist }): void => {
this.channelStore.setPlaylist(response.payload);
this.store.dispatch(
setPlaylist({ playlist: response.payload })
);
this.setSidebarView('CHANNELS');
},
},
@@ -116,8 +121,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
*/
constructor(
private activatedRoute: ActivatedRoute,
private channelQuery: ChannelQuery,
private channelStore: ChannelStore,
private store: Store,
public dataService: DataService,
private ngZone: NgZone,
private overlay: Overlay,
@@ -200,7 +204,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
*/
addToFavorites(channel: Channel): void {
this.snackBar.open('Favorites were updated!', null, { duration: 2000 });
this.channelStore.updateFavorite(channel);
this.store.dispatch(updateFavorites({ channel }));
}
/**
+4 -3
View File
@@ -1,8 +1,9 @@
import { Injectable } from '@angular/core';
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { EPG_FETCH } from '../../../shared/ipc-commands';
import { ChannelStore } from '../state';
import { setEpgAvailableFlag } from '../state/actions';
import { DataService } from './data.service';
@Injectable({
@@ -16,7 +17,7 @@ export class EpgService {
};
constructor(
private channelStore: ChannelStore,
private readonly store: Store,
private electronService: DataService,
private snackBar: MatSnackBar,
private translate: TranslateService
@@ -46,7 +47,7 @@ export class EpgService {
* Handles the event when the EPG fetching is done
*/
onEpgFetchDone(): void {
this.channelStore.setEpgAvailableFlag(true);
this.store.dispatch(setEpgAvailableFlag({ value: true }));
this.snackBar.open(
this.translate.instant('EPG.DOWNLOAD_SUCCESS'),
null,
+4 -3
View File
@@ -2,7 +2,6 @@ import { HttpClient } from '@angular/common/http';
import { ApplicationRef, inject, Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { SwUpdate } from '@angular/service-worker';
import { guid } from '@datorama/akita';
import { parse } from 'iptv-playlist-parser';
import { NgxIndexedDBService } from 'ngx-indexed-db';
import {
@@ -41,6 +40,7 @@ import {
import {
aggregateFavoriteChannels,
createFavoritesPlaylist,
getUUID,
} from '../../../shared/playlist.utils';
import { AppConfig } from '../../environments/environment';
import { ParsedPlaylist } from '../../typings';
@@ -272,6 +272,7 @@ export class PwaService extends DataService {
this.fetchFromUrl(payload);
} else if (type === PLAYLIST_UPDATE) {
this.http
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
.get(`${this.corsProxyUrl}${payload.url}`, {
responseType: 'text',
})
@@ -385,7 +386,7 @@ export class PwaService extends DataService {
urlOrPath?: string,
uploadType?: 'URL' | 'FILE'
): Playlist {
const id = guid();
const id = getUUID();
return {
id,
_id: id,
@@ -395,7 +396,7 @@ export class PwaService extends DataService {
playlist: {
...playlist,
items: playlist.items.map((item) => ({
id: guid(),
id: getUUID(),
...item,
})),
},
+4 -6
View File
@@ -7,13 +7,13 @@ import {
} from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import * as semver from 'semver';
import { DataService } from '../services/data.service';
import { EpgService } from '../services/epg.service';
import { STORE_KEY } from '../shared/enums/store-keys.enum';
import { ChannelQuery } from '../state';
import { selectIsEpgAvailable } from '../state/selectors';
import { SettingsService } from './../services/settings.service';
import { Language } from './language.enum';
import { Settings, VideoPlayer } from './settings.interface';
@@ -47,9 +47,7 @@ export class SettingsComponent implements OnInit {
updateMessage: string;
/** EPG availability flag */
epgAvailable$: Observable<boolean> = this.channelQuery.select(
(store) => store.epgAvailable
);
epgAvailable$ = this.store.select(selectIsEpgAvailable);
/** Flag that indicates whether the app runs in electron environment */
isElectron = this.electronService.isElectron;
@@ -74,7 +72,7 @@ export class SettingsComponent implements OnInit {
* required dependencies into the component
*/
constructor(
private channelQuery: ChannelQuery,
private store: Store,
private electronService: DataService,
private epgService: EpgService,
private formBuilder: UntypedFormBuilder,
File renamed without changes.
+45
View File
@@ -0,0 +1,45 @@
import { createAction, props } from '@ngrx/store';
import { Channel } from '../../../shared/channel.interface';
import { Playlist } from '../../../shared/playlist.interface';
import { EpgProgram } from '../player/models/epg-program.model';
const STORE_KEY = '[PLAYLIST STORE]';
export const updateFavorites = createAction(
`${STORE_KEY} Add/remove provided channel to the favorites`,
props<{ channel: Channel }>()
);
export const setActiveChannel = createAction(
`${STORE_KEY} Set active channel`,
props<{ channel: Channel }>()
);
export const setActiveChannelSuccess = createAction(
`${STORE_KEY} Set active channel success`,
props<{ channel: Channel }>()
);
export const setActiveEpgProgram = createAction(
`${STORE_KEY} Sets the given timestamp for the epg program`,
props<{ program: EpgProgram }>()
);
export const setCurrentEpgProgram = createAction(
`${STORE_KEY} Updates the active epg program for the active channel`,
props<{ program: EpgProgram }>()
);
export const resetActiveEpgProgram = createAction(
`${STORE_KEY} Reset active epg program`
);
export const setEpgAvailableFlag = createAction(
`${STORE_KEY} Reset active epg program`,
props<{ value: boolean }>()
);
export const setPlaylist = createAction(
`${STORE_KEY} Set playlist as active`,
props<{ playlist: Playlist }>()
);
-10
View File
@@ -1,10 +0,0 @@
import { Injectable } from '@angular/core';
import { QueryEntity } from '@datorama/akita';
import { ChannelStore, ChannelState } from './channel.store';
@Injectable({ providedIn: 'root' })
export class ChannelQuery extends QueryEntity<ChannelState> {
constructor(protected store: ChannelStore) {
super(store);
}
}
-6
View File
@@ -1,6 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ChannelService {
constructor() {}
}
-164
View File
@@ -1,164 +0,0 @@
import { Injectable } from '@angular/core';
import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';
import * as moment from 'moment';
import { createChannel } from '.';
import { Channel } from '../../../shared/channel.interface';
import {
CHANNEL_SET_USER_AGENT,
EPG_GET_PROGRAM,
PLAYLIST_UPDATE_FAVORITES,
} from '../../../shared/ipc-commands';
import { Playlist } from '../../../shared/playlist.interface';
import { EpgProgram } from '../player/models/epg-program.model';
import { DataService } from '../services/data.service';
export interface ChannelState extends EntityState<Channel> {
active: Channel;
epgAvailable: boolean;
currentEpgProgram: EpgProgram;
favorites: string[];
playlistId: string;
playlistFilename: string;
}
@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'channel', resettable: true })
export class ChannelStore extends EntityStore<ChannelState> {
/**
* Creates an instance of ChannelStore
* @param electronService electron service
*/
constructor(private electronService: DataService) {
super({
active: undefined,
currentEpgProgram: undefined,
epgAvailable: false,
favorites: [],
playlistId: '',
playlistFilename: '',
});
}
/**
* Adds/removes the given channel from the favorites list
* @param channel channel to add/remove
*/
updateFavorite(channel: Channel): void {
let favorites;
this.update((store) => {
if (store.favorites.includes(channel.id)) {
favorites = [
...store.favorites.filter((id) => id !== channel.id),
];
} else {
favorites = [...store.favorites, channel.id];
}
this.electronService.sendIpcEvent(PLAYLIST_UPDATE_FAVORITES, {
id: store.playlistId,
favorites,
});
return { favorites };
});
}
/**
* Updates selected/active channel in store
* @param channel selected channel
*/
setActiveChannel(channel: Channel): void {
this.update((store) => {
if (store.epgAvailable) {
this.electronService.sendIpcEvent(EPG_GET_PROGRAM, {
channel,
});
if (channel.http['user-agent']) {
this.electronService.sendIpcEvent(CHANNEL_SET_USER_AGENT, {
referer: channel.http.referrer,
userAgent: channel.http['user-agent'],
});
}
}
return {
...store,
active: { ...channel, epgParams: '' },
};
});
}
/**
* Sets the given timestamp for the epg program
* @param program epg program to set as active
*/
setActiveEpgProgram(program: EpgProgram): void {
const from = moment(
program['_attributes'].start,
'YYYYMMDDHHmm ZZ'
).unix();
const now = moment(Date.now()).unix();
const epgParams = `?utc=${from}&lutc=${now}`;
this.update((store) => ({
...store,
active: { ...store.active, epgParams },
}));
}
/**
* Sets the active channel from epg program back to the live translation
*/
resetActiveEpgProgram(): void {
this.update((store) => ({
...store,
active: { ...store.active, epgParams: '' },
}));
}
/**
* Updates the epg availability flag
* @param value
*/
setEpgAvailableFlag(value: boolean): void {
this.update((store) => {
if (store.active && store.active.name) {
this.electronService.sendIpcEvent(EPG_GET_PROGRAM, {
channelName: store.active.name,
});
}
return {
...store,
epgAvailable: value,
};
});
}
/**
* Updates the active epg program for the active channel
* @param currentEpgProgram program to set
*/
setCurrentEpgProgram(currentEpgProgram: EpgProgram): void {
this.update((store) => ({
...store,
currentEpgProgram,
}));
}
/**
* Sets the given playlist as active for the current session
* @param playlist playlist object
*/
setPlaylist(playlist: Playlist): void {
this.remove();
const favorites = playlist?.favorites || [];
const channels = playlist?.playlist.items.map((element) =>
createChannel(element)
);
this.upsertMany(channels);
this.update((store) => ({
...store,
active: undefined,
favorites,
playlistId: playlist._id,
playlistFilename: playlist.title || playlist.filename,
}));
}
}
+87
View File
@@ -0,0 +1,87 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { combineLatestWith, map } from 'rxjs/operators';
import {
CHANNEL_SET_USER_AGENT,
EPG_GET_PROGRAM,
PLAYLIST_UPDATE_FAVORITES,
} from '../../../shared/ipc-commands';
import { DataService } from '../services/data.service';
import * as PlaylistActions from './actions';
import {
selectActive,
selectFavorites,
selectIsEpgAvailable,
selectPlaylistId,
} from './selectors';
@Injectable({ providedIn: 'any' })
export class PlaylistEffects {
updateFavorites$ = createEffect(
() => {
return this.actions$.pipe(
ofType(PlaylistActions.updateFavorites),
combineLatestWith(
this.store.select(selectFavorites),
this.store.select(selectPlaylistId)
),
map(([action, favorites, playlistId]) => {
this.dataService.sendIpcEvent(PLAYLIST_UPDATE_FAVORITES, {
id: playlistId,
favorites,
});
})
);
},
{ dispatch: false }
);
setActiveChannel$ = createEffect(() => {
return this.actions$.pipe(
ofType(PlaylistActions.setActiveChannel),
combineLatestWith(this.store.select(selectIsEpgAvailable)),
map(([action, isEpgAvailable]) => {
const { channel } = action;
if (isEpgAvailable) {
this.dataService.sendIpcEvent(EPG_GET_PROGRAM, {
channel,
});
}
if (channel.http['user-agent']) {
this.dataService.sendIpcEvent(CHANNEL_SET_USER_AGENT, {
referer: channel.http.referrer,
userAgent: channel.http['user-agent'],
});
}
return PlaylistActions.setActiveChannelSuccess({
channel: action.channel,
});
})
);
});
setEpgAvailableFlag$ = createEffect(
() => {
return this.actions$.pipe(
ofType(PlaylistActions.setEpgAvailableFlag),
combineLatestWith(this.store.select(selectActive)),
map(([action, activeChannel]) => {
if (activeChannel && activeChannel.name) {
this.dataService.sendIpcEvent(EPG_GET_PROGRAM, {
channelName: activeChannel.name,
});
}
})
);
},
{ dispatch: false }
);
constructor(
private actions$: Actions,
private dataService: DataService,
private store: Store
) {}
}
-4
View File
@@ -1,4 +0,0 @@
export * from './channel.query';
export * from './channel.store';
export * from './channel.service';
export * from './channel.model';
+85
View File
@@ -0,0 +1,85 @@
import { createReducer, on } from '@ngrx/store';
import * as moment from 'moment';
import { createChannel } from '../shared/channel.model';
import * as PlaylistActions from './actions';
import { initialState, PlaylistState } from './state';
export const playlistReducer = createReducer(
initialState,
on(PlaylistActions.setActiveEpgProgram, (state, action): PlaylistState => {
const { program } = action;
const from = moment(program.start, 'YYYYMMDDHHmm ZZ').unix();
const now = moment(Date.now()).unix();
const epgParams = `?utc=${from}&lutc=${now}`;
return {
...state,
active: { ...state.active, epgParams },
};
}),
on(
PlaylistActions.resetActiveEpgProgram,
(state): PlaylistState => ({
...state,
active: { ...state.active, epgParams: '' },
})
),
on(
PlaylistActions.setActiveChannelSuccess,
(state, action): PlaylistState => {
const { channel } = action;
return {
...state,
active: { ...channel, epgParams: '' },
};
}
),
on(
PlaylistActions.setCurrentEpgProgram,
(state, action): PlaylistState => ({
...state,
currentEpgProgram: action.program,
})
),
on(
PlaylistActions.setEpgAvailableFlag,
(state, action): PlaylistState => ({
...state,
epgAvailable: action.value,
})
),
on(PlaylistActions.setPlaylist, (state, action): PlaylistState => {
const { playlist } = action;
return {
...state,
channels: playlist?.playlist.items.map((element) =>
createChannel(element)
),
favorites: playlist?.favorites || [],
active: undefined,
playlistId: playlist._id,
playlistFilename: playlist.title || playlist.filename,
};
}),
on(PlaylistActions.updateFavorites, (state, action): PlaylistState => {
let favorites;
const { channel } = action;
if (state.favorites.includes(channel.id)) {
favorites = [...state.favorites.filter((id) => id !== channel.id)];
} else {
favorites = [...state.favorites, channel.id];
}
return { ...state, favorites };
})
);
export const selectFavorites = (state: PlaylistState) => state.favorites;
export const selectPlaylistId = (state: PlaylistState) => state.playlistId;
export const selectPlaylistFilename = (state: PlaylistState) =>
state.playlistFilename;
export const selectIsEpgAvailable = (state: PlaylistState) =>
state.epgAvailable;
export const selectActive = (state: PlaylistState) => state.active;
export const selectCurrentEpgProgram = (state: PlaylistState) =>
state.currentEpgProgram;
export const selectChannels = (state: PlaylistState) => state.channels;
+41
View File
@@ -0,0 +1,41 @@
import { createFeatureSelector, createSelector } from '@ngrx/store';
import * as fromPlaylistState from './reducers';
import { PlaylistState } from './state';
export const selectPlaylistState =
createFeatureSelector<PlaylistState>('playlistState');
export const selectFavorites = createSelector(
selectPlaylistState,
fromPlaylistState.selectFavorites
);
export const selectPlaylistId = createSelector(
selectPlaylistState,
fromPlaylistState.selectPlaylistId
);
export const selectIsEpgAvailable = createSelector(
selectPlaylistState,
fromPlaylistState.selectIsEpgAvailable
);
export const selectPlaylistFilename = createSelector(
selectPlaylistState,
fromPlaylistState.selectPlaylistFilename
);
export const selectActive = createSelector(
selectPlaylistState,
fromPlaylistState.selectActive
);
export const selectCurrentEpgProgram = createSelector(
selectPlaylistState,
fromPlaylistState.selectCurrentEpgProgram
);
export const selectChannels = createSelector(
selectPlaylistState,
fromPlaylistState.selectChannels
);
+22
View File
@@ -0,0 +1,22 @@
import { Channel } from '../../../shared/channel.interface';
import { EpgProgram } from '../player/models/epg-program.model';
export interface PlaylistState {
active: Channel | undefined;
currentEpgProgram: EpgProgram | undefined;
epgAvailable: boolean;
favorites: string[];
playlistId: string;
playlistFilename: string;
channels: Channel[]; // TODO: use entity store
}
export const initialState: PlaylistState = {
active: undefined,
currentEpgProgram: undefined,
epgAvailable: false,
favorites: [],
playlistId: '',
playlistFilename: '',
channels: [],
};