mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
refactor: switch from akita to ngrx
This commit is contained in:
1 parent
9b557690c1
commit
466bbade53
25 files changed
+2058
-4187
No files matched your search
+13
-3
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
+1617
-3893
File diff suppressed because it is too large.
Load diff
+17
-14
@@ -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,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;
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
@@ -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'
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
+24
-21
@@ -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 }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
},
|
||||
|
||||
@@ -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.
@@ -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 }>()
|
||||
);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ChannelService {
|
||||
constructor() {}
|
||||
}
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './channel.query';
|
||||
export * from './channel.store';
|
||||
export * from './channel.service';
|
||||
export * from './channel.model';
|
||||
@@ -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;
|
||||
@@ -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
|
||||
);
|
||||
@@ -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: [],
|
||||
};
|
||||
Reference in new issue
Block a user