diff --git a/src/js/actions/albumActions.js b/src/js/actions/albumActions.js deleted file mode 100755 index d7864d46..00000000 --- a/src/js/actions/albumActions.js +++ /dev/null @@ -1,12 +0,0 @@ - -/** - * Actions and Action Creators - **/ - -export const LOAD_ALBUMS = 'LOAD_ALBUMS' - -export function loadAlbums(){ - return { - type: LOAD_ALBUMS - } -} \ No newline at end of file diff --git a/src/js/actions/index.js b/src/js/actions/index.js deleted file mode 100755 index c632dc3a..00000000 --- a/src/js/actions/index.js +++ /dev/null @@ -1,3 +0,0 @@ - -import albumActions from './albumActions' - diff --git a/src/js/actions/queueActions.js b/src/js/actions/queueActions.js deleted file mode 100755 index e69de29b..00000000 diff --git a/src/js/actions/spotifyActions.js b/src/js/actions/spotifyActions.js deleted file mode 100755 index 041e620e..00000000 --- a/src/js/actions/spotifyActions.js +++ /dev/null @@ -1,29 +0,0 @@ - -/** - * Actions and Action Creators - **/ - -export const AUTHORIZE_SPOTIFY = 'AUTHORIZE_SPOTIFY' -export const AUTHORIZE_SPOTIFY_SUCCESS = 'AUTHORIZE_SPOTIFY_SUCCESS' -export const MOPIDY_ONLINE = 'MOPIDY_ONLINE' - -export function authorizeSpotify( authorize = true ){ - return { - type: AUTHORIZE_SPOTIFY, - authorize: authorize - } -} - -export function authorizeSpotifySuccess( authorization ){ - return { - type: AUTHORIZE_SPOTIFY_SUCCESS, - authorization: authorization - } -} - -export function mopidyOnline( online ){ - return { - type: MOPIDY_ONLINE, - online: online - } -} \ No newline at end of file diff --git a/src/js/bootstrap.js b/src/js/bootstrap.js index 8ba7ef11..060fbb95 100755 --- a/src/js/bootstrap.js +++ b/src/js/bootstrap.js @@ -9,16 +9,23 @@ import spotify from './services/spotify/reducer' import thunk from 'redux-thunk' import mopidyMiddleware from './services/mopidy/middleware' import spotifyMiddleware from './services/spotify/middleware' +import localstorageMiddleware from './services/localstorageMiddleware' let reducers = combineReducers({ mopidy, spotify }); +// load our state from localStorage +var initialState = {}; +if( localStorage.getItem('state') ){ + initialState = JSON.parse( localStorage.getItem('state') ); +} + let store = createStore( reducers, - {}, - applyMiddleware( thunk, mopidyMiddleware, spotifyMiddleware ) + initialState, + applyMiddleware( thunk, localstorageMiddleware, mopidyMiddleware, spotifyMiddleware ) ); export default store; diff --git a/src/js/components/Artist.js b/src/js/components/Artist.js new file mode 100755 index 00000000..2e39c4a5 --- /dev/null +++ b/src/js/components/Artist.js @@ -0,0 +1,56 @@ + +import React, { PropTypes } from 'react' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' + +import TrackList from './TrackList' +import * as spotifyActions from '../services/spotify/actions' + +class Artist extends React.Component{ + + constructor(props) { + super(props); + } + + // on render + componentDidMount(){ + this.props.spotifyActions.loadArtist( this.props.params.uri ); + } + + // when props changed + componentWillReceiveProps( nextProps ){ + if( nextProps.params.uri != this.props.params.uri ){ + this.props.spotifyActions.loadArtist( nextProps.params.uri ); + } + } + + render(){ + if( this.props.spotify.artist ){ + return ( +
+

{ this.props.spotify.artist.name }

+
+ ); + } + return null; + } +} + + +/** + * Export our component + * + * We also integrate our global store, using connect() + **/ + +const mapStateToProps = (state, ownProps) => { + return state; +} + +const mapDispatchToProps = (dispatch) => { + return { + spotifyActions: bindActionCreators(spotifyActions, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Artist) \ No newline at end of file diff --git a/src/js/components/ConfirmationButton.js b/src/js/components/ConfirmationButton.js new file mode 100755 index 00000000..cb63e02d --- /dev/null +++ b/src/js/components/ConfirmationButton.js @@ -0,0 +1,39 @@ + +import React, { PropTypes } from 'react' +import FontAwesome from 'react-fontawesome' + +export default class ConfirmationButton extends React.Component{ + + constructor(props) { + super(props); + this.state = { + confirming: false + } + } + + handleClick(e){ + if( this.state.confirming ){ + this.setState({ confirming: false }); + this.props.onConfirm(); + }else{ + this.setState({ confirming: true }); + } + } + + render(){ + + var className = 'button'; + var content = this.props.content; + + if( this.state.confirming ){ + className += ' confirming'; + content = this.props.confirmingContent; + } + + return ( + this.handleClick(e) }> + { content } + + ); + } +} \ No newline at end of file diff --git a/src/js/components/SpotifyAuthenticationFrame.js b/src/js/components/SpotifyAuthenticationFrame.js index 216d09a8..20d0567c 100755 --- a/src/js/components/SpotifyAuthenticationFrame.js +++ b/src/js/components/SpotifyAuthenticationFrame.js @@ -2,6 +2,7 @@ import React, { PropTypes } from 'react' import { connect } from 'react-redux' import { createStore, bindActionCreators } from 'redux' +import FontAwesome from 'react-fontawesome' import * as actions from '../services/spotify/actions' @@ -11,9 +12,40 @@ class SpotifyAuthenticationFrame extends React.Component{ super(props); } - handleClick(e){ - console.log('handling click') - this.props.actions.authorize(); + componentDidMount(){ + + let self = this; + + // listen for incoming messages from the authorization iframe + // this is triggered when authentication is granted from the popup + window.addEventListener('message', function(event){ + + // only allow incoming data from our authorized authenticator proxy + if( !/^https?:\/\/jamesbarnsley\.co\.nz/.test(event.origin) ) return false; + + var data = JSON.parse(event.data); + self.props.actions.completeAuthorization( data ); + + }, false); + } + + renderAuthorizeButton(){ + if( this.props.spotify.authorizing ){ + return ( + + + Authorizing... + + ); + }else if( this.props.spotify.authorization ){ + return ( + + ); + }else{ + return ( + + ); + } } render(){ @@ -23,7 +55,7 @@ class SpotifyAuthenticationFrame extends React.Component{ } return (
- + { this.renderAuthorizeButton() }
); diff --git a/src/js/index.js b/src/js/index.js index 65add352..7eb82327 100755 --- a/src/js/index.js +++ b/src/js/index.js @@ -9,17 +9,24 @@ import { createStore } from 'redux' import { Router, Route, Link, hashHistory } from 'react-router' import store from './bootstrap.js' -import App from './components/App' -import Album from './components/Album' -import Queue from './components/Queue' + +import App from './views/App' +import Album from './views/Album' +import Artist from './views/Artist' +import Queue from './views/Queue' +import Settings from './views/Settings' +import LibraryArtists from './views/LibraryArtists' ReactDOM.render( + + + diff --git a/src/js/reducers/album.js b/src/js/reducers/album.js deleted file mode 100755 index 847330e3..00000000 --- a/src/js/reducers/album.js +++ /dev/null @@ -1,16 +0,0 @@ - -import * as actions from '../actions/albumActions' - -export default function album(album = {}, action) { - switch (action.type) { - - case actions.LOAD_ALBUM: - return album//Object.assign({}, album, action.album); - - default: - return album - } -} - - - diff --git a/src/js/reducers/index.js b/src/js/reducers/index.js deleted file mode 100755 index 5e0f3653..00000000 --- a/src/js/reducers/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Root reducer - * - * This combines all our application reducers into a single, top-level reducer. - * We need to combine reducers to create the unified application-level store. - **/ - -// import all of our application reducers -import album from './album' -import mopidy from '../services/mopidy/reducer' - -import { combineReducers } from 'redux' - -// combine them into one root reducer -export default combineReducers({ - album, - mopidy -}) - - - diff --git a/src/js/reducers/spotifyReducer.js b/src/js/reducers/spotifyReducer.js deleted file mode 100755 index 89ebda8c..00000000 --- a/src/js/reducers/spotifyReducer.js +++ /dev/null @@ -1,29 +0,0 @@ - -import * as actions from './actions' - -export default function reducer(state = {}, action){ - switch (action.type) { - - case actions.AUTHORIZE_SPOTIFY: - return Object.assign({}, state.spotify, { - authorizing: action.authorize - }); - - case actions.AUTHORIZE_SPOTIFY_SUCCESS: - console.info('Spotify authorization successful'); - var state = Object.assign({}, state.spotify, action.authorization) - state = Object.assign({}, state.spotify, { online: true, authorizingSpotify: false }) - return state - - case actions.MOPIDY_ONLINE: - return Object.assign({}, state.mopidy, { - online: action.online - }); - - default: - return state - } -} - - - diff --git a/src/js/services/localstorageMiddleware.js b/src/js/services/localstorageMiddleware.js new file mode 100755 index 00000000..cfcaa2dc --- /dev/null +++ b/src/js/services/localstorageMiddleware.js @@ -0,0 +1,20 @@ + +const localstorageMiddleware = (function(){ + + /** + * The actual middleware inteceptor + **/ + return store => next => action => { + + // proceed as normal first + // this way, any reducers and middleware do their thing BEFORE we store our new state + next(action); + + // get the state, and plug it in to our localStorage + var state = store.getState(); + localStorage.setItem('state', JSON.stringify(state)); + } + +})(); + +export default localstorageMiddleware \ No newline at end of file diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index 74b33c74..54b590f2 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -104,8 +104,11 @@ const MopidyMiddleware = (function(){ if(socket != null) socket.close(); store.dispatch({ type: 'MOPIDY_CONNECTING' }); + var domain = "tv.barnsley.nz"; + if( window.location.hostname == 'iris.james' ) domain = 'music.james'; + socket = new Mopidy({ - webSocketUrl: "ws://tv.barnsley.nz:6680/mopidy/ws", + webSocketUrl: 'ws://'+domain+':6680/mopidy/ws', callingConvention: 'by-position-or-by-name' }); diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js index 8640ac1c..87332b3e 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -3,9 +3,22 @@ * Actions and Action Creators **/ -export function authorize(){ +export function startAuthorization(){ return { - type: 'SPOTIFY_AUTHORIZING' + type: 'SPOTIFY_START_AUTHORIZATION' + } +} + +export function removeAuthorization(){ + return { + type: 'SPOTIFY_REMOVE_AUTHORIZATION' + } +} + +export function completeAuthorization( data ){ + return { + type: 'SPOTIFY_COMPLETE_AUTHORIZATION', + data: data } } @@ -28,3 +41,10 @@ export function loadAlbum( uri ){ } } +export function loadArtist( uri ){ + return { + type: 'SPOTIFY_LOAD_ARTIST', + uri: uri + } +} + diff --git a/src/js/services/spotify/middleware.js b/src/js/services/spotify/middleware.js index a2ee0f81..a565a075 100755 --- a/src/js/services/spotify/middleware.js +++ b/src/js/services/spotify/middleware.js @@ -3,30 +3,56 @@ import actions from './actions' const SpotifyMiddleware = (function(){ - return store => next => action => { + /** + * Send an ajax request to the Spotify API + * + * @param endpoint string = the url to query (ie /albums/:uri) + * @param method string + * @param data mixed = request payload + * @return ajax promise + **/ + const sendRequest = (endpoint, method = 'GET', data = false) => { + return $.ajax({ + method: method, + cache: true, + url: 'https://api.spotify.com/v1/'+endpoint, + data: data + }); + } + /** + * The actual middleware inteceptor + **/ + return store => next => action => { switch(action.type) { + case 'SPOTIFY_LOAD_ALBUM': - // clear the current album store.dispatch({ type: 'SPOTIFY_ALBUM_LOADED', data: false }) - var id = action.uri.replace('spotify:album:',''); - $.ajax({ - method: 'GET', - cache: true, - url: 'https://api.spotify.com/v1/albums/'+id, - success: function(album){ - store.dispatch({ type: 'SPOTIFY_ALBUM_LOADED', data: album }) - } - }); + + sendRequest('albums/'+id) + .then( (response) => { + store.dispatch({ type: 'SPOTIFY_ALBUM_LOADED', data: response }) + }); break; + + case 'SPOTIFY_LOAD_ARTIST': + + store.dispatch({ type: 'SPOTIFY_ARTIST_LOADED', data: false }) + var id = action.uri.replace('spotify:artist:',''); + + sendRequest('artists/'+id) + .then( (response) => { + store.dispatch({ type: 'SPOTIFY_ARTIST_LOADED', data: response }) + }); + break; + + case 'SPOTIFY_CONNECT': - console.log('Spotify wants to connect') - break; case 'SPOTIFY_DISCONNECT': diff --git a/src/js/services/spotify/reducer.js b/src/js/services/spotify/reducer.js index 606a2117..2a0e371b 100755 --- a/src/js/services/spotify/reducer.js +++ b/src/js/services/spotify/reducer.js @@ -5,9 +5,15 @@ export default function reducer(spotify = {}, action){ case 'SPOTIFY_CONNECTING': return Object.assign({}, spotify, { connected: false, connecting: true }); - case 'SPOTIFY_AUTHORIZING': + case 'SPOTIFY_START_AUTHORIZATION': return Object.assign({}, spotify, { authorizing: true }); + case 'SPOTIFY_COMPLETE_AUTHORIZATION': + return Object.assign({}, spotify, { authorizing: false, authorization: action.data }); + + case 'SPOTIFY_REMOVE_AUTHORIZATION': + return Object.assign({}, spotify, { authorizing: false, authorization: false }); + case 'SPOTIFY_CONNECTED': return Object.assign({}, spotify, { connected: true, connecting: false }); @@ -17,6 +23,12 @@ export default function reducer(spotify = {}, action){ case 'SPOTIFY_ALBUM_LOADED': return Object.assign({}, spotify, { album: action.data }); + case 'SPOTIFY_ARTIST_LOADED': + return Object.assign({}, spotify, { artist: action.data }); + + case 'SPOTIFY_LIBRARY_ARTISTS_LOADED': + return Object.assign({}, spotify, { library_artists: action.data }); + default: return spotify } diff --git a/src/js/components/Album.js b/src/js/views/Album.js similarity index 92% rename from src/js/components/Album.js rename to src/js/views/Album.js index 7e33223a..8f94d3a8 100755 --- a/src/js/components/Album.js +++ b/src/js/views/Album.js @@ -3,7 +3,7 @@ import React, { PropTypes } from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' -import TrackList from './TrackList' +import TrackList from '../components/TrackList' import * as spotifyActions from '../services/spotify/actions' class Album extends React.Component{ diff --git a/src/js/components/App.js b/src/js/views/App.js similarity index 76% rename from src/js/components/App.js rename to src/js/views/App.js index a2e7924c..2d88a544 100755 --- a/src/js/components/App.js +++ b/src/js/views/App.js @@ -5,10 +5,10 @@ import { createStore, bindActionCreators } from 'redux' import { Link } from 'react-router' import { connect } from 'react-redux' -import * as actions from '../actions/index' +import Player from '../components/Player' + import * as mopidyActions from '../services/mopidy/actions' import * as spotifyActions from '../services/spotify/actions' -import SpotifyAuthenticationFrame from './SpotifyAuthenticationFrame' /** @@ -30,10 +30,11 @@ class App extends React.Component{
- {this.props.children} +
); } @@ -51,7 +52,6 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = (dispatch) => { return { - actions: bindActionCreators(actions, dispatch), mopidyActions: bindActionCreators(mopidyActions, dispatch), spotifyActions: bindActionCreators(spotifyActions, dispatch) } diff --git a/src/js/views/Artist.js b/src/js/views/Artist.js new file mode 100755 index 00000000..5d9cc94d --- /dev/null +++ b/src/js/views/Artist.js @@ -0,0 +1,56 @@ + +import React, { PropTypes } from 'react' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' + +import TrackList from '../components/TrackList' +import * as spotifyActions from '../services/spotify/actions' + +class Artist extends React.Component{ + + constructor(props) { + super(props); + } + + // on render + componentDidMount(){ + this.props.spotifyActions.loadArtist( this.props.params.uri ); + } + + // when props changed + componentWillReceiveProps( nextProps ){ + if( nextProps.params.uri != this.props.params.uri ){ + this.props.spotifyActions.loadArtist( nextProps.params.uri ); + } + } + + render(){ + if( this.props.spotify.artist ){ + return ( +
+

{ this.props.spotify.artist.name }

+
+ ); + } + return null; + } +} + + +/** + * Export our component + * + * We also integrate our global store, using connect() + **/ + +const mapStateToProps = (state, ownProps) => { + return state; +} + +const mapDispatchToProps = (dispatch) => { + return { + spotifyActions: bindActionCreators(spotifyActions, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Artist) \ No newline at end of file diff --git a/src/js/views/LibraryArtists.js b/src/js/views/LibraryArtists.js new file mode 100755 index 00000000..8e5c8d8a --- /dev/null +++ b/src/js/views/LibraryArtists.js @@ -0,0 +1,45 @@ + +import React, { PropTypes } from 'react' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' + +import * as mopidyActions from '../services/mopidy/actions' +import * as spotifyActions from '../services/spotify/actions' + +import TrackList from '../components/TrackList' + +class LibraryArtists extends React.Component{ + + constructor(props) { + super(props); + } + + // on render + componentDidMount(){ + this.props.spotifyActions.loadLibraryArtists(); + } + + render(){ + return null; + } +} + + +/** + * Export our component + * + * We also integrate our global store, using connect() + **/ + +const mapStateToProps = (state, ownProps) => { + return state; +} + +const mapDispatchToProps = (dispatch) => { + return { + mopidyActions: bindActionCreators(mopidyActions, dispatch), + spotifyActions: bindActionCreators(spotifyActions, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(LibraryArtists) \ No newline at end of file diff --git a/src/js/components/Queue.js b/src/js/views/Queue.js similarity index 95% rename from src/js/components/Queue.js rename to src/js/views/Queue.js index 992d0afd..35e496dc 100755 --- a/src/js/components/Queue.js +++ b/src/js/views/Queue.js @@ -44,9 +44,9 @@ class Queue extends React.Component{

Now playing

{ this.renderTrackInFocus() } +

Other tracks

{ this.renderTrackList() } -
); } diff --git a/src/js/views/Settings.js b/src/js/views/Settings.js new file mode 100755 index 00000000..ba80014b --- /dev/null +++ b/src/js/views/Settings.js @@ -0,0 +1,53 @@ + +import React, { PropTypes } from 'react' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' + +import FontAwesome from 'react-fontawesome' +import SpotifyAuthenticationFrame from '../components/SpotifyAuthenticationFrame' +import ConfirmationButton from '../components/ConfirmationButton' + +import * as mopidyActions from '../services/mopidy/actions' +import * as spotifyActions from '../services/spotify/actions' + +class Settings extends React.Component{ + + constructor(props) { + super(props); + } + + resetAllSettings(){ + localStorage.removeItem('state'); + window.location.reload(); + } + + render(){ + return ( +
+

Settings

+ + this.resetAllSettings()} /> +
+ ); + } +} + + +/** + * Export our component + * + * We also integrate our global store, using connect() + **/ + +const mapStateToProps = (state, ownProps) => { + return state; +} + +const mapDispatchToProps = (dispatch) => { + return { + mopidyActions: bindActionCreators(mopidyActions, dispatch), + spotifyActions: bindActionCreators(spotifyActions, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Settings) \ No newline at end of file