@@ -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 (
+
+ )
+ }
+}
+
+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 (
-
+
);
}
}
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
-