diff --git a/src/js/bootstrap.js b/src/js/bootstrap.js index 2a6371df..e3611045 100755 --- a/src/js/bootstrap.js +++ b/src/js/bootstrap.js @@ -65,6 +65,11 @@ var initialState = { playlists: [], tracks: [] }, + library_artists: [], + artists: [], + library_albums: [], + albums: [], + library_playlists: [], playlists: [], notifications: [] } diff --git a/src/js/components/GridItem.js b/src/js/components/GridItem.js index ac8ff435..bd3a2d50 100755 --- a/src/js/components/GridItem.js +++ b/src/js/components/GridItem.js @@ -38,7 +38,7 @@ export default class GridItem extends React.Component{
{ item.name }
{ item.artists ? : null } - { item.type == 'playlist' && item.tracks ? item.tracks.total+' tracks' : null } + { item.type == 'playlist' && item.tracks_total ? item.tracks_total+' tracks' : null } { item.followers ? item.followers.total.toLocaleString()+' followers' : null } { item.type == 'playlist' && item.can_edit ? : null }
diff --git a/src/js/components/TrackList.js b/src/js/components/TrackList.js index 426198b9..b4e40abe 100755 --- a/src/js/components/TrackList.js +++ b/src/js/components/TrackList.js @@ -245,7 +245,7 @@ class TrackList extends React.Component{ } render(){ - if( !this.state.tracks ) return null + if( !this.state.tracks || Object.prototype.toString.call(this.state.tracks) !== '[object Array]' ) return null let self = this; return ( diff --git a/src/js/helpers.js b/src/js/helpers.js index 9843299d..17cb6a8c 100755 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -93,6 +93,28 @@ export let getTrackIcon = function( track = false ){ } +/** + * Get a track's icon + * @param track object + * @return string + **/ +export let flattenTracks = function( tracks ){ + var flattened = [] + for( var i = 0; i < tracks.length; i++ ){ + flattened.push( Object.assign( + {}, + tracks[i].track, + { + added_by: tracks[i].added_by, + added_at: tracks[i].added_at + } + )) + } + + return flattened +} + + /** diff --git a/src/js/services/localstorage/middleware.js b/src/js/services/localstorage/middleware.js index b04cc042..435627b4 100755 --- a/src/js/services/localstorage/middleware.js +++ b/src/js/services/localstorage/middleware.js @@ -12,7 +12,7 @@ const localstorageMiddleware = (function(){ // append our state to a global variable. This gives us access to debug the store at any point window._store = store - //console.log(action) + console.log(action) switch( action.type ){ diff --git a/src/js/services/mopidy/actions.js b/src/js/services/mopidy/actions.js index 13954f9c..09f3a6be 100755 --- a/src/js/services/mopidy/actions.js +++ b/src/js/services/mopidy/actions.js @@ -157,47 +157,47 @@ export function deletePlaylist( uri ){ } } -export function getPlaylists(){ - return { type: 'MOPIDY_PLAYLISTS' } +export function getLibraryPlaylists(){ + return { type: 'MOPIDY_GET_LIBRARY_PLAYLISTS' } } export function getPlaylist( uri ){ return { - type: 'MOPIDY_PLAYLIST', + type: 'MOPIDY_GET_PLAYLIST', data: { uri: uri } } } export function getDirectory( uri ){ return { - type: 'MOPIDY_DIRECTORY', + type: 'MOPIDY_GET_DIRECTORY', data: { uri: uri } } } export function getAlbum( uri ){ return { - type: 'MOPIDY_ALBUM', + type: 'MOPIDY_GET_ALBUM', data: { uri: uri } } } export function getArtist( uri ){ return { - type: 'MOPIDY_ARTIST', + type: 'MOPIDY_GET_ARTIST', data: { uri: uri } } } export function getArtists(){ return { - type: 'MOPIDY_ARTISTS' + type: 'MOPIDY_GET_ARTISTS' } } export function getAlbums(){ return { - type: 'MOPIDY_ALBUMS' + type: 'MOPIDY_GET_ALBUMS' } } diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index 5f2ff38d..f10054fe 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -282,29 +282,47 @@ const MopidyMiddleware = (function(){ }) break; - case 'MOPIDY_PLAYLISTS': + + /** + * =============================================================== PLAYLIST(S) ========== + * ====================================================================================== + **/ + + case 'MOPIDY_GET_LIBRARY_PLAYLISTS': instruct( socket, store, 'playlists.asList' ) .then( response => { - for( var i = 0; i < response.length; i++ ){ - if( response[i].uri.startsWith('m3u:')) response[i] = Object.assign({}, response[i], { can_edit: true }) - } - store.dispatch({ type: 'MOPIDY_PLAYLISTS_LOADED', data: response }); - // TODO: dispatch an action to fetch extra data for each playlist - // this will then trigger an PLAYLIST_REFRESHED action to update a specific store item + // drop in our URI list + var playlist_uris = helpers.asURIs(response) + + store.dispatch({ type: 'LIBRARY_PLAYLISTS_LOADED', uris: playlist_uris }); + + // get the full playlist objects + for (var i = 0; i < response.length; i++ ){ + instruct( socket, store, 'playlists.lookup', { uri: response[i].uri }) + .then( response => { + var playlist = Object.assign( + {}, + { + images: [] + }, + { + can_edit: (response.uri.startsWith('m3u:')) + }, + response, + { + tracks: ( response.tracks ? response.tracks : [] ), + tracks_total: ( response.tracks ? response.tracks.length : 0 ) + } + ) + + store.dispatch({ type: 'PLAYLIST_LOADED', playlist: playlist }) + }) + } }) break; - case 'MOPIDY_CREATE_PLAYLIST': - instruct( socket, store, 'playlists.create', { name: action.name, uri_scheme: action.scheme }) - .then( response => { - - // re-load our global playlists - store.dispatch({ type: 'MOPIDY_PLAYLISTS' }); - }); - break - - case 'MOPIDY_PLAYLIST': + case 'MOPIDY_GET_PLAYLIST': store.dispatch({ type: 'MOPIDY_PLAYLIST_LOADED', data: false }); instruct( socket, store, 'playlists.lookup', action.data ) .then( response => { @@ -321,18 +339,15 @@ const MopidyMiddleware = (function(){ ) // tracks? get the full track objects - if( playlist.tracks.length > 0 ) store.dispatch({ type: 'MOPIDY_PLAYLIST_TRACKS', tracks: playlist.tracks }) + if( playlist.tracks.length > 0 ) store.dispatch({ type: 'MOPIDY_RESOLVE_PLAYLIST_TRACKS', tracks: playlist.tracks }) - store.dispatch({ type: 'MOPIDY_PLAYLIST_LOADED', data: playlist }) + store.dispatch({ type: 'PLAYLIST_LOADED', playlist: playlist }) }) break; - case 'MOPIDY_PLAYLIST_TRACKS': + case 'MOPIDY_RESOLVE_PLAYLIST_TRACKS': var tracks = Object.assign([], action.tracks) - var uris = []; - for( var i = 0; i < tracks.length; i++ ){ - uris.push( tracks[i].uri ); - } + var uris = helpers.asURIs(tracks) instruct( socket, store, 'library.lookup', { uris: uris } ) .then( response => { @@ -357,7 +372,7 @@ const MopidyMiddleware = (function(){ } } - store.dispatch({ type: 'PLAYLIST_TRACKS_LOADED', tracks: tracks }) + store.dispatch({ type: 'PLAYLIST_TRACKS_LOADED', playlist_uri: action.playlist_uri, tracks: tracks }) }) break @@ -460,14 +475,37 @@ const MopidyMiddleware = (function(){ }); break + case 'MOPIDY_CREATE_PLAYLIST': + instruct( socket, store, 'playlists.create', { name: action.name, uri_scheme: action.scheme }) + .then( response => { + + // re-load our global playlists + //store.dispatch({ type: 'MOPIDY_GET_PLAYLISTS' }); + }); + break + case 'MOPIDY_DELETE_PLAYLIST': instruct( socket, store, 'playlists.delete', { uri: action.uri }) .then( response => { // re-load our global playlists - store.dispatch({ type: 'MOPIDY_PLAYLISTS' }); + // store.dispatch({ type: 'MOPIDY_PLAYLISTS' }); }); break + + + /** + * =============================================================== ALBUM(S) ============= + * ====================================================================================== + **/ + + case 'MOPIDY_ALBUMS': + store.dispatch({ type: 'MOPIDY_ALBUMS_LOADED', data: false }); + instruct( socket, store, 'library.browse', { uri: 'local:directory?type=album' } ) + .then( response => { + store.dispatch({ type: 'MOPIDY_ALBUMS_LOADED', data: response }); + }) + break; case 'MOPIDY_ALBUM': //store.dispatch({ type: 'MOPIDY_ALBUM_LOADED', data: false }); @@ -526,6 +564,20 @@ const MopidyMiddleware = (function(){ }) }) break; + + + /** + * =============================================================== ARTIST(S) ============ + * ====================================================================================== + **/ + + case 'MOPIDY_ARTISTS': + store.dispatch({ type: 'MOPIDY_ARTISTS_LOADED', data: false }); + instruct( socket, store, 'library.browse', { uri: 'local:directory?type=artist' } ) + .then( response => { + store.dispatch({ type: 'MOPIDY_ARTISTS_LOADED', data: response }); + }) + break; case 'MOPIDY_ARTIST': store.dispatch({ type: 'MOPIDY_ARTIST_LOADED', data: false }); @@ -560,6 +612,12 @@ const MopidyMiddleware = (function(){ store.dispatch({ type: 'MOPIDY_ARTIST_LOADED', data: artist }); }) break; + + + /** + * =============================================================== LOCAL ================ + * ====================================================================================== + **/ case 'MOPIDY_DIRECTORY': store.dispatch({ type: 'MOPIDY_DIRECTORY_LOADED', data: false }); @@ -569,22 +627,6 @@ const MopidyMiddleware = (function(){ }) break; - case 'MOPIDY_ARTISTS': - store.dispatch({ type: 'MOPIDY_ARTISTS_LOADED', data: false }); - instruct( socket, store, 'library.browse', { uri: 'local:directory?type=artist' } ) - .then( response => { - store.dispatch({ type: 'MOPIDY_ARTISTS_LOADED', data: response }); - }) - break; - - case 'MOPIDY_ALBUMS': - store.dispatch({ type: 'MOPIDY_ALBUMS_LOADED', data: false }); - instruct( socket, store, 'library.browse', { uri: 'local:directory?type=album' } ) - .then( response => { - store.dispatch({ type: 'MOPIDY_ALBUMS_LOADED', data: response }); - }) - break; - // This action is irrelevant to us, pass it on to the next middleware default: return next(action); diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js index 11ca1f07..4034ae29 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -262,9 +262,18 @@ export function getFeaturedPlaylists(){ sendRequest( dispatch, getState, 'browse/featured-playlists?timestamp='+timestamp+'&country='+getState().spotify.country+'&limit=50&locale='+getState().spotify.locale ) .then( response => { + for (var i = 0; i < response.playlists.items.length; i++){ + dispatch({ + type: 'PLAYLIST_LOADED', + playlist: response.playlists.items[i] + }); + } dispatch({ type: 'SPOTIFY_FEATURED_PLAYLISTS_LOADED', - data: response + data: { + message: response.message, + playlists: helpers.asURIs(response.playlists.items) + } }); }); } @@ -290,28 +299,41 @@ export function getCategory( id ){ dispatch({ type: 'SPOTIFY_CATEGORY_LOADED', data: false }); - sendRequest( dispatch, getState, 'browse/categories/'+id+'?country='+getState().spotify.country+'&locale='+getState().spotify.locale ) - .then( response => { - dispatch({ - type: 'SPOTIFY_CATEGORY_LOADED', - data: response - }); - }); - } -} - -export function getCategoryPlaylists( id ){ - return (dispatch, getState) => { - - dispatch({ type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED', data: false }); - - sendRequest( dispatch, getState, 'browse/categories/'+id+'/playlists?limit=50&country='+getState().spotify.country+'&locale='+getState().spotify.locale ) - .then( response => { - dispatch({ - type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED', - data: response.playlists - }); + $.when( + + sendRequest( dispatch, getState, 'browse/categories/'+id+'?country='+getState().spotify.country+'&locale='+getState().spotify.locale ), + sendRequest( dispatch, getState, 'browse/categories/'+id+'/playlists?limit=50&country='+getState().spotify.country+'&locale='+getState().spotify.locale ) + + ).then( ( category_response, playlists_response ) => { + + var playlists = [] + for (var i = 0; i < playlists_response.playlists.items.length; i++){ + playlists.push( + Object.assign( + {}, + playlists_response.playlists.items[i], + { + tracks: false, + tracks_total: playlists_response.playlists.items[i].tracks.total + } + ) + ) + } + + var category = Object.assign( + {}, + category_response, + { + items: false, + playlists: playlists + } + ) + + dispatch({ + type: 'SPOTIFY_CATEGORY_LOADED', + data: category }); + }); } } @@ -330,13 +352,14 @@ export function getNewReleases(){ } } -export function getURL( url, action_name ){ +export function getURL( url, action_name, uri = false ){ return (dispatch, getState) => { sendRequest( dispatch, getState, url ) .then( response => { dispatch({ type: action_name, - data: response + data: response, + uri: uri }); }); } @@ -774,15 +797,24 @@ export function savePlaylist( uri, name, is_public ){ export function getPlaylist( uri ){ return (dispatch, getState) => { - // flush out the previous store value - dispatch({ type: 'SPOTIFY_PLAYLIST_LOADED', data: false }); - // get the main playlist object sendRequest( dispatch, getState, 'users/'+ helpers.getFromUri('userid',uri) +'/playlists/'+ helpers.getFromUri('playlistid',uri) +'?market='+getState().spotify.country ) .then( response => { + + var playlist = Object.assign( + {}, + response, + { + can_edit: (getState().spotify.me && response.owner.id == getState().spotify.me.id), + tracks: helpers.flattenTracks(response.tracks.items), + tracks_more: response.tracks.next, + tracks_total: response.tracks.total + } + ) + dispatch({ - type: 'SPOTIFY_PLAYLIST_LOADED', - data: response + type: 'PLAYLIST_LOADED', + playlist: playlist }) }) } @@ -801,15 +833,28 @@ function loadNextPlaylistsBatch( dispatch, getState, playlists, lastResponse ){ // used to define what we can add tracks to if( getState().spotify.authorized ){ for( var i = 0; i < playlists.length; i++ ){ - if( playlists[i].owner.id == getState().spotify.me.id ){ - playlists[i] = Object.assign({}, playlists[i], { can_edit: true }) - } + + var playlist = Object.assign( + {}, + playlists[i], + { + can_edit: (getState().spotify.me && playlists[i].owner.id == getState().spotify.me.id), + tracks: helpers.flattenTracks(playlists[i].tracks.items), + tracks_more: response.tracks.next, + tracks_total: response.tracks.total + } + ) + + dispatch({ + type: 'PLAYLIST_LOADED', + playlist: playlist + }); } } dispatch({ - type: 'SPOTIFY_LIBRARY_PLAYLISTS_LOADED', - data: playlists + type: 'LIBRARY_PLAYLISTS_LOADED', + uris: helpers.asURIs(playlists) }); } } diff --git a/src/js/services/ui/reducer.js b/src/js/services/ui/reducer.js index 72c1487b..a7024819 100755 --- a/src/js/services/ui/reducer.js +++ b/src/js/services/ui/reducer.js @@ -231,46 +231,21 @@ export default function reducer(ui = {}, action){ if( !action.data ) return Object.assign({}, ui, { playlist: false }) return Object.assign({}, ui, { playlist: action.data }) - case 'SPOTIFY_PLAYLIST_LOADED': - if( !action.data ) return Object.assign({}, ui, { playlist: false }) + case 'PLAYLIST_LOADED_MORE_TRACKS': - var tracks = [] - for( var i = 0; i < action.data.tracks.items.length; i++ ){ - tracks.push( Object.assign( - {}, - action.data.tracks.items[i].track, - { - added_by: action.data.tracks.items[i].added_by, - added_at: action.data.tracks.items[i].added_at - } - )) - } + var playlists = ui.playlists + var playlist = Object.assign( + {}, + playlists[action.uri], + { + tracks: [...playlists[action.uri].tracks, ...helpers.flattenTracks(action.data.items)], + tracks_more: action.data.next, + tracks_total: action.data.total + } + ) - var playlist = Object.assign({}, action.data, { - tracks: tracks, - tracks_more: action.data.tracks.next, - tracks_total: action.data.tracks.total - }) - return Object.assign({}, ui, { playlist: playlist }); - - case 'SPOTIFY_PLAYLIST_LOADED_MORE': - var tracks = [] - for( var i = 0; i < action.data.items.length; i++ ){ - tracks.push( Object.assign( - {}, - action.data.items[i].track, - { - added_by: action.data.items[i].added_by, - added_at: action.data.items[i].added_at - } - )) - } - - var playlist = Object.assign({}, ui.playlist, { - tracks: [...ui.playlist.tracks, ...tracks], - tracks_more: action.data.next - }) - return Object.assign({}, ui, { playlist: playlist }); + playlists[action.uri] = playlist + return Object.assign({}, ui, { playlists: playlists }); case 'PLAYLIST_TRACKS_REMOVED': var tracks = Object.assign([], ui.playlist.tracks) @@ -316,14 +291,25 @@ export default function reducer(ui = {}, action){ * Library Playlists **/ - case 'MOPIDY_PLAYLISTS_LOADED': - case 'SPOTIFY_LIBRARY_PLAYLISTS_LOADED': - if( !action.data ) return ui - var playlists = [...ui.playlists, ...action.data] - playlists = helpers.mergeDuplicates(playlists, 'uri') - helpers.sortItems(playlists, 'name') + case 'PLAYLIST_LOADED': + case 'PLAYLIST_UPDATED': + var playlists = ui.playlists + var playlist = Object.assign({}, action.playlist) + + // if we already have one in our list, fetch it and update it + if (playlists[action.playlist.uri]){ + playlist = Object.assign({}, playlists[action.playlist.uri], action.playlist) + } + + playlists[action.playlist.uri] = playlist + return Object.assign({}, ui, { playlists: playlists }); + + case 'LIBRARY_PLAYLISTS_LOADED': + var library_playlists = [] + if (ui.library_playlists) library_playlists = ui.library_playlists + return Object.assign({}, ui, { - playlists: playlists + library_playlists: [...library_playlists, ...action.uris] }); diff --git a/src/js/views/App.js b/src/js/views/App.js index fe17141c..45e80322 100755 --- a/src/js/views/App.js +++ b/src/js/views/App.js @@ -53,7 +53,7 @@ class App extends React.Component{ // mopidy comes online if( !this.props.mopidy_connected && nextProps.mopidy_connected ){ - this.props.mopidyActions.getPlaylists(); + this.props.mopidyActions.getLibraryPlaylists(); } // spotify authorized diff --git a/src/js/views/Playlist.js b/src/js/views/Playlist.js index f985a768..faa42ca4 100755 --- a/src/js/views/Playlist.js +++ b/src/js/views/Playlist.js @@ -55,7 +55,7 @@ class Playlist extends React.Component{ loadMore(){ if( !this.props.playlist.tracks_more ) return - this.props.spotifyActions.getURL( this.props.playlist.tracks_more, 'SPOTIFY_PLAYLIST_LOADED_MORE' ); + this.props.spotifyActions.getURL( this.props.playlist.tracks_more, 'PLAYLIST_LOADED_MORE_TRACKS', this.props.playlist.uri ); } play(){ @@ -90,19 +90,6 @@ class Playlist extends React.Component{ this.props.uiActions.removeTracksFromPlaylist( this.props.playlist.uri, tracks_indexes ) } - isEditable(){ - if( helpers.uriSource( this.props.playlist.uri ) == 'spotify' ){ - if( !this.props.spotify_authorized ) return false - - return ( this.props.playlist && - this.props.playlist.owner && - this.props.playlist.owner.id == this.props.spotify_userid - ) - }else{ - return true - } - } - renderExtraButtons(){ switch( helpers.uriSource( this.props.playlist.uri ) ){ @@ -115,7 +102,7 @@ class Playlist extends React.Component{ ) case 'spotify': - if( this.isEditable() ){ + if( this.props.playlist.can_edit ){ return ( @@ -167,7 +154,7 @@ class Playlist extends React.Component{
- { this.props.playlist.tracks ? this.removeTracks(tracks_indexes) } reorderTracks={ (indexes, index) => this.reorderTracks(indexes, index) } /> : null } + { this.props.playlist.tracks ? this.removeTracks(tracks_indexes) } reorderTracks={ (indexes, index) => this.reorderTracks(indexes, index) } /> : null } this.loadMore() }/>
@@ -186,7 +173,7 @@ class Playlist extends React.Component{ const mapStateToProps = (state, ownProps) => { return { - playlist: state.ui.playlist, + playlist: state.ui.playlists[ownProps.params.uri], mopidy_connected: state.mopidy.connected, spotify_authorized: state.spotify.authorized, spotify_userid: state.spotify.me.id diff --git a/src/js/views/discover/DiscoverCategory.js b/src/js/views/discover/DiscoverCategory.js index 6ac1dcd1..769f1188 100755 --- a/src/js/views/discover/DiscoverCategory.js +++ b/src/js/views/discover/DiscoverCategory.js @@ -17,13 +17,11 @@ class DiscoverCategory extends React.Component{ componentDidMount(){ this.props.spotifyActions.getCategory( this.props.params.id ); - this.props.spotifyActions.getCategoryPlaylists( this.props.params.id ); } componentWillReceiveProps( nextProps ){ if( nextProps.params.id != this.props.params.id ){ this.props.spotifyActions.getCategory( nextProps.params.id ); - this.props.spotifyActions.getCategoryPlaylists( nextProps.params.id ); } } @@ -39,7 +37,7 @@ class DiscoverCategory extends React.Component{
- { this.props.spotify.category_playlists ? : null } + { this.props.spotify.category.playlists ? : null }
this.loadMore() }/>
diff --git a/src/js/views/discover/DiscoverFeatured.js b/src/js/views/discover/DiscoverFeatured.js index 7e6d8ad9..afa19248 100755 --- a/src/js/views/discover/DiscoverFeatured.js +++ b/src/js/views/discover/DiscoverFeatured.js @@ -20,11 +20,22 @@ class DiscoverFeatured extends React.Component{ } render(){ + + var playlists = [] + if (this.props.featured_playlists){ + for (var i = 0; i < this.props.featured_playlists.playlists.length; i++){ + var uri = this.props.featured_playlists.playlists[i] + if (this.props.playlists.hasOwnProperty(uri)){ + playlists.push(this.props.playlists[uri]) + } + } + } + return (
- { this.props.spotify.featured_playlists ? : null } + { playlists ? : null }
); @@ -39,7 +50,10 @@ class DiscoverFeatured extends React.Component{ **/ const mapStateToProps = (state, ownProps) => { - return state; + return { + featured_playlists: state.spotify.featured_playlists, + playlists: state.ui.playlists + } } const mapDispatchToProps = (dispatch) => { diff --git a/src/js/views/library/LibraryPlaylists.js b/src/js/views/library/LibraryPlaylists.js index 6e89607a..f4f2ef00 100755 --- a/src/js/views/library/LibraryPlaylists.js +++ b/src/js/views/library/LibraryPlaylists.js @@ -33,9 +33,16 @@ class LibraryPlaylists extends React.Component{ } renderView(){ - if( !this.props.playlists ) return null + if( !this.props.library_playlists || !this.props.playlists ) return null + + var playlists = [] + for (var i = 0; i < this.props.library_playlists.length; i++){ + var uri = this.props.library_playlists[i] + if (this.props.playlists.hasOwnProperty(uri)){ + playlists.push(this.props.playlists[uri]) + } + } - var playlists = Object.assign([], this.props.playlists) if( this.props.sort ){ playlists = helpers.sortItems(playlists, this.props.sort, this.props.sort_reverse) } @@ -141,6 +148,7 @@ const mapStateToProps = (state, ownProps) => { view: state.ui.library_playlists_view, sort: state.ui.library_playlists_sort, sort_reverse: state.ui.library_playlists_sort_reverse, + library_playlists: state.ui.library_playlists, playlists: state.ui.playlists } }