Merge pull request #198 from 4gray/feat/ngrx

Switch from akita to ngrx
This commit is contained in:
4gray authored and GitHub committed 2022-12-07 22:58:59 +01:00
commit bfd7fbe679
32 files changed
+2155 -4247

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)
);
+3 -1
View File
@@ -3,11 +3,12 @@ import {
ComponentFixture,
inject,
TestBed,
waitForAsync,
waitForAsync
} from '@angular/core/testing';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { MockModule, MockPipe, MockProviders } from 'ng-mocks';
import { NgxWhatsNewModule } from 'ngx-whats-new';
@@ -50,6 +51,7 @@ describe('AppComponent', () => {
provide: DataService,
useClass: ElectronServiceStub,
},
provideMockStore(),
],
imports: [
MockModule(MatSnackBarModule),
+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: [
{
+12 -2
View File
@@ -5,10 +5,13 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTabsModule } from '@angular/material/tabs';
import { Router } from '@angular/router';
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, TranslateService } from '@ngx-translate/core';
import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks';
import { NgxIndexedDBModule, NgxIndexedDBService } from 'ngx-indexed-db';
import { of } from 'rxjs';
import { Observable, of } from 'rxjs';
import { FileUploadComponent } from '../home/file-upload/file-upload.component';
import { RecentPlaylistsComponent } from '../home/recent-playlists/recent-playlists.component';
import { UrlUploadComponent } from '../home/url-upload/url-upload.component';
@@ -17,7 +20,7 @@ import { ElectronServiceStub } from '../services/electron.service.stub';
import { HeaderComponent } from '../shared/components/header/header.component';
import {
PLAYLIST_PARSE,
PLAYLIST_PARSE_BY_URL,
PLAYLIST_PARSE_BY_URL
} from './../../../shared/ipc-commands';
import { HomeComponent } from './home.component';
@@ -36,6 +39,8 @@ describe('HomeComponent', () => {
let fixture: ComponentFixture<HomeComponent>;
let electronService: DataService;
let router: Router;
let mockStore: MockStore;
const actions$ = new Observable<Actions>();
beforeEach(() => {
TestBed.configureTestingModule({
@@ -63,6 +68,8 @@ describe('HomeComponent', () => {
provide: NgxIndexedDBService,
useClass: NgxIndexedDBServiceStub,
},
provideMockStore(),
provideMockActions(actions$),
],
}).compileComponents();
});
@@ -72,6 +79,9 @@ describe('HomeComponent', () => {
component = fixture.componentInstance;
electronService = TestBed.inject(DataService);
mockStore = TestBed.inject(MockStore);
mockStore.setState({});
router = TestBed.inject(Router);
TestBed.inject(NgxIndexedDBService);
fixture.detectChanges();
+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
) {
@@ -10,15 +10,17 @@ import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { By } from '@angular/platform-browser';
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 { Observable } from 'rxjs';
import * as MOCKED_PLAYLIST from '../../../../mocks/playlist.json';
import { DataService } from '../../../services/data.service';
import { ElectronServiceStub } from '../../../services/electron.service.stub';
import { createChannel } from '../../../shared/channel.model';
import { FilterPipe } from '../../../shared/pipes/filter.pipe';
import { createChannel } from '../../../state';
import { ChannelQuery } from '../../../state/channel.query';
import { ChannelStore } from '../../../state/channel.store';
import { ChannelListContainerComponent } from './channel-list-container.component';
class MatSnackBarStub {
@@ -28,7 +30,8 @@ class MatSnackBarStub {
describe('ChannelListContainerComponent', () => {
let component: ChannelListContainerComponent;
let fixture: ComponentFixture<ChannelListContainerComponent>;
let store: ChannelStore;
let mockStore: MockStore;
const actions$ = new Observable<Actions>();
beforeEach(() => {
TestBed.configureTestingModule({
@@ -37,9 +40,10 @@ describe('ChannelListContainerComponent', () => {
MockPipes(TranslatePipe, FilterPipe),
],
providers: [
ChannelQuery,
{ provide: MatSnackBar, useClass: MatSnackBarStub },
{ provide: DataService, useClass: ElectronServiceStub },
provideMockStore(),
provideMockActions(actions$),
],
imports: [
MockModule(MatSnackBarModule),
@@ -59,25 +63,22 @@ describe('ChannelListContainerComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ChannelListContainerComponent);
component = fixture.componentInstance;
TestBed.inject(ChannelQuery);
store = TestBed.inject(ChannelStore);
store.update({
favorites: [],
playlistId: '',
active: undefined,
});
mockStore = TestBed.inject(MockStore);
// set channels
const channels = MOCKED_PLAYLIST.playlist.items.map((element) =>
createChannel(element)
);
store.upsertMany(channels);
component.channelList = channels;
// set favorites
store.update({
favorites: [MOCKED_PLAYLIST.playlist.items[0].url],
mockStore.setState({
playlistState: {
channels,
favorites: [MOCKED_PLAYLIST.playlist.items[0].url],
playlistId: '',
active: undefined,
},
});
component.channelList = channels;
fixture.detectChanges();
});
@@ -144,22 +145,22 @@ describe('ChannelListContainerComponent', () => {
});
it('should update store after channel was selected', () => {
jest.spyOn(store, 'update');
jest.spyOn(mockStore, 'dispatch');
component.selectChannel(component._channelList[0]);
fixture.detectChanges();
expect(store.update).toHaveBeenCalledTimes(1);
expect(mockStore.dispatch).toHaveBeenCalledTimes(1);
});
it('should update store after channel was favorited', () => {
jest.spyOn(store, 'updateFavorite');
jest.spyOn(mockStore, 'dispatch');
component.toggleFavoriteChannel(
component._channelList[0],
new MouseEvent('click')
);
fixture.detectChanges();
expect(store.updateFavorite).toHaveBeenCalledWith(
component._channelList[0]
expect(mockStore.dispatch).toHaveBeenCalledWith(
{channel: component._channelList[0], type: expect.stringContaining('favorites')},
);
expect(store.updateFavorite).toHaveBeenCalledTimes(1);
expect(mockStore.dispatch).toHaveBeenCalledTimes(1);
});
});
@@ -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>
@@ -4,15 +4,18 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
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 } from '@ngx-translate/core';
import * as moment from 'moment';
import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks';
import { Observable } from 'rxjs';
import { Channel } from '../../../../../shared/channel.interface';
import { EPG_GET_PROGRAM_DONE } from '../../../../../shared/ipc-commands';
import { DataService } from '../../../services/data.service';
import { ElectronServiceStub } from '../../../services/electron.service.stub';
import { MomentDatePipe } from '../../../shared/pipes/moment-date.pipe';
import { ChannelStore } from '../../../state';
import { EpgListItemComponent } from './epg-list-item/epg-list-item.component';
import { EpgData, EpgListComponent } from './epg-list.component';
@@ -20,7 +23,8 @@ describe('EpgListComponent', () => {
let component: EpgListComponent;
let fixture: ComponentFixture<EpgListComponent>;
let electronService: DataService;
let channelStore: ChannelStore;
let mockStore: MockStore;
const actions$ = new Observable<Actions>();
const MOCKED_PROGRAMS = {
channel: {
@@ -103,6 +107,8 @@ describe('EpgListComponent', () => {
providers: [
{ provide: DataService, useClass: ElectronServiceStub },
MockProvider(MatDialog),
provideMockStore(),
provideMockActions(actions$),
],
}).compileComponents();
})
@@ -112,16 +118,21 @@ describe('EpgListComponent', () => {
fixture = TestBed.createComponent(EpgListComponent);
component = fixture.componentInstance;
electronService = TestBed.inject(DataService);
channelStore = TestBed.inject(ChannelStore);
channelStore.setActiveChannel({
id: '',
url: '',
name: '',
group: { title: '' },
tvg: {
rec: '3',
mockStore = TestBed.inject(MockStore);
mockStore.setState({
playlistState: {
active: {
id: '',
url: '',
name: '',
group: { title: '' },
tvg: {
rec: '3',
},
} as unknown as Channel,
},
} as unknown as Channel);
});
fixture.detectChanges();
});
@@ -158,19 +169,19 @@ describe('EpgListComponent', () => {
});
it('should set epg program as active', () => {
jest.spyOn(channelStore, 'setActiveEpgProgram');
jest.spyOn(mockStore, 'dispatch');
component.setEpgProgram(MOCKED_PROGRAMS.items[0], false, true);
expect(channelStore.setActiveEpgProgram).toHaveBeenCalledTimes(1);
expect(channelStore.setActiveEpgProgram).toHaveBeenCalledWith(
MOCKED_PROGRAMS.items[0]
expect(mockStore.dispatch).toHaveBeenCalledTimes(1);
expect(mockStore.dispatch).toHaveBeenCalledWith(
{program: MOCKED_PROGRAMS.items[0], type: expect.stringContaining('epg program')}
);
});
it('should reset active epg program', () => {
jest.spyOn(channelStore, 'resetActiveEpgProgram');
jest.spyOn(mockStore, 'dispatch');
component.setEpgProgram(MOCKED_PROGRAMS.items[0], true);
expect(channelStore.resetActiveEpgProgram).toHaveBeenCalledTimes(1);
expect(mockStore.dispatch).toHaveBeenCalledTimes(1);
component.setEpgProgram(MOCKED_PROGRAMS.items[0], true, true);
expect(channelStore.resetActiveEpgProgram).toHaveBeenCalledTimes(2);
expect(mockStore.dispatch).toHaveBeenCalledTimes(2);
});
});
@@ -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;
}
@@ -7,14 +7,14 @@ import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
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 { DataService } from '../../../services/data.service';
import { ElectronServiceStub } from '../../../services/electron.service.stub';
import { VideoPlayer } from '../../../settings/settings.interface';
import { createChannel } from '../../../state';
import { ChannelStore } from '../../../state/channel.store';
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';
@@ -22,6 +22,10 @@ import { VjsPlayerComponent } from '../vjs-player/vjs-player.component';
import { InfoOverlayComponent } from './../info-overlay/info-overlay.component';
import { VideoPlayerComponent } from './video-player.component';
import { Actions } from '@ngrx/effects';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable } from 'rxjs';
class MatSnackBarStub {
open(): void {}
}
@@ -29,8 +33,9 @@ class MatSnackBarStub {
describe('VideoPlayerComponent', () => {
let component: VideoPlayerComponent;
let fixture: ComponentFixture<VideoPlayerComponent>;
let store: ChannelStore;
let channels;
let mockStore: MockStore;
const actions$ = new Observable<Actions>();
beforeEach(
waitForAsync(() => {
@@ -58,6 +63,8 @@ describe('VideoPlayerComponent', () => {
},
},
},
provideMockStore(),
provideMockActions(actions$),
],
imports: [
MockModule(MatSidenavModule),
@@ -74,12 +81,14 @@ describe('VideoPlayerComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(VideoPlayerComponent);
component = fixture.componentInstance;
store = TestBed.inject(ChannelStore);
mockStore = TestBed.inject(MockStore);
// set channels
channels = MOCKED_PLAYLIST.playlist.items.map((element) =>
createChannel(element)
);
store.upsertMany(channels);
mockStore.setState({ playlistState: {channels} });
});
it('should create and init component', () => {
@@ -98,11 +107,10 @@ describe('VideoPlayerComponent', () => {
});
it('should update store after channel was faved', () => {
jest.spyOn(store, 'updateFavorite');
jest.spyOn(mockStore, 'dispatch');
const [firstChannel] = channels;
component.addToFavorites(firstChannel);
//fixture.detectChanges();
expect(store.updateFavorite).toHaveBeenCalledTimes(1);
expect(store.updateFavorite).toHaveBeenCalledWith(firstChannel);
expect(mockStore.dispatch).toHaveBeenCalledTimes(1);
expect(mockStore.dispatch).toHaveBeenCalledWith({channel: firstChannel, type: expect.stringContaining('favorite')});
});
});
@@ -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 }));
}
/**
+6 -5
View File
@@ -1,8 +1,8 @@
import { inject, TestBed } from '@angular/core/testing';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { TranslateService } from '@ngx-translate/core';
import { MockModule, MockProviders } from 'ng-mocks';
import { ChannelStore } from '../state';
import { DataService } from './data.service';
import { EpgService } from './epg.service';
@@ -12,6 +12,7 @@ describe('EpgService', () => {
providers: [
EpgService,
MockProviders(DataService, TranslateService, MatSnackBar),
provideMockStore()
],
imports: [MockModule(MatSnackBarModule)],
});
@@ -34,17 +35,17 @@ describe('EpgService', () => {
));
it('should handle epg download success', inject(
[MatSnackBar, ChannelStore, EpgService],
[MatSnackBar, MockStore, EpgService],
(
snackbar: MatSnackBar,
channelStore: ChannelStore,
channelStore: MockStore,
service: EpgService
) => {
jest.spyOn(snackbar, 'open');
jest.spyOn(channelStore, 'setEpgAvailableFlag');
jest.spyOn(channelStore, 'dispatch');
service.onEpgFetchDone();
expect(snackbar.open).toHaveBeenCalledTimes(1);
expect(channelStore.setEpgAvailableFlag).toHaveBeenCalledWith(true);
expect(channelStore.dispatch).toHaveBeenCalledWith({value: true, type: expect.stringContaining('active epg')});
}
));
});
+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,
})),
},
+6 -2
View File
@@ -1,6 +1,6 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { UntypedFormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { FormsModule, ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDividerModule } from '@angular/material/divider';
@@ -12,12 +12,12 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { StorageMap } from '@ngx-pwa/local-storage';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { MockComponent, MockModule, MockPipe } from 'ng-mocks';
import { of } from 'rxjs';
import { EPG_FETCH } from '../../../shared/ipc-commands';
/* eslint-disable @typescript-eslint/unbound-method */
import { DataService } from '../services/data.service';
import { ElectronServiceStub } from '../services/electron.service.stub';
import { TranslateServiceStub } from './../../testing/translate.stub';
@@ -52,6 +52,7 @@ describe('SettingsComponent', () => {
let router: Router;
let storage: StorageMap;
let translate: TranslateService;
let mockStore: MockStore;
beforeEach(
waitForAsync(() => {
@@ -74,6 +75,7 @@ describe('SettingsComponent', () => {
useClass: MockRouter,
},
StorageMap,
provideMockStore(),
],
imports: [
HttpClientTestingModule,
@@ -99,6 +101,8 @@ describe('SettingsComponent', () => {
storage = TestBed.inject(StorageMap);
router = TestBed.inject(Router);
translate = TestBed.inject(TranslateService);
mockStore = TestBed.inject(MockStore);
component = fixture.componentInstance;
fixture.detectChanges();
});
+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: [],
};