diff --git a/js/actionTypes.js b/js/actionTypes.js index 9556cca8..9dd2361c 100644 --- a/js/actionTypes.js +++ b/js/actionTypes.js @@ -42,6 +42,7 @@ export const UNSET_USER_MESSAGE = "UNSET_USER_MESSAGE"; export const SET_PLAYLIST_SCROLL_POSITION = "SET_PLAYLIST_SCROLL_POSITION"; export const CLICKED_TRACK = "CLICKED_TRACK"; export const CTRL_CLICKED_TRACK = "CTRL_CLICKED_TRACK"; +export const SHIFT_CLICKED_TRACK = "SHIFT_CLICKED_TRACK"; export const SELECT_ALL = "SELECT_ALL"; export const SELECT_ZERO = "SELECT_ZERO"; export const INVERT_SELECTION = "INVERT_SELECTION"; diff --git a/js/components/PlaylistWindow/TrackCell.js b/js/components/PlaylistWindow/TrackCell.js index 0bf6f0e4..8d76b4dc 100644 --- a/js/components/PlaylistWindow/TrackCell.js +++ b/js/components/PlaylistWindow/TrackCell.js @@ -4,6 +4,7 @@ import classnames from "classnames"; import { CLICKED_TRACK, CTRL_CLICKED_TRACK, + SHIFT_CLICKED_TRACK, PLAY_TRACK } from "../../actionTypes"; import { getCurrentTrackId } from "../../selectors"; @@ -14,7 +15,6 @@ const TrackCell = props => { selected, current, clickTrack, - ctrlClickTrack, playTrack, children, onMouseDown @@ -30,7 +30,7 @@ const TrackCell = props => { onMouseDown={onMouseDown} onClick={clickTrack} onDoubleClick={playTrack} - onContextMenu={ctrlClickTrack} + onContextMenu={clickTrack} > {children} @@ -45,14 +45,18 @@ const mapStateToProps = (state, ownProps) => { }; const mapDispatchToProps = (dispatch, ownProps) => ({ - clickTrack: () => dispatch({ type: CLICKED_TRACK, id: ownProps.id }), - ctrlClickTrack: e => { - if (e.ctrlKey) { + clickTrack: e => { + if (e.shiftKey) { e.preventDefault(); - return dispatch({ type: CTRL_CLICKED_TRACK, id: ownProps.id }); + return dispatch({ type: SHIFT_CLICKED_TRACK, index: ownProps.index }); + } else if (e.metaKey) { + e.preventDefault(); + return dispatch({ type: CTRL_CLICKED_TRACK, index: ownProps.index }); + } else if (e.ctrlKey) { + e.preventDefault(); + return dispatch({ type: CTRL_CLICKED_TRACK, index: ownProps.index }); } - return null; - // TODO: We need to spawn our own context menu + return dispatch({ type: CLICKED_TRACK, index: ownProps.index }); }, playTrack: () => dispatch({ type: PLAY_TRACK, id: ownProps.id }) }); diff --git a/js/components/PlaylistWindow/TrackList.js b/js/components/PlaylistWindow/TrackList.js index cf048dd9..cf0925bd 100644 --- a/js/components/PlaylistWindow/TrackList.js +++ b/js/components/PlaylistWindow/TrackList.js @@ -34,7 +34,7 @@ class TrackList extends React.Component { _renderTracks(format) { return this.props.trackIds.map((id, i) => ( - + {format(id, i)} )); diff --git a/js/reducers/playlist.js b/js/reducers/playlist.js index 954a1de4..a001bf1c 100644 --- a/js/reducers/playlist.js +++ b/js/reducers/playlist.js @@ -2,6 +2,7 @@ import { SET_MEDIA, CLICKED_TRACK, CTRL_CLICKED_TRACK, + SHIFT_CLICKED_TRACK, SELECT_ALL, SELECT_ZERO, INVERT_SELECTION, @@ -21,27 +22,49 @@ import { shuffle, moveSelected, mapObject, filterObject } from "../utils"; const defaultPlaylistState = { trackOrder: [], currentTrack: null, - tracks: {} + tracks: {}, + lastSelectedIndex: null }; const playlist = (state = defaultPlaylistState, action) => { switch (action.type) { case CLICKED_TRACK: + const clickedId = String(state.trackOrder[action.index]); return { ...state, tracks: mapObject(state.tracks, (track, id) => ({ ...track, - selected: id === String(action.id) - })) + selected: id === clickedId + })), + lastSelectedIndex: action.index }; case CTRL_CLICKED_TRACK: - const t = state.tracks[action.id]; + const id = state.trackOrder[action.index]; + const t = state.tracks[id]; return { ...state, tracks: { ...state.tracks, - [action.id]: { ...t, selected: !t.selected } - } + [id]: { ...t, selected: !t.selected } + }, + // 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); + return { + ...state, + tracks: state.trackOrder.map((trackId, i) => ({ + ...state.tracks[trackId], + selected: i >= start && i <= end + })) }; case SELECT_ALL: return { @@ -66,24 +89,36 @@ const playlist = (state = defaultPlaylistState, action) => { }; case REMOVE_ALL_TRACKS: // TODO: Consider disposing of ObjectUrls - return { ...state, trackOrder: [], currentTrack: null, tracks: {} }; + return { + ...state, + trackOrder: [], + currentTrack: null, + tracks: {}, + lastSelectedIndex: null + }; case REMOVE_TRACKS: // TODO: Consider disposing of ObjectUrls const actionIds = action.ids.map(Number); const { currentTrack } = state; return { ...state, - trackOrder: state.trackOrder.filter(id => !actionIds.includes(id)), + trackOrder: state.trackOrder.filter( + trackId => !actionIds.includes(trackId) + ), currentTrack: actionIds.includes(currentTrack) ? null : currentTrack, tracks: filterObject( state.tracks, - (track, id) => !action.ids.includes(id) - ) + (track, trackId) => !action.ids.includes(trackId) + ), + // 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() + 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 { @@ -106,7 +141,9 @@ const playlist = (state = defaultPlaylistState, action) => { duration: null, url: action.url } - } + }, + // TODO: This could probably be made to work, but we clear it just to be safe. + lastSelectedIndex: null }; case SET_MEDIA: return { @@ -141,12 +178,11 @@ const playlist = (state = defaultPlaylistState, action) => { ...state, trackOrder: moveSelected( state.trackOrder, - i => { - const id = state.trackOrder[i]; - return state.tracks[id].selected; - }, + i => state.tracks[state.trackOrder[i]].selected, action.offset - ) + ), + // TODO: This could probably be made to work, but we clear it just to be safe. + lastSelectedIndex: null }; default: return state;