import { PlaylistTrack, Action } from "../types"; import { SET_MEDIA, CLICKED_TRACK, CTRL_CLICKED_TRACK, SHIFT_CLICKED_TRACK, SELECT_ALL, SELECT_ZERO, INVERT_SELECTION, REMOVE_ALL_TRACKS, REMOVE_TRACKS, ADD_TRACK_FROM_URL, REVERSE_LIST, RANDOMIZE_LIST, SET_TRACK_ORDER, PLAY_TRACK, BUFFER_TRACK, DRAG_SELECTED } from "../actionTypes"; import { shuffle, moveSelected } from "../utils"; export interface PlaylistState { trackOrder: number[]; lastSelectedIndex: number | null; currentTrack: number | null; selectedTracks: Set; } const defaultPlaylistState: PlaylistState = { trackOrder: [], currentTrack: null, lastSelectedIndex: null, selectedTracks: new Set() }; function toggleSetMembership(set: Set, value: T): void { if (set.has(value)) { set.delete(value); } else { set.add(value); } } const playlist = ( state: PlaylistState = defaultPlaylistState, action: Action ): PlaylistState => { switch (action.type) { case CLICKED_TRACK: return { ...state, selectedTracks: new Set([state.trackOrder[action.index]]), lastSelectedIndex: action.index }; case CTRL_CLICKED_TRACK: const id = state.trackOrder[action.index]; const newSelectedTracks = new Set(state.selectedTracks); toggleSetMembership(newSelectedTracks, id); return { ...state, selectedTracks: newSelectedTracks, // Using this as the lastClickedIndex is kinda funny, since you // may have just _un_selected the track. However, this is what // Winamp 2 does, so we'll copy it. lastSelectedIndex: action.index }; case SHIFT_CLICKED_TRACK: if (state.lastSelectedIndex == null) { return state; } const clickedIndex = action.index; const start = Math.min(clickedIndex, state.lastSelectedIndex); const end = Math.max(clickedIndex, state.lastSelectedIndex); const selectedTracks = new Set(state.trackOrder.slice(start, end + 1)); return { ...state, selectedTracks }; case SELECT_ALL: return { ...state, selectedTracks: new Set(state.trackOrder) }; case SELECT_ZERO: return { ...state, selectedTracks: new Set() }; case INVERT_SELECTION: return { ...state, selectedTracks: new Set( state.trackOrder.filter(id => !state.selectedTracks.has(id)) ) }; case REMOVE_ALL_TRACKS: // TODO: Consider disposing of ObjectUrls return { ...state, trackOrder: [], currentTrack: null, selectedTracks: new Set(), lastSelectedIndex: null }; case REMOVE_TRACKS: // TODO: Consider disposing of ObjectUrls const actionIds = new Set(action.ids.map(Number)); const { currentTrack } = state; return { ...state, trackOrder: state.trackOrder.filter(trackId => !actionIds.has(trackId)), currentTrack: actionIds.has(Number(currentTrack)) ? null : currentTrack, selectedTracks: new Set( Array.from(state.selectedTracks).filter(id => actionIds.has(id)) ), // TODO: This could probably be made to work, but we clear it just to be safe. lastSelectedIndex: null }; case REVERSE_LIST: return { ...state, trackOrder: [...state.trackOrder].reverse(), // TODO: This could probably be made to work, but we clear it just to be safe. lastSelectedIndex: null }; case RANDOMIZE_LIST: return { ...state, trackOrder: shuffle(state.trackOrder) }; case SET_TRACK_ORDER: const { trackOrder } = action; return { ...state, trackOrder }; case ADD_TRACK_FROM_URL: const atIndex = action.atIndex == null ? state.trackOrder.length : action.atIndex; return { ...state, trackOrder: [ ...state.trackOrder.slice(0, atIndex), Number(action.id), ...state.trackOrder.slice(atIndex) ], // TODO: This could probably be made to work, but we clear it just to be safe. lastSelectedIndex: null }; case PLAY_TRACK: case BUFFER_TRACK: return { ...state, currentTrack: action.id }; case DRAG_SELECTED: return { ...state, trackOrder: moveSelected( state.trackOrder, i => state.selectedTracks.has(state.trackOrder[i]), action.offset ), // TODO: This could probably be made to work, but we clear it just to be safe. lastSelectedIndex: null }; default: return state; } }; export default playlist;