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{
- Now playing
- - Library: Albums
+ - Library: My artists
+ - Settings
-
{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