diff --git a/src/app/state/actions.ts b/src/app/state/actions.ts index b933c92b7..17974ec53 100644 --- a/src/app/state/actions.ts +++ b/src/app/state/actions.ts @@ -2,8 +2,59 @@ 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'; +import { PlaylistMeta } from '../shared/playlist-meta.type'; -const STORE_KEY = '[PLAYLIST STORE]'; +const STORE_KEY = '[GLOBAL STORE]'; +const PLAYLISTS_STORE_KEY = '[PLAYLISTS STORE]'; + +export const loadPlaylists = createAction( + `${PLAYLISTS_STORE_KEY} Load playlists from db` +); + +export const loadPlaylistsSuccess = createAction( + `${PLAYLISTS_STORE_KEY} Successfully loaded playlists from db`, + props<{ playlists: PlaylistMeta[] }>() +); + +export const addPlaylist = createAction( + `${PLAYLISTS_STORE_KEY} Add new playlist`, + props<{ playlist: Playlist }>() +); + +export const addManyPlaylists = createAction( + `${PLAYLISTS_STORE_KEY} Add many playlists`, + props<{ playlists: Playlist[] }>() +); + +export const removePlaylist = createAction( + `${PLAYLISTS_STORE_KEY} Remove playlist by id`, + props<{ playlistId: string }>() +); + +export const updatePlaylistMeta = createAction( + `${PLAYLISTS_STORE_KEY} update playlist meta`, + props<{ playlist: PlaylistMeta }>() +); + +export const updatePlaylist = createAction( + `${PLAYLISTS_STORE_KEY} update playlist`, + props<{ playlist: Playlist; playlistId: string }>() +); + +export const parsePlaylist = createAction( + `${PLAYLISTS_STORE_KEY} parse playlist`, + props<{ + uploadType: 'FILE' | 'URL' | 'TEXT'; + playlist: string; + title: string; + path?: string; + }>() +); + +export const setActivePlaylist = createAction( + `${PLAYLISTS_STORE_KEY} set active playlist`, + props<{ playlistId: string }>() +); export const updateFavorites = createAction( `${STORE_KEY} Add/remove provided channel to the favorites`, @@ -43,3 +94,10 @@ export const setPlaylist = createAction( `${STORE_KEY} Set playlist as active`, props<{ playlist: Playlist }>() ); + +export const updatePlaylistPositions = createAction( + `${STORE_KEY} Update playlist positions`, + props<{ + positionUpdates: { id: string; changes: { position: number } }[]; + }>() +); diff --git a/src/app/state/effects.ts b/src/app/state/effects.ts index a15513d33..c046c65e5 100644 --- a/src/app/state/effects.ts +++ b/src/app/state/effects.ts @@ -1,19 +1,20 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; -import { combineLatestWith, map } from 'rxjs/operators'; +import { combineLatestWith, map, switchMap, tap } 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 { PlaylistsService } from '../services/playlists.service'; import * as PlaylistActions from './actions'; import { + selectActivePlaylistId, selectFavorites, selectIsEpgAvailable, - selectPlaylistId, } from './selectors'; @Injectable({ providedIn: 'any' }) @@ -24,14 +25,11 @@ export class PlaylistEffects { ofType(PlaylistActions.updateFavorites), combineLatestWith( this.store.select(selectFavorites), - this.store.select(selectPlaylistId) + this.store.select(selectActivePlaylistId) ), - map(([action, favorites, playlistId]) => { - this.dataService.sendIpcEvent(PLAYLIST_UPDATE_FAVORITES, { - id: playlistId, - favorites, - }); - }) + switchMap(([action, favorites, playlistId]) => + this.playlistsService.updateFavorites(playlistId, favorites) + ) ); }, { dispatch: false } @@ -61,9 +59,125 @@ export class PlaylistEffects { ); }); + loadPlaylists$ = createEffect(() => { + return this.actions$.pipe( + ofType(PlaylistActions.loadPlaylists), + switchMap(() => + this.playlistsService.getAllPlaylists().pipe( + map((playlists) => + PlaylistActions.loadPlaylistsSuccess({ + playlists, + }) + ) + ) + ) + ); + }); + + removePlaylist$ = createEffect( + () => { + return this.actions$.pipe( + ofType(PlaylistActions.removePlaylist), + switchMap((action) => + this.playlistsService.deletePlaylist(action.playlistId) + ) + ); + }, + { dispatch: false } + ); + + updatePlaylist$ = createEffect( + () => { + return this.actions$.pipe( + ofType(PlaylistActions.updatePlaylist), + switchMap((action) => + this.playlistsService.updatePlaylist(action.playlistId, { + ...action.playlist, + _id: action.playlistId, + }) + ) + ); + }, + { dispatch: false } + ); + + parsePlaylist$ = createEffect(() => { + return this.actions$.pipe( + ofType(PlaylistActions.parsePlaylist), + map((action) => + this.playlistsService.handlePlaylistParsing( + action.uploadType, + action.playlist, + action.title, + action.path + ) + ), + map((playlist) => + PlaylistActions.addPlaylist({ + playlist, + }) + ) + ); + }); + + addPlaylist$ = createEffect( + () => { + return this.actions$.pipe( + ofType(PlaylistActions.addPlaylist), + switchMap((action) => + this.playlistsService.addPlaylist(action.playlist) + ), + tap((playlist) => { + this.router.navigate(['/playlists/', playlist._id]); + }) + ); + }, + { dispatch: false } + ); + + updatePlaylistMeta$ = createEffect( + () => { + return this.actions$.pipe( + ofType(PlaylistActions.updatePlaylistMeta), + switchMap((action) => + this.playlistsService.updatePlaylistMeta(action.playlist) + ) + ); + }, + { dispatch: false } + ); + + updatePlaylistPositions$ = createEffect( + () => { + return this.actions$.pipe( + ofType(PlaylistActions.updatePlaylistPositions), + switchMap((action) => + this.playlistsService.updatePlaylistPositions( + action.positionUpdates + ) + ) + ); + }, + { dispatch: false } + ); + + addManyPlaylists$ = createEffect( + () => { + return this.actions$.pipe( + ofType(PlaylistActions.addManyPlaylists), + switchMap((action) => + this.playlistsService.addManyPlaylists(action.playlists) + ) + ); + }, + { dispatch: false } + ); + constructor( private actions$: Actions, + private playlistsService: PlaylistsService, private dataService: DataService, - private store: Store + private store: Store, + private router: Router ) {} } diff --git a/src/app/state/playlists.state.ts b/src/app/state/playlists.state.ts new file mode 100644 index 000000000..630b47045 --- /dev/null +++ b/src/app/state/playlists.state.ts @@ -0,0 +1,24 @@ +import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; +import { Playlist } from '../../../shared/playlist.interface'; +import { PlaylistMeta } from '../shared/playlist-meta.type'; + +export const playlistsAdapter: EntityAdapter = + createEntityAdapter({ + selectId: (item: Playlist) => item._id, + }); + +export interface PlaylistMetaState extends EntityState { + selectedId: string; +} + +export const initialPlaylistMetaState: PlaylistMetaState = + playlistsAdapter.getInitialState({ + selectedId: '', + }); + +export const { + selectIds, + selectEntities: getPlaylistMetaEntities, + selectAll: getAllPlaylistsMeta, + selectTotal, +} = playlistsAdapter.getSelectors(); diff --git a/src/app/state/reducers.ts b/src/app/state/reducers.ts index db592ea0a..aceb06ce1 100644 --- a/src/app/state/reducers.ts +++ b/src/app/state/reducers.ts @@ -1,7 +1,7 @@ import { createReducer, on } from '@ngrx/store'; import * as moment from 'moment'; -import { createChannel } from '../shared/channel.model'; import * as PlaylistActions from './actions'; +import { playlistsAdapter } from './playlists.state'; import { initialState, PlaylistState } from './state'; export const playlistReducer = createReducer( @@ -47,39 +47,135 @@ export const playlistReducer = createReducer( epgAvailable: action.value, }) ), - on(PlaylistActions.setPlaylist, (state, action): PlaylistState => { - const { playlist } = action; + /* 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)]; + const playlistFavorites = + state.playlists.entities[state.playlists.selectedId].favorites; + if (playlistFavorites.includes(channel.id)) { + favorites = [ + ...playlistFavorites.filter((id) => id !== channel.id), + ]; } else { - favorites = [...state.favorites, channel.id]; + favorites = [...playlistFavorites, channel.id]; } - return { ...state, favorites }; + return { + ...state, + playlists: { + ...state.playlists, + entities: { + ...state.playlists.entities, + [state.playlists.selectedId]: { + ...state.playlists.entities[state.playlists.selectedId], + favorites, + }, + }, + }, + }; + }), + on(PlaylistActions.loadPlaylistsSuccess, (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.upsertMany( + action.playlists, + state.playlists + ), + }; + }), + on(PlaylistActions.removePlaylist, (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.removeOne( + action.playlistId, + state.playlists + ), + }; + }), + on(PlaylistActions.updatePlaylist, (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.updateOne( + { + id: action.playlistId, + changes: { ...action.playlist, _id: action.playlistId }, + }, + state.playlists + ), + }; + }), + on(PlaylistActions.addPlaylist, (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.addOne( + action.playlist, + state.playlists + ), + }; + }), + on(PlaylistActions.addManyPlaylists, (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.addMany( + action.playlists, + state.playlists + ), + }; + }), + on(PlaylistActions.setActivePlaylist, (state, action): PlaylistState => { + return { + ...state, + playlists: { + ...state.playlists, + selectedId: action.playlistId, + }, + }; + }), + on( + PlaylistActions.updatePlaylistPositions, + (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.updateMany( + action.positionUpdates, + state.playlists + ), + }; + } + ), + on(PlaylistActions.updatePlaylistMeta, (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.updateOne( + { + id: action.playlist._id, + changes: { + title: action.playlist.title, + autoRefresh: action.playlist.autoRefresh || false, + }, + }, + state.playlists + ), + }; }) ); -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; +export const selectPlaylists = (state: PlaylistState) => state.playlists; +export const selectPlaylistId = (state: PlaylistState) => + state.playlists?.selectedId; diff --git a/src/app/state/selectors.ts b/src/app/state/selectors.ts index f9280915d..5f7806766 100644 --- a/src/app/state/selectors.ts +++ b/src/app/state/selectors.ts @@ -1,30 +1,16 @@ import { createFeatureSelector, createSelector } from '@ngrx/store'; +import * as fromPlaylistMetaState from './playlists.state'; import * as fromPlaylistState from './reducers'; import { PlaylistState } from './state'; export const selectPlaylistState = createFeatureSelector('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 @@ -39,3 +25,67 @@ export const selectChannels = createSelector( selectPlaylistState, fromPlaylistState.selectChannels ); + +/** Playlist entity store selectors */ +export const selectPlaylistsMetaState = createSelector( + selectPlaylistState, + fromPlaylistState.selectPlaylists +); + +export const selectAllPlaylistsMeta = createSelector( + selectPlaylistsMetaState, + fromPlaylistMetaState.getAllPlaylistsMeta +); + +export const selectPlaylistEntity = (id: string) => + createSelector( + selectPlaylistsMetaState, + fromPlaylistMetaState.getPlaylistMetaEntities, + (data) => { + return data.entities[id]; + } + ); + +export const selectActivePlaylistId = createSelector( + selectPlaylistsMetaState, + fromPlaylistMetaState.getPlaylistMetaEntities, + (data) => data.selectedId +); + +export const selectPlaylistTitle = createSelector( + selectPlaylistsMetaState, + fromPlaylistMetaState.getPlaylistMetaEntities, + fromPlaylistState.selectPlaylistId, + (data) => { + if ( + data.entities && + data.selectedId && + data.entities[data.selectedId] + ) { + return ( + data.entities[data.selectedId].title || + data.entities[data.selectedId].filename + ); + } else return 'Untitled playlist'; + } +); + +export const selectPlaylistEntities = createSelector( + selectPlaylistsMetaState, + fromPlaylistMetaState.getPlaylistMetaEntities +); + +export const selectFavorites = createSelector( + selectPlaylistsMetaState, + fromPlaylistMetaState.getPlaylistMetaEntities, + fromPlaylistState.selectPlaylistId, + (data) => { + if ( + data.entities && + data.selectedId && + data.entities[data.selectedId] + ) { + return data.entities[data.selectedId].favorites || []; + } else return []; + } +); diff --git a/src/app/state/state.ts b/src/app/state/state.ts index 4e06b6bf9..cbca3d34c 100644 --- a/src/app/state/state.ts +++ b/src/app/state/state.ts @@ -1,22 +1,20 @@ import { Channel } from '../../../shared/channel.interface'; +import { initialPlaylistMetaState, PlaylistMetaState } from './playlists.state'; + 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 + playlists: PlaylistMetaState; } export const initialState: PlaylistState = { active: undefined, currentEpgProgram: undefined, epgAvailable: false, - favorites: [], - playlistId: '', - playlistFilename: '', channels: [], + playlists: initialPlaylistMetaState, };