-
Notifications
You must be signed in to change notification settings - Fork 0
/
usePlaylistReducer.ts
118 lines (99 loc) · 3.46 KB
/
usePlaylistReducer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import * as React from 'react';
import { Playlist, PlaylistItem, ItemId } from './playlistModels';
interface State {
playlist: Playlist;
currentlyPlaying: ItemId | null;
}
type Effects = 'REPLAY';
type StateEffects = [State, Effects?];
type Actions =
| { type: 'PLAY'; itemId: ItemId }
| { type: 'PLAY_NEXT_VIDEO' }
| { type: 'ADD_TO_PLAYLIST'; payload: PlaylistItem }
| { type: 'REMOVE_FROM_PLAYLIST'; itemId: ItemId } // the item id
| { type: 'RESET_EFFECT' };
const initialValue: StateEffects = [
{
playlist: [],
currentlyPlaying: null,
},
];
/**
* This reducer controls both our playlist state and effects. Interestingly, the effects produced
* are still pure, meaning it doesn't run any real side-effects (e.g IO). Instead, it only yields
* some effect description so that the component could handle and react upon it.
*/
export const playlistReducer: React.Reducer<StateEffects, Actions> = ([state, effects], action) => {
if (action.type === 'PLAY') {
const nextState = {
...state,
currentlyPlaying: action.itemId,
};
// When user clicks the same video as the one currently being played, replay the video from start.
// Otherwise, just play the clicked video
const nextEffect = action.itemId === state.currentlyPlaying ? 'REPLAY' : undefined;
return [nextState, nextEffect];
}
if (action.type === 'PLAY_NEXT_VIDEO') {
// We don't play the next video when playlist is empty. It's just impossible.
if (!state.playlist.length) return [{ ...state, currentlyPlaying: null }];
const currIndex = state.playlist.findIndex(p => p.id === state.currentlyPlaying);
// Play next video when not in bottom, otheriwse start from the top again
const nextIndex = currIndex === state.playlist.length - 1 ? 0 : currIndex + 1;
const nextItemId = state.playlist[nextIndex].id;
if (state.playlist.length === 1) {
return playlistReducer([state], { type: 'PLAY', itemId: nextItemId });
}
return [
{
...state,
currentlyPlaying: nextItemId,
},
];
}
if (action.type === 'ADD_TO_PLAYLIST') {
return [
{
...state,
playlist: [action.payload, ...state.playlist],
},
];
}
if (action.type === 'REMOVE_FROM_PLAYLIST') {
const nextStateEffects: StateEffects = [
{
...state,
playlist: state.playlist.filter(x => x.id !== action.itemId),
},
];
// When user removes the currently-playing item from playlist,
// remove the video then play the next video
if (state.currentlyPlaying === action.itemId) {
return playlistReducer(nextStateEffects, { type: 'PLAY_NEXT_VIDEO' });
}
return nextStateEffects;
}
if (action.type === 'RESET_EFFECT') {
return [state];
}
return [state, effects];
};
const usePlaylistReducer = () => {
const [stateAndEffects, dispatch] = React.useReducer(playlistReducer, initialValue);
const play = (itemId: ItemId) => dispatch({ type: 'PLAY', itemId });
const playNext = () => dispatch({ type: 'PLAY_NEXT_VIDEO' });
const addToPlaylist = (payload: PlaylistItem) => dispatch({ type: 'ADD_TO_PLAYLIST', payload });
const removeFromPlaylist = (itemId: ItemId) => dispatch({ type: 'REMOVE_FROM_PLAYLIST', itemId });
const resetEffect = () => dispatch({ type: 'RESET_EFFECT' });
return [
stateAndEffects,
{
play,
playNext,
addToPlaylist,
removeFromPlaylist,
resetEffect,
},
] as const;
};
export default usePlaylistReducer;