From 5885439e4810d0e98340325c1ade24dd97d90d06 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Mon, 12 Dec 2016 09:07:47 +1300 Subject: [PATCH] Push spotify auth; Fixing MIA authentication --- src/js/bootstrap.js | 3 +- src/js/components/Modal/AddToPlaylistModal.js | 4 +- src/js/components/Modal/Modal.js | 4 ++ .../Modal/SendAuthorizationModal.js | 70 +++++++++++++++++++ src/js/components/Parallax.js | 2 +- .../components/SpotifyAuthenticationFrame.js | 4 +- src/js/components/Thumbnail.js | 46 ++++++++---- src/js/components/Track.js | 9 ++- src/js/helpers.js | 39 ++++++++--- src/js/services/localstorage/middleware.js | 20 +++--- src/js/services/pusher/actions.js | 4 +- src/js/services/pusher/middleware.js | 16 ++++- src/js/services/spotify/actions.js | 12 ++-- src/js/services/spotify/reducer.js | 49 +++++++------ src/js/views/Settings.js | 56 +++++++++------ src/js/views/User.js | 4 ++ src/js/views/library/LibraryPlaylists.js | 16 ++++- src/scss/components/_modal.scss | 14 ++-- 18 files changed, 275 insertions(+), 97 deletions(-) create mode 100755 src/js/components/Modal/SendAuthorizationModal.js diff --git a/src/js/bootstrap.js b/src/js/bootstrap.js index 32bbb4c4..a1092d6c 100755 --- a/src/js/bootstrap.js +++ b/src/js/bootstrap.js @@ -50,8 +50,7 @@ var initialState = { spotify: { connected: false, country: 'NZ', - locale: 'en_NZ', - me: false + locale: 'en_NZ' }, ui: { context_menu: { diff --git a/src/js/components/Modal/AddToPlaylistModal.js b/src/js/components/Modal/AddToPlaylistModal.js index b1c8ec01..3b95e25e 100755 --- a/src/js/components/Modal/AddToPlaylistModal.js +++ b/src/js/components/Modal/AddToPlaylistModal.js @@ -25,11 +25,11 @@ export default class AddToPlaylistModal extends React.Component{ return (

Add to playlist

-
+
{ playlists.map( playlist => { return ( -
this.playlistSelected(playlist.uri) }> +
this.playlistSelected(playlist.uri) }>   { playlist.name } diff --git a/src/js/components/Modal/Modal.js b/src/js/components/Modal/Modal.js index 5f230379..2a743177 100755 --- a/src/js/components/Modal/Modal.js +++ b/src/js/components/Modal/Modal.js @@ -8,10 +8,12 @@ import Icon from '../Icon' import AddToPlaylistModal from './AddToPlaylistModal' import CreatePlaylistModal from './CreatePlaylistModal' import EditPlaylistModal from './EditPlaylistModal' +import SendAuthorizationModal from './SendAuthorizationModal' import * as uiActions from '../../services/ui/actions' import * as mopidyActions from '../../services/mopidy/actions' import * as spotifyActions from '../../services/spotify/actions' +import * as pusherActions from '../../services/pusher/actions' class Modal extends React.Component{ @@ -32,6 +34,7 @@ class Modal extends React.Component{ { this.props.modal.name == 'add_to_playlist' ? : null } { this.props.modal.name == 'create_playlist' ? : null } { this.props.modal.name == 'edit_playlist' ? : null } + { this.props.modal.name == 'send_authorization' ? : null }
@@ -52,6 +55,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = (dispatch) => { return { uiActions: bindActionCreators(uiActions, dispatch), + pusherActions: bindActionCreators(pusherActions, dispatch), spotifyActions: bindActionCreators(spotifyActions, dispatch), mopidyActions: bindActionCreators(mopidyActions, dispatch) } diff --git a/src/js/components/Modal/SendAuthorizationModal.js b/src/js/components/Modal/SendAuthorizationModal.js new file mode 100755 index 00000000..f96f0318 --- /dev/null +++ b/src/js/components/Modal/SendAuthorizationModal.js @@ -0,0 +1,70 @@ + +import React, { PropTypes } from 'react' +import { connect } from 'react-redux' +import { Link } from 'react-router' +import { createStore, bindActionCreators } from 'redux' + +import * as uiActions from '../../services/ui/actions' +import * as helpers from '../../helpers' + +import Icon from '../Icon' + +class SendAuthorizationModal extends React.Component{ + + constructor(props){ + super(props) + } + + handleClick(e, connectionid){ + e.preventDefault() + var data = { + recipients: [connectionid], + action: 'send_authorization', + data: { + authorization: this.props.authorization, + me: this.props.me + } + } + this.props.pusherActions.instruct( 'broadcast', data ) + this.props.uiActions.closeModal() + return false; + } + + render(){ + return ( +
+

Share Spotify authentication

+

Send your authentication tokens to another client. When the recipient client imports this, their Iris will have full access to your Spotify account.

+
+ { + this.props.connections.map( (connection, index) => { + return ( +
this.handleClick(e, connection.connectionid) }> + { connection.username } +   + ({ connection.ip }) +
+ ); + }) + } +
+
+ ) + } +} + +const mapStateToProps = (state, ownProps) => { + return { + me: state.spotify.me, + authorization: state.spotify.authorization, + connections: state.pusher.connections + } +} + +const mapDispatchToProps = (dispatch) => { + return { + uiActions: bindActionCreators(uiActions, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(SendAuthorizationModal) \ No newline at end of file diff --git a/src/js/components/Parallax.js b/src/js/components/Parallax.js index 662b26cf..af94cd06 100755 --- a/src/js/components/Parallax.js +++ b/src/js/components/Parallax.js @@ -56,7 +56,7 @@ export default class Parallax extends React.Component{ } componentWillReceiveProps( nextProps ){ - if( ( !this.props.url || nextProps.image != this.props.url ) && !this._loading ){ + if( ( !this.state.url || nextProps.image != this.state.url ) && !this._loading ){ this._loading = true this.setState({ url: nextProps.image, image: false, loading: true }) this.loadImage( nextProps.image ) diff --git a/src/js/components/SpotifyAuthenticationFrame.js b/src/js/components/SpotifyAuthenticationFrame.js index d70369bf..e6216c44 100755 --- a/src/js/components/SpotifyAuthenticationFrame.js +++ b/src/js/components/SpotifyAuthenticationFrame.js @@ -99,12 +99,12 @@ class SpotifyAuthenticationFrame extends React.Component{ render(){ return ( -
+ { this.renderAuthorizeButton() }    { this.renderRefreshButton() } -
+ ); } } diff --git a/src/js/components/Thumbnail.js b/src/js/components/Thumbnail.js index 66dc631c..0a1ce0bf 100755 --- a/src/js/components/Thumbnail.js +++ b/src/js/components/Thumbnail.js @@ -6,41 +6,61 @@ export default class Thumbnail extends React.Component{ constructor(props) { super(props); - - this.state = { - url: require('../../assets/no-image.svg') - } } - componentDidMount(){ - this.mapImageSizes(); - } + // TODO: ascertain whether this is improving or hindering performance + // The UI appears to work perfectly fine without this + shouldComponentUpdate(nextProps, nextState){ - componentWillReceiveProps( nextProps ){ - this.mapImageSizes( nextProps ); + // no images at all, and we already know it + if( + typeof(nextProps.image) == 'undefined' && + typeof(this.props.image) == 'undefined' && + typeof(nextProps.images) == 'undefined' && + typeof(this.props.images) == 'undefined' + ) return false + + // image changed + if( !this.props.image && nextProps.image ) return true + if( this.props.image && nextProps.image ) return true + if( this.props.image != nextProps.image ) return true + + // images array changed + if( typeof(this.props.images) === 'undefined' && nextProps.images ) return true + if( this.props.images && typeof(nextProps.images) === 'undefined' ) return true + if( this.props.images.length != nextProps.images.length ) return true + + // image item changed + var size = 'medium' + var images = helpers.sizedImages( nextProps.images ) + if( this.props.size ) size = this.props.size + if( this.props.images[size] != images[size] ) return true + + return false } mapImageSizes( props = this.props ){ // no images if( !this.props.image && !this.props.images ){ - this.setState({ url: require('../../assets/no-image.svg') }) + return require('../../assets/no-image.svg') // single image }else if( this.props.image ){ - this.setState({ url: this.props.image }) + return this.props.image // multiple images }else if( this.props.images && this.props.images.length > 0 ){ var images = helpers.sizedImages( this.props.images ) var size = 'medium' if( this.props.size ) size = this.props.size - this.setState({ url: images[size] }) + return images[size] } } render(){ - var style = { backgroundImage: 'url("'+this.state.url+'")' } + var image = this.mapImageSizes() + var style = { backgroundImage: 'url("'+image+'")' } var className = 'thumbnail '+this.props.size; if( this.props.circle ) className += ' circle'; diff --git a/src/js/components/Track.js b/src/js/components/Track.js index 8962878b..856f327a 100755 --- a/src/js/components/Track.js +++ b/src/js/components/Track.js @@ -20,6 +20,13 @@ export default class Track extends React.Component{ } } + handleTouchEnd(e){ + var target = $(e.target); + if( !target.is('a') && target.closest('a').length <= 0 ){ + this.props.handleTouchEnd(e); + } + } + handleContextMenu(e){ e.preventDefault(); this.props.handleContextMenu(e); @@ -46,7 +53,7 @@ export default class Track extends React.Component{
this.props.handleTouchStart(e) } - onTouchEnd={ e => this.props.handleTouchEnd(e) } + onTouchEnd={ e => this.handleTouchEnd(e) } onMouseDown={ e => this.handleMouseDown(e) } onMouseUp={ e => this.props.handleMouseUp(e) } onDoubleClick={ e => this.props.handleDoubleClick(e) } diff --git a/src/js/helpers.js b/src/js/helpers.js index 8d13767a..5307132d 100755 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -257,24 +257,43 @@ export let createRange = function (indexes){ export let sortItems = function (array, property, reverse = false){ function compare(a,b) { - switch( typeof(a[property]) ){ + var a_value = a + var a_property_split = property.split('.') + for( var i = 0; i < a_property_split.length; i++ ){ + if( typeof(a_value[a_property_split[i]]) === 'undefined' ) return -1 + a_value = a_value[a_property_split[i]] + } - case 'boolean': - return a[property] - break + var b_value = b + var b_property_split = property.split('.') + for( var i = 0; i < b_property_split.length; i++ ){ + if( typeof(b_value[b_property_split[i]]) === 'undefined' ) return -1 + b_value = b_value[b_property_split[i]] + } - default: + if( typeof(a_value) === 'boolean'){ + return a_value - // both objects must have this property - if( typeof(a[property]) === 'undefined' || typeof(b[property]) === 'undefined' ) return 0 + }else if( typeof(a_value) === 'string'){ + if(a_value.toLowerCase() > b_value.toLowerCase()) return 1 + return -1 - if(a[property] > b[property]) return 1 - if(a[property] < b[property]) return -1 - return 0 + }else{ + if( parseInt(a_value) > parseInt(b_value) ) return 1 + return -1 } } var sorted = array.sort(compare) if( reverse ) sorted.reverse() return sorted +} + +/** + * Figure out if a value is a number + * @param data = mixed + * @return boolean + **/ +export let isNumeric = function (data) { + return !isNaN(parseFloat(data)) && isFinite(data) } \ No newline at end of file diff --git a/src/js/services/localstorage/middleware.js b/src/js/services/localstorage/middleware.js index 01b275c3..67bac113 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 ){ @@ -57,10 +57,14 @@ const localstorageMiddleware = (function(){ break; case 'SPOTIFY_SET_CONFIG': - var spotify = { - country: action.config.country, - locale: action.config.locale - }; + var spotify = JSON.parse( localStorage.getItem('spotify') ); + if( !spotify ) spotify = {}; + Object.assign( + spotify,{ + country: action.config.country, + locale: action.config.locale + } + ); localStorage.setItem('spotify', JSON.stringify(spotify)); break; @@ -70,6 +74,7 @@ const localstorageMiddleware = (function(){ Object.assign( spotify,{ authorized: true, + authorization: action.data, access_token: action.data.access_token, refresh_token: action.data.refresh_token, token_expiry: action.data.token_expiry @@ -109,9 +114,8 @@ const localstorageMiddleware = (function(){ var spotify = JSON.parse( localStorage.getItem('spotify') ); if( !spotify ) spotify = {}; Object.assign( - spotify,{ - me: action.data - } + spotify, + { me: action.data } ); localStorage.setItem('spotify', JSON.stringify(spotify)); break; diff --git a/src/js/services/pusher/actions.js b/src/js/services/pusher/actions.js index 7e5f00a7..585d79c6 100755 --- a/src/js/services/pusher/actions.js +++ b/src/js/services/pusher/actions.js @@ -43,10 +43,10 @@ export function getConnectionList(){ } } -export function instruct( action, data = null ){ +export function instruct( message_type, data = null ){ return { type: 'PUSHER_INSTRUCT', - action: action, + message_type: message_type, data: data } } diff --git a/src/js/services/pusher/middleware.js b/src/js/services/pusher/middleware.js index 525aa47c..6d8c2980 100755 --- a/src/js/services/pusher/middleware.js +++ b/src/js/services/pusher/middleware.js @@ -87,7 +87,7 @@ const PusherMiddleware = (function(){ break; case 'PUSHER_INSTRUCT': - switch( action.action ){ + switch( action.message_type ){ case 'query': makeRequest( action.data ) break @@ -150,6 +150,20 @@ const PusherMiddleware = (function(){ var notification = new notification( title, options ); break; + case 'PUSHER_SEND_AUTHORIZATION': + if( window.confirm('Spotify authorization for user '+action.data.me.id+' received. Do you want to import?') ){ + + // remove any existing authentication + store.dispatch({ type: 'SPOTIFY_AUTHORIZATION_REVOKED' }) + + // import our new authentication + store.dispatch({ type: 'SPOTIFY_ME_LOADED', data: action.data.me }) + store.dispatch({ type: 'SPOTIFY_AUTHORIZATION_GRANTED', data: action.data.authorization }) + }else{ + console.log('Authorization ignored') + } + 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 fd329779..17bbf5b2 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -52,7 +52,7 @@ function getToken( dispatch, getState ){ // token is okay for now, so just resolve with the current token if( new Date().getTime() < getState().spotify.token_expiry ){ - resolve(getState().spotify.access_token); + resolve(getState().spotify.access_token) return } @@ -81,16 +81,17 @@ function refreshToken( dispatch, getState ){ }) .then( response => { - response.token_expiry = new Date().getTime() + ( response.expires_in * 1000 ); - response.source = 'spotify'; + response.token_expiry = new Date().getTime() + ( response.expires_in * 1000 ) + response.source = 'spotify' dispatch({ type: 'SPOTIFY_TOKEN_REFRESHED', provider: 'spotify-http-api', data: response - }); - resolve(response); + }) + resolve(response) }, error => { + dispatch({ type: 'SPOTIFY_DISCONNECTED' }) console.error('Could not refresh token', error) reject(error) } @@ -116,6 +117,7 @@ function refreshToken( dispatch, getState ){ resolve(response); }, error => { + dispatch({ type: 'SPOTIFY_DISCONNECTED' }) console.error('Could not refresh token', error) reject(error) } diff --git a/src/js/services/spotify/reducer.js b/src/js/services/spotify/reducer.js index b30fa2ed..5ec46a0b 100755 --- a/src/js/services/spotify/reducer.js +++ b/src/js/services/spotify/reducer.js @@ -4,10 +4,13 @@ export default function reducer(spotify = {}, action){ case 'SPOTIFY_CONNECT': case 'SPOTIFY_CONNECTING': - return Object.assign({}, spotify, { connected: false, connecting: true }); + return Object.assign({}, spotify, { connected: false, connecting: true }) case 'SPOTIFY_CONNECTED': - return Object.assign({}, spotify, { connected: true, connecting: false }); + return Object.assign({}, spotify, { connected: true, connecting: false }) + + case 'SPOTIFY_DISCONNECTED': + return Object.assign({}, spotify, { connected: false, connecting: false }) case 'PUSHER_SPOTIFY_TOKEN': if( spotify.authorized ) return spotify; @@ -17,7 +20,7 @@ export default function reducer(spotify = {}, action){ authorization: false, access_token: action.data.access_token, token_expiry: action.data.token_expiry - }); + }) case 'SPOTIFY_AUTHORIZATION_GRANTED': return Object.assign({}, spotify, { @@ -27,7 +30,7 @@ export default function reducer(spotify = {}, action){ access_token: action.data.access_token, refresh_token: action.data.refresh_token, token_expiry: action.data.token_expiry - }); + }) case 'SPOTIFY_AUTHORIZATION_REVOKED': return Object.assign({}, spotify, { @@ -38,45 +41,45 @@ export default function reducer(spotify = {}, action){ refresh_token: false, token_expiry: 0, me: false - }); + }) case 'SPOTIFY_TOKEN_REFRESHING': - return Object.assign({}, spotify, { refreshing_token: true }); + return Object.assign({}, spotify, { refreshing_token: true }) case 'SPOTIFY_TOKEN_REFRESHED': return Object.assign({}, spotify, { + connected: true, refreshing_token: false, - authorization: action.data, access_token: action.data.access_token, token_expiry: action.data.token_expiry, provider: action.provider - }); + }) case 'SPOTIFY_DISCONNECTED': - return Object.assign({}, spotify, { connected: false, connecting: false }); + return Object.assign({}, spotify, { connected: false, connecting: false }) case 'SPOTIFY_ME_LOADED': - return Object.assign({}, spotify, { me: action.data }); + return Object.assign({}, spotify, { me: action.data }) case 'SPOTIFY_ARTISTS_LOADED': if( !action.data ) return Object.assign({}, spotify) return Object.assign({}, spotify, { artists: action.data.artists.items, artists_more: action.data.artists.next - }); + }) case 'SPOTIFY_LIBRARY_ARTISTS_LOADED': if( !action.data ) return Object.assign({}, spotify) return Object.assign({}, spotify, { library_artists: action.data.artists.items, library_artists_more: action.data.artists.next - }); + }) case 'SPOTIFY_LIBRARY_ARTISTS_LOADED_MORE': return Object.assign({}, spotify, { library_artists: [ ...spotify.library_artists, ...action.data.artists.items ], library_artists_more: action.data.artists.next - }); + }) case 'SPOTIFY_LIBRARY_ALBUMS_LOADED': if( !action.data ) return Object.assign({}, spotify) @@ -93,7 +96,7 @@ export default function reducer(spotify = {}, action){ return Object.assign({}, spotify, { library_albums: albums, library_albums_more: action.data.next - }); + }) case 'SPOTIFY_LIBRARY_ALBUMS_LOADED_MORE': var albums = [] @@ -109,7 +112,7 @@ export default function reducer(spotify = {}, action){ return Object.assign({}, spotify, { library_albums: [...spotify.library_albums, ...albums ], library_albums_more: action.data.next - }); + }) case 'SPOTIFY_LIBRARY_TRACKS_LOADED': if( !action.data ) return Object.assign({}, spotify) @@ -126,7 +129,7 @@ export default function reducer(spotify = {}, action){ return Object.assign({}, spotify, { library_tracks: tracks, library_tracks_more: action.data.next - }); + }) case 'SPOTIFY_LIBRARY_TRACKS_LOADED_MORE': var tracks = Object.assign([], action.data.items) @@ -142,19 +145,19 @@ export default function reducer(spotify = {}, action){ return Object.assign({}, spotify, { library_tracks: [...spotify.library_tracks, ...tracks], library_tracks_more: action.data.next - }); + }) case 'SPOTIFY_FEATURED_PLAYLISTS_LOADED': - return Object.assign({}, spotify, { featured_playlists: action.data }); + return Object.assign({}, spotify, { featured_playlists: action.data }) case 'SPOTIFY_CATEGORIES_LOADED': - return Object.assign({}, spotify, { categories: action.data }); + return Object.assign({}, spotify, { categories: action.data }) case 'SPOTIFY_CATEGORY_LOADED': - return Object.assign({}, spotify, { category: action.data }); + return Object.assign({}, spotify, { category: action.data }) case 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED': - return Object.assign({}, spotify, { category_playlists: action.data }); + return Object.assign({}, spotify, { category_playlists: action.data }) case 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED_MORE': return Object.assign({}, spotify, { category_playlists: { @@ -162,7 +165,7 @@ export default function reducer(spotify = {}, action){ next: action.data.next, previous: action.data.previous, items: [ ...spotify.category_playlists.items, ...action.data.items ] - }}); + }}) case 'SPOTIFY_NEW_RELEASES_LOADED': return Object.assign({}, spotify, { new_releases: action.data }); @@ -173,7 +176,7 @@ export default function reducer(spotify = {}, action){ next: action.data.albums.next, previous: action.data.albums.previous, items: [ ...spotify.new_releases.items, ...action.data.albums.items ] - }}); + }}) default: return spotify diff --git a/src/js/views/Settings.js b/src/js/views/Settings.js index 941fd972..8e5affc2 100755 --- a/src/js/views/Settings.js +++ b/src/js/views/Settings.js @@ -13,6 +13,7 @@ import VersionManager from '../components/VersionManager' import Header from '../components/Header' import Thumbnail from '../components/Thumbnail' +import * as uiActions from '../services/ui/actions' import * as pusherActions from '../services/pusher/actions' import * as mopidyActions from '../services/mopidy/actions' import * as spotifyActions from '../services/spotify/actions' @@ -108,6 +109,18 @@ class Settings extends React.Component{ } } + renderSendAuthorizationButton(){ + if( !this.props.spotify.authorized ) return null + + return ( + + ) + } + render(){ return (
@@ -175,13 +188,13 @@ class Settings extends React.Component{

Spotify

-
this.setSpotifyConfig(e)}> -
-
Status
-
- { this.renderConnectionStatus('spotify') } -
+
+
Status
+
+ { this.renderConnectionStatus('spotify') }
+
+
Country
@@ -202,21 +215,23 @@ class Settings extends React.Component{ value={ this.state.spotify_locale } />
-
-
Current user
-
-
- { this.renderSpotifyUser() } -
-
-
-
-
Authentication
-
- -
-
+
+
Current user
+
+
+ { this.renderSpotifyUser() } +
+
+
+
+
Authentication
+
+ +    + { this.renderSendAuthorizationButton() } +
+

Advanced

@@ -278,6 +293,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = (dispatch) => { return { + uiActions: bindActionCreators(uiActions, dispatch), pusherActions: bindActionCreators(pusherActions, dispatch), mopidyActions: bindActionCreators(mopidyActions, dispatch), spotifyActions: bindActionCreators(spotifyActions, dispatch) diff --git a/src/js/views/User.js b/src/js/views/User.js index 502a6121..305ce8a5 100755 --- a/src/js/views/User.js +++ b/src/js/views/User.js @@ -8,6 +8,7 @@ import Thumbnail from '../components/Thumbnail' import PlaylistGrid from '../components/PlaylistGrid' import FollowButton from '../components/FollowButton' import LazyLoadListener from '../components/LazyLoadListener' +import Header from '../components/Header' import * as helpers from '../helpers' import * as mopidyActions from '../services/mopidy/actions' @@ -51,6 +52,9 @@ class User extends React.Component{ return (
+ +
+
diff --git a/src/js/views/library/LibraryPlaylists.js b/src/js/views/library/LibraryPlaylists.js index 5522c425..589b42e8 100755 --- a/src/js/views/library/LibraryPlaylists.js +++ b/src/js/views/library/LibraryPlaylists.js @@ -21,12 +21,23 @@ class LibraryPlaylists extends React.Component{ super(props); } + setSort(value){ + var reverse = false + if( this.props.sort == value ) reverse = !this.props.sort_reverse + + var data = { + library_playlists_sort_reverse: reverse, + library_playlists_sort: value + } + this.props.uiActions.set(data) + } + renderView(){ if( !this.props.playlists ) return null var playlists = this.props.playlists if( this.props.sort ){ - playlists = helpers.sortItems(playlists, this.props.sort) + playlists = helpers.sortItems(playlists, this.props.sort, this.props.sort_reverse) } if( this.props.view == 'list' ){ @@ -96,7 +107,7 @@ class LibraryPlaylists extends React.Component{ var actions = (
- this.props.uiActions.set({ library_playlists_sort: value }) } /> + this.setSort(value) } /> this.props.uiActions.set({ library_playlists_view: value }) } />