From 57311f1b6a83f435b2a3cb234b345ecade61c4b4 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Sun, 13 Nov 2016 14:41:08 +1300 Subject: [PATCH] Merging playlists by uri; Hide context on scroll --- src/js/components/ContextMenu.js | 153 ++++++++++++++------------ src/js/helpers.js | 25 +++++ src/js/services/mopidy/middleware.js | 22 ++++ src/js/services/spotify/actions.js | 13 +++ src/js/services/spotify/middleware.js | 12 +- src/js/services/ui/actions.js | 2 +- src/js/services/ui/reducer.js | 11 +- src/js/views/Playlist.js | 4 +- 8 files changed, 163 insertions(+), 79 deletions(-) diff --git a/src/js/components/ContextMenu.js b/src/js/components/ContextMenu.js index 9e928ec7..ce60a017 100755 --- a/src/js/components/ContextMenu.js +++ b/src/js/components/ContextMenu.js @@ -13,6 +13,90 @@ class ContextMenu extends React.Component{ constructor(props) { super(props); + this.handleScroll = this.handleScroll.bind(this); + } + + componentDidMount(){ + window.addEventListener("scroll", this.handleScroll, false); + } + + componentWillUnmount(){ + window.removeEventListener("scroll", this.handleScroll, false); + } + + handleScroll(){ + if( this.props.context_menu.show ){ + this.props.uiActions.hideContextMenu(); + } + } + + playQueueItem(){ + var selectedTracks = this.props.context_menu.data.selected_tracks; + this.props.mopidyActions.changeTrack( selectedTracks[0].tlid ); + this.props.uiActions.hideContextMenu(); + } + + removeFromQueue(){ + var selected_tracks = this.props.context_menu.data.selected_tracks; + var selected_tracks_tlids = []; + for( var i = 0; i < selected_tracks.length; i++ ){ + selected_tracks_tlids.push( selected_tracks[i].tlid ); + } + this.props.mopidyActions.removeTracks( selected_tracks_tlids ); + this.props.uiActions.hideContextMenu(); + } + + playItems(){ + var selected_tracks = this.props.context_menu.data.selected_tracks; + var selected_tracks_uris = []; + for( var i = 0; i < selected_tracks.length; i++ ){ + selected_tracks_uris.push( selected_tracks[i].uri ); + } + this.props.mopidyActions.playTracks(selected_tracks_uris); + this.props.uiActions.hideContextMenu(); + } + + playItemsNext(){ + var selected_tracks = this.props.context_menu.data.selected_tracks; + var selected_tracks_uris = []; + for( var i = 0; i < selected_tracks.length; i++ ){ + selected_tracks_uris.push( selected_tracks[i].uri ); + } + + var current_track = this.props.current_track + var current_track_index = -1 + for( var i = 0; i < this.props.current_tracklist.length; i++ ){ + if( this.props.current_tracklist[i].tlid == this.props.current_track.tlid ){ + current_track_index = i + break + } + } + + var at_position = null + if( current_track_index > -1 ) at_position = current_track_index + 1 + + this.props.mopidyActions.enqueueTracks(selected_tracks_uris, at_position); + this.props.uiActions.hideContextMenu(); + } + + addToQueue(){ + var selected_tracks = this.props.context_menu.data.selected_tracks; + var selected_tracks_uris = []; + for( var i = 0; i < selected_tracks.length; i++ ){ + selected_tracks_uris.push( selected_tracks[i].uri ); + } + this.props.mopidyActions.enqueueTracks(selected_tracks_uris); + this.props.uiActions.hideContextMenu(); + } + + addToPlaylist(){ + this.props.uiActions.openModal( 'AddToPlaylistModal', { track_indexes: this.props.context_menu.data.selected_tracks_indexes }) + this.props.uiActions.hideContextMenu(); + } + + removeFromPlaylist(){ + this.props.uiActions.removeTracksFromPlaylist( this.props.playlist.uri, this.props.context_menu.data.selected_tracks_indexes ) + this.props.uiActions.hideContextMenu(); } renderPlaylistSubmenu(){ @@ -96,75 +180,6 @@ class ContextMenu extends React.Component{ ); } - playQueueItem(){ - var selectedTracks = this.props.context_menu.data.selected_tracks; - this.props.mopidyActions.changeTrack( selectedTracks[0].tlid ); - this.props.uiActions.hideContextMenu(); - } - - removeFromQueue(){ - var selected_tracks = this.props.context_menu.data.selected_tracks; - var selected_tracks_tlids = []; - for( var i = 0; i < selected_tracks.length; i++ ){ - selected_tracks_tlids.push( selected_tracks[i].tlid ); - } - this.props.mopidyActions.removeTracks( selected_tracks_tlids ); - this.props.uiActions.hideContextMenu(); - } - - playItems(){ - var selected_tracks = this.props.context_menu.data.selected_tracks; - var selected_tracks_uris = []; - for( var i = 0; i < selected_tracks.length; i++ ){ - selected_tracks_uris.push( selected_tracks[i].uri ); - } - this.props.mopidyActions.playTracks(selected_tracks_uris); - this.props.uiActions.hideContextMenu(); - } - - playItemsNext(){ - var selected_tracks = this.props.context_menu.data.selected_tracks; - var selected_tracks_uris = []; - for( var i = 0; i < selected_tracks.length; i++ ){ - selected_tracks_uris.push( selected_tracks[i].uri ); - } - - var current_track = this.props.current_track - var current_track_index = -1 - for( var i = 0; i < this.props.current_tracklist.length; i++ ){ - if( this.props.current_tracklist[i].tlid == this.props.current_track.tlid ){ - current_track_index = i - break - } - } - - var at_position = null - if( current_track_index > -1 ) at_position = current_track_index + 1 - - this.props.mopidyActions.enqueueTracks(selected_tracks_uris, at_position); - this.props.uiActions.hideContextMenu(); - } - - addToQueue(){ - var selected_tracks = this.props.context_menu.data.selected_tracks; - var selected_tracks_uris = []; - for( var i = 0; i < selected_tracks.length; i++ ){ - selected_tracks_uris.push( selected_tracks[i].uri ); - } - this.props.mopidyActions.enqueueTracks(selected_tracks_uris); - this.props.uiActions.hideContextMenu(); - } - - addToPlaylist(){ - this.props.uiActions.openModal( 'AddToPlaylistModal', { track_indexes: this.props.context_menu.data.selected_tracks_indexes }) - this.props.uiActions.hideContextMenu(); - } - - removeFromPlaylist(){ - this.props.uiActions.removeTracksFromPlaylist( this.props.playlist.uri, this.props.context_menu.data.selected_tracks_indexes ) - this.props.uiActions.hideContextMenu(); - } - render(){ if( !this.props.context_menu.show ) return null; diff --git a/src/js/helpers.js b/src/js/helpers.js index e61825b6..ab094638 100755 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -154,4 +154,29 @@ export let uriType = function( uri ){ } return null; +} + +/** + * Merge duplicated items in an array + * + * @param list Array the unclean array + * @param key string = the unique key (uri, tlid, etc) + **/ +export let mergeDuplicates = function(list, key){ + var clean_list = []; + var keyed_list = {}; + + for( var i in list ){ + var item = list[i] + if( item[key] in keyed_list ){ + item = Object.assign({}, keyed_list[item[key]], item) + } + keyed_list[item[key]] = item; + } + + for( i in keyed_list ){ + clean_list.push(keyed_list[i]); + } + + return clean_list; } \ No newline at end of file diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index 2a65c572..48b1e984 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -248,6 +248,28 @@ const MopidyMiddleware = (function(){ }) break; + case 'MOPIDY_ADD_PLAYLIST_TRACKS': + + instruct( socket, store, 'playlists.lookup', { uri: action.playlist_uri }) + .then( response => { + var tracks = []; + for( var i = 0; i < action.tracks_uris.length; i++ ){ + tracks.push({ + __model__: "Track", + uri: action.tracks_uris[i] + }); + } + + var playlist = Object.assign({}, response) + playlist.tracks = [...playlist.tracks, ...tracks] + + instruct( socket, store, 'playlists.save', { playlist: playlist } ) + .then( response => { + store.dispatch({ type: 'PLAYLIST_TRACKS_ADDED', tracks_uris: action.tracks_uris }); + }) + }); + break + case 'MOPIDY_REMOVE_PLAYLIST_TRACKS': // reverse order our indexes (otherwise removing from top will affect the keys following) diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js index 830925f2..040f143e 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -588,6 +588,19 @@ export function toggleFollowingPlaylist( uri, method ){ } } +export function addTracksToPlaylist( playlist_uri, tracks_uris ){ + return (dispatch, getState) => { + sendRequest( dispatch, getState, 'users/'+ helpers.getFromUri('userid',playlist_uri) + '/playlists/'+ helpers.getFromUri('playlistid',playlist_uri) + '/tracks', 'POST', { uris: tracks_uris } ) + .then( response => { + dispatch({ + type: 'PLAYLIST_TRACKS_ADDED', + tracks_uris: tracks_uris, + snapshot_id: response.snapshot_id + }); + }); + } +} + export function deleteTracksFromPlaylist( uri, snapshot_id, tracks_indexes ){ return (dispatch, getState) => { sendRequest( dispatch, getState, 'users/'+ helpers.getFromUri('userid',uri) + '/playlists/'+ helpers.getFromUri('playlistid',uri) + '/tracks', 'DELETE', { snapshot_id: snapshot_id, positions: tracks_indexes } ) diff --git a/src/js/services/spotify/middleware.js b/src/js/services/spotify/middleware.js index 02a0d1e8..8d86ece7 100755 --- a/src/js/services/spotify/middleware.js +++ b/src/js/services/spotify/middleware.js @@ -24,12 +24,22 @@ const SpotifyMiddleware = (function(){ return } if( !store.getState().spotify.me || store.getState().spotify.me.id != playlist.owner.id ){ - alert('You can only modify tracks you own') + alert('You can only modify playlists you own') return } store.dispatch( actions.deleteTracksFromPlaylist( playlist.uri, playlist.snapshot_id, action.tracks_indexes )) break + + case 'SPOTIFY_ADD_PLAYLIST_TRACKS': + + if( !store.getState().spotify.authorized ){ + alert('Must be logged in to Spotify to do this') + return + } + store.dispatch( actions.addTracksToPlaylist( action.playlist_uri, action.tracks_uris )) + break + // when our mopidy server current track changes case 'MOPIDY_CURRENTTLTRACK': diff --git a/src/js/services/ui/actions.js b/src/js/services/ui/actions.js index 6cb05f29..75b4df56 100755 --- a/src/js/services/ui/actions.js +++ b/src/js/services/ui/actions.js @@ -107,7 +107,7 @@ export function addTracksToPlaylist( playlist_uri, tracks ){ case 'm3u': return { - type: 'MOPIDY_ADD_TRACKS_TO_PLAYLIST', + type: 'MOPIDY_ADD_PLAYLIST_TRACKS', playlist_uri: playlist_uri, tracks_uris: tracks_uris } diff --git a/src/js/services/ui/reducer.js b/src/js/services/ui/reducer.js index 36e9cb57..f028d51a 100755 --- a/src/js/services/ui/reducer.js +++ b/src/js/services/ui/reducer.js @@ -1,4 +1,6 @@ +import * as helpers from '../../helpers' + export default function reducer(ui = {}, action){ switch (action.type) { @@ -246,15 +248,12 @@ export default function reducer(ui = {}, action){ **/ case 'MOPIDY_PLAYLISTS_LOADED': - if( !action.data ) return ui - return Object.assign({}, ui, { - playlists: [...ui.playlists, ...action.data], - }); - case 'SPOTIFY_LIBRARY_PLAYLISTS_LOADED': if( !action.data ) return ui + var playlists = [...ui.playlists, ...action.data] + playlists = helpers.mergeDuplicates(playlists, 'uri') return Object.assign({}, ui, { - playlists: [...ui.playlists, ...action.data] + playlists: playlists }); diff --git a/src/js/views/Playlist.js b/src/js/views/Playlist.js index 3f6edbf7..640e5386 100755 --- a/src/js/views/Playlist.js +++ b/src/js/views/Playlist.js @@ -80,12 +80,12 @@ class Playlist extends React.Component{ switch( helpers.uriSource( this.props.params.uri ) ){ case 'm3u': - return this.delete() } /> + return this.delete() } /> break case 'spotify': if( this.props.playlist.owner && this.props.playlist.owner.id == this.props.spotify_userid ){ - return this.delete() } /> + return this.unfollow() } /> }else if( this.props.playlist.following ){ return }else{