refactor: streamline playlist management and navigation logic

This commit is contained in:
4gray committed 2026-03-28 21:41:49 +01:00
1 parent 2bfa557438
commit 0c9fd18677
9 files changed
+111 -125

No files matched your search

-1
View File
@@ -23,7 +23,6 @@ export const PlaylistActions = createActionGroup({
positionUpdates: { id: string; changes: { position: number } }[];
}>(),
'Remove All Playlists': emptyProps(),
'Set Current Playlist Id': props<{ playlistId: string | undefined }>(),
'Handle Adding Playlist By Url': props<{
isTemporary: boolean;
playlist: Playlist;
+21 -19
View File
@@ -268,30 +268,18 @@ export class PlaylistEffects {
PlaylistActions.addPlaylist,
PlaylistActions.handleAddingPlaylistByUrl
),
tap((action) => {
if ('isTemporary' in action && action.isTemporary) {
return;
}
this.navigateToPlaylist(action.playlist);
}),
switchMap((action) => {
if ('isTemporary' in action && action.isTemporary) {
return EMPTY;
}
return this.playlistsService.addPlaylist(action.playlist);
}),
map((playlist: Playlist) => {
if (playlist.serverUrl) {
this.router.navigate([
'/workspace/xtreams/',
playlist._id,
]);
} else if (playlist.macAddress) {
this.router.navigate([
'/workspace/stalker/',
playlist._id,
]);
} else {
this.router.navigate([
'/workspace/playlists/',
playlist._id,
]);
}
return playlist;
})
);
},
@@ -375,4 +363,18 @@ export class PlaylistEffects {
})
);
});
private navigateToPlaylist(playlist: Playlist): void {
if (playlist.serverUrl) {
void this.router.navigate(['/workspace', 'xtreams', playlist._id]);
return;
}
if (playlist.macAddress) {
void this.router.navigate(['/workspace', 'stalker', playlist._id]);
return;
}
void this.router.navigate(['/workspace', 'playlists', playlist._id]);
}
}
@@ -4,27 +4,28 @@ import { PlaylistState } from '../state';
export const favoritesReducers = [
on(FavoritesActions.updateFavorites, (state, action): PlaylistState => {
let favorites;
const { channel } = action;
const playlistFavorites =
state.playlists.entities[state.playlists.selectedId]?.favorites;
if (playlistFavorites?.includes(channel.url)) {
favorites = [
...(playlistFavorites ?? []).filter(
(url: string) => url !== channel.url
),
];
} else {
favorites = [...(playlistFavorites ?? []), channel.url];
const selectedId = state.playlists.selectedId;
const playlist = state.playlists.entities[selectedId];
if (!selectedId || !playlist) {
return state;
}
const { channel } = action;
const playlistFavorites = (playlist.favorites ?? []).filter(
(favorite): favorite is string => typeof favorite === 'string'
);
const favorites = playlistFavorites.includes(channel.url)
? playlistFavorites.filter((url) => url !== channel.url)
: [...playlistFavorites, channel.url];
return {
...state,
playlists: {
...state.playlists,
entities: {
...state.playlists.entities,
[state.playlists.selectedId]: {
...state.playlists.entities[state.playlists.selectedId],
[selectedId]: {
...playlist,
favorites,
},
},
@@ -32,6 +33,12 @@ export const favoritesReducers = [
};
}),
on(FavoritesActions.setFavorites, (state, action): PlaylistState => {
const selectedId = state.playlists.selectedId;
const playlist = state.playlists.entities[selectedId];
if (!selectedId || !playlist) {
return state;
}
const { channelIds } = action;
return {
...state,
@@ -39,8 +46,8 @@ export const favoritesReducers = [
...state.playlists,
entities: {
...state.playlists.entities,
[state.playlists.selectedId]: {
...state.playlists.entities[state.playlists.selectedId],
[selectedId]: {
...playlist,
favorites: channelIds,
},
},
-2
View File
@@ -28,5 +28,3 @@ export const selectCurrentEpgProgramReducer = (state: PlaylistState) =>
export const selectChannelsReducer = (state: PlaylistState) => state.channels;
export const selectPlaylistsLoadingFlagReducer = (state: PlaylistState) =>
state.playlists?.allPlaylistsLoaded;
export const selectCurrentPlaylistIdReducer = (state: PlaylistState) =>
state.currentPlaylistId;
@@ -15,12 +15,19 @@ export const playlistReducers = [
};
}),
on(PlaylistActions.removePlaylist, (state, action): PlaylistState => {
const playlists = playlistsAdapter.removeOne(
action.playlistId,
state.playlists
);
return {
...state,
playlists: playlistsAdapter.removeOne(
action.playlistId,
state.playlists
),
playlists: {
...playlists,
selectedId:
state.playlists.selectedId === action.playlistId
? ''
: playlists.selectedId,
},
};
}),
on(PlaylistActions.updatePlaylist, (state, action): PlaylistState => {
@@ -137,20 +144,15 @@ export const playlistReducers = [
};
}),
on(PlaylistActions.removeAllPlaylists, (state): PlaylistState => {
const playlists = playlistsAdapter.removeAll(state.playlists);
return {
...state,
playlists: playlistsAdapter.removeAll(state.playlists),
playlists: {
...playlists,
selectedId: '',
},
};
}),
on(
PlaylistActions.setCurrentPlaylistId,
(state, { playlistId }): PlaylistState => {
return {
...state,
currentPlaylistId: playlistId,
};
}
),
on(
PlaylistActions.handleAddingPlaylistByUrl,
(state, action): PlaylistState => {
+29
View File
@@ -0,0 +1,29 @@
import { PlaylistMeta } from 'shared-interfaces';
import { selectActivePlaylist } from './selectors';
describe('selectors', () => {
const playlistOne = {
_id: 'playlist-1',
title: 'Playlist One',
} as PlaylistMeta;
const playlistTwo = {
_id: 'playlist-2',
title: 'Playlist Two',
} as PlaylistMeta;
const entities = {
'playlist-1': playlistOne,
'playlist-2': playlistTwo,
};
describe('selectActivePlaylist', () => {
it('resolves the active playlist from entity state and selected id', () => {
expect(selectActivePlaylist.projector(entities, 'playlist-2')).toBe(
playlistTwo
);
});
it('returns null when there is no active playlist id', () => {
expect(selectActivePlaylist.projector(entities, '')).toBeNull();
});
});
});
+21 -71
View File
@@ -1,9 +1,7 @@
import { EntityState } from '@ngrx/entity';
import { createFeatureSelector, createSelector, Selector } from '@ngrx/store';
import { Playlist, PlaylistMeta } from 'shared-interfaces';
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { Playlist } from 'shared-interfaces';
import * as fromPlaylistMetaState from './playlists.state';
import * as fromPlaylistState from './reducers';
import { selectRouteParam } from './router.selectors';
import { PlaylistState } from './state';
export const selectPlaylistState =
@@ -24,11 +22,6 @@ export const selectCurrentEpgProgram = createSelector(
fromPlaylistState.selectCurrentEpgProgramReducer
);
export const selectCurrentPlaylistId = createSelector(
selectPlaylistState,
fromPlaylistState.selectCurrentPlaylistIdReducer
);
export const selectChannels = createSelector(
selectPlaylistState,
fromPlaylistState.selectChannelsReducer
@@ -52,41 +45,15 @@ export const selectAllPlaylistsMeta = createSelector(
export const selectActiveTypeFilters = createSelector(
selectPlaylistsMetaState,
fromPlaylistMetaState.getPlaylistMetaEntities,
(state) => state.selectedFilters
);
export const selectPlaylistEntity = (id: string) =>
createSelector(
selectPlaylistsMetaState,
fromPlaylistMetaState.getPlaylistMetaEntities,
(data) => {
return data.entities[id];
}
);
createSelector(selectPlaylistEntities, (entities) => entities[id]);
export const selectActivePlaylistId = createSelector(
selectPlaylistsMetaState,
fromPlaylistMetaState.getPlaylistMetaEntities,
(data) => data.selectedId
);
export const selectPlaylistTitle = createSelector(
selectPlaylistsMetaState,
fromPlaylistMetaState.getPlaylistMetaEntities,
selectCurrentPlaylistId,
(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';
}
(state) => state.selectedId
);
export const selectPlaylistEntities = createSelector(
@@ -94,18 +61,23 @@ export const selectPlaylistEntities = createSelector(
fromPlaylistMetaState.getPlaylistMetaEntities
);
export const selectCurrentPlaylist = createSelector(
export const selectActivePlaylist = createSelector(
selectPlaylistEntities,
selectRouteParam('id'),
selectCurrentPlaylistId,
(entities, id, currentPlaylistId) => {
if (entities) {
return entities[id!] || entities[currentPlaylistId!];
selectActivePlaylistId,
(entities, activePlaylistId) => {
if (!entities || !activePlaylistId) {
return null;
}
return null;
return entities[activePlaylistId] ?? null;
}
);
export const selectPlaylistTitle = createSelector(
selectActivePlaylist,
(playlist) => playlist?.title || playlist?.filename || 'Untitled playlist'
);
export const selectPlaylistById = (id: string) =>
createSelector(selectPlaylistEntities, (entities) => {
if (entities) {
@@ -114,32 +86,10 @@ export const selectPlaylistById = (id: string) =>
return null;
});
export const selectActivePlaylist = createSelector(
selectPlaylistsMetaState,
(state) => {
if (state.entities && state.selectedId !== '') {
return state.entities[state.selectedId] as Playlist;
}
return null;
}
);
export const selectFavorites = createSelector(
selectPlaylistsMetaState,
fromPlaylistMetaState.getPlaylistMetaEntities,
fromPlaylistState.selectPlaylistId as unknown as Selector<
EntityState<PlaylistMeta>,
string
>,
(data) => {
if (
data.entities &&
data.selectedId &&
data.entities[data.selectedId]
) {
return (data.entities[data.selectedId]?.favorites || []).filter(
(f): f is string => typeof f === 'string'
);
} else return [];
}
selectActivePlaylist,
(playlist) =>
(playlist?.favorites || []).filter(
(favorite): favorite is string => typeof favorite === 'string'
)
);
-2
View File
@@ -9,7 +9,6 @@ export interface PlaylistState {
epgAvailable: boolean;
channels: Channel[]; // TODO: use entity store
playlists: PlaylistMetaState;
currentPlaylistId: string | undefined;
}
export const initialState: PlaylistState = {
@@ -18,5 +17,4 @@ export const initialState: PlaylistState = {
epgAvailable: false,
channels: [],
playlists: initialPlaylistMetaState,
currentPlaylistId: undefined,
};
@@ -1,2 +1,3 @@
export * from './lib/m3u-collection-route/m3u-collection-route.component';
export * from './lib/m3u-workspace-route-session.service';
export * from './lib/video-player/video-player.component';