refactor: refactor state management

This commit is contained in:
4gray committed 2023-02-22 14:09:26 +01:00
1 parent 96e13d0afb
commit c4e69adef2
6 files changed
+388 -48

No files matched your search

+59 -1
View File
@@ -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 } }[];
}>()
);
+125 -11
View File
@@ -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
) {}
}
+24
View File
@@ -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<PlaylistMeta> =
createEntityAdapter<PlaylistMeta>({
selectId: (item: Playlist) => item._id,
});
export interface PlaylistMetaState extends EntityState<PlaylistMeta> {
selectedId: string;
}
export const initialPlaylistMetaState: PlaylistMetaState =
playlistsAdapter.getInitialState({
selectedId: '',
});
export const {
selectIds,
selectEntities: getPlaylistMetaEntities,
selectAll: getAllPlaylistsMeta,
selectTotal,
} = playlistsAdapter.getSelectors();
+111 -15
View File
@@ -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;
+65 -15
View File
@@ -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>('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 [];
}
);
+4 -6
View File
@@ -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,
};