Polishing spotify actions, removing middleware; Mopidy current track selection
This commit is contained in:
@ -4,6 +4,7 @@
|
||||
<title>Iris</title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed" rel="stylesheet">
|
||||
<link href="app.css" rel="stylesheet" />
|
||||
|
||||
</head>
|
||||
|
||||
@ -10,6 +10,11 @@ class SpotifyAuthenticationFrame extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
frameUrl: '//jamesbarnsley.co.nz/spotmop.php?action=frame',
|
||||
authorizing: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
@ -25,28 +30,41 @@ class SpotifyAuthenticationFrame extends React.Component{
|
||||
|
||||
var data = JSON.parse(event.data);
|
||||
self.props.actions.authorizationGranted( data );
|
||||
self.props.actions.getMe();
|
||||
|
||||
// and turn off our authorizing switch
|
||||
self.setState({
|
||||
frameUrl: '//jamesbarnsley.co.nz/spotmop.php?action=frame',
|
||||
authorizing: false
|
||||
})
|
||||
|
||||
}, false);
|
||||
}
|
||||
|
||||
startAuthorization(){
|
||||
this.setState({
|
||||
frameUrl: '//jamesbarnsley.co.nz/spotmop.php?action=authorize&app='+location.protocol+'//'+window.location.host,
|
||||
authorizing: true
|
||||
})
|
||||
}
|
||||
|
||||
renderMe(){
|
||||
if( this.props.spotify.me ){
|
||||
if( this.props.spotify.me.display_name ){
|
||||
return <span>Logged in as { this.props.spotify.me.display_name }</span>
|
||||
}else{
|
||||
return <span>Logged in as { this.props.spotify.me.id }</span>
|
||||
}
|
||||
if( !this.props.spotify.me ) return null;
|
||||
|
||||
if( this.props.spotify.me.display_name ){
|
||||
return <span>Logged in as { this.props.spotify.me.display_name }</span>
|
||||
}else{
|
||||
return <span>Logged in as { this.props.spotify.me.id }</span>
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderAuthorizeButton(){
|
||||
if( this.props.spotify.authorizing ){
|
||||
if( this.state.authorizing ){
|
||||
return (
|
||||
<span>
|
||||
<button disabled>
|
||||
<FontAwesome name="circle-o-notch" spin />
|
||||
<span>Authorizing...</span>
|
||||
</span>
|
||||
Authorizing...
|
||||
</button>
|
||||
);
|
||||
}else if( this.props.spotify.authorized ){
|
||||
return (
|
||||
@ -57,20 +75,34 @@ class SpotifyAuthenticationFrame extends React.Component{
|
||||
);
|
||||
}else{
|
||||
return (
|
||||
<button onClick={() => this.props.actions.startAuthorization()}>Log in</button>
|
||||
<button onClick={() => this.startAuthorization()}>Log in</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderRefreshButton(){
|
||||
if( !this.props.spotify.authorized ) return null;
|
||||
|
||||
if( this.props.spotify.refreshing_token ){
|
||||
return (
|
||||
<button disabled>
|
||||
<FontAwesome name="circle-o-notch" spin />
|
||||
Refreshing...
|
||||
</button>
|
||||
);
|
||||
}else{
|
||||
return (
|
||||
<button onClick={() => this.props.actions.refreshToken()}>Refresh token</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
var src = '//jamesbarnsley.co.nz/spotmop.php?action=frame';
|
||||
if( this.props.spotify.authorizing ){
|
||||
src = '//jamesbarnsley.co.nz/spotmop.php?action=authorize&app='+location.protocol+'//'+window.location.host;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{ this.renderAuthorizeButton() }
|
||||
<iframe src={src} style={{ display: 'none' }}></iframe>
|
||||
{ this.renderRefreshButton() }
|
||||
<iframe src={this.state.frameUrl} style={{ display: 'none' }}></iframe>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -39,27 +39,33 @@ export default class Track extends React.Component{
|
||||
|
||||
render(){
|
||||
|
||||
var className = 'track';
|
||||
var selectedIcon = 'square-o';
|
||||
if( typeof(this.props.track.selected) !== 'undefined' && this.props.track.selected ){
|
||||
selectedIcon = 'check-square-o';
|
||||
className += ' selected';
|
||||
}
|
||||
|
||||
if( this.props.track.playing ){
|
||||
className += ' playing';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="track"
|
||||
className={className}
|
||||
onDoubleClick={ (e) => this.handleDoubleClick(e) }
|
||||
onClick={ (e) => this.handleClick(e) }>
|
||||
<FontAwesome name={selectedIcon} fixedWidth />
|
||||
<span className="name">
|
||||
<FontAwesome name={selectedIcon} className="select-state" fixedWidth />
|
||||
<span className="col name">
|
||||
{this.props.track.name}
|
||||
</span>
|
||||
<span className="artists">
|
||||
<span className="col artists">
|
||||
<ArtistList artists={this.props.track.artists} />
|
||||
</span>
|
||||
<span className="album">
|
||||
<span className="col album">
|
||||
{ this.props.track.album ? <AlbumLink album={this.props.track.album} /> : null }
|
||||
</span>
|
||||
<span className="duration">
|
||||
<span className="col duration">
|
||||
{ this.formatDuration() }
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -15,11 +15,14 @@ require('../scss/app.scss');
|
||||
import App from './views/App'
|
||||
import Album from './views/Album'
|
||||
import Artist from './views/Artist'
|
||||
import Playlist from './views/Playlist'
|
||||
import Queue from './views/Queue'
|
||||
import Settings from './views/Settings'
|
||||
import LibraryArtists from './views/LibraryArtists'
|
||||
import LibraryAlbums from './views/LibraryAlbums'
|
||||
import LibraryTracks from './views/LibraryTracks'
|
||||
|
||||
import LibraryArtists from './views/library/LibraryArtists'
|
||||
import LibraryAlbums from './views/library/LibraryAlbums'
|
||||
import LibraryTracks from './views/library/LibraryTracks'
|
||||
import LibraryPlaylists from './views/library/LibraryPlaylists'
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
@ -29,8 +32,11 @@ ReactDOM.render(
|
||||
<Route path="library/artists" component={LibraryArtists} />
|
||||
<Route path="library/albums" component={LibraryAlbums} />
|
||||
<Route path="library/tracks" component={LibraryTracks} />
|
||||
<Route path="library/playlists" component={LibraryPlaylists} />
|
||||
|
||||
<Route path="album/:uri" component={Album} />
|
||||
<Route path="artist/:uri" component={Artist} />
|
||||
<Route path="playlist/:uri" component={Playlist} />
|
||||
<Route path="queue" component={Queue} />
|
||||
<Route path="settings" component={Settings} />
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ export function disconnect(){
|
||||
|
||||
export function changeTrack( tlid ){
|
||||
return {
|
||||
type: 'MOPIDY_CHANGE_TRACK',
|
||||
type: 'MOPIDY_INSTRUCT',
|
||||
call: 'playback.play',
|
||||
value: { tlid: tlid }
|
||||
}
|
||||
@ -38,11 +38,11 @@ export function playTracks( uris ){
|
||||
}
|
||||
|
||||
export function enqueueTracks( uris, at_position = false ){
|
||||
if( typeof(uris) !== 'array' ) uris = [uris];
|
||||
if( typeof(uris) !== 'object' ) uris = [uris];
|
||||
var value = { uris: uris };
|
||||
if( at_position ) value.at_position = at_position;
|
||||
return {
|
||||
type: 'MOPIDY_ENQUEUE_TRACKS',
|
||||
type: 'MOPIDY_INSTRUCT',
|
||||
call: 'tracklist.add',
|
||||
value: value
|
||||
}
|
||||
@ -50,7 +50,7 @@ export function enqueueTracks( uris, at_position = false ){
|
||||
|
||||
export function removeTracks( tlids ){
|
||||
return {
|
||||
type: 'MOPIDY_REMOVE_TRACKS',
|
||||
type: 'MOPIDY_INSTRUCT',
|
||||
call: 'tracklist.remove',
|
||||
value: { tlid: tlids }
|
||||
}
|
||||
|
||||
@ -19,7 +19,10 @@ const MopidyMiddleware = (function(){
|
||||
instruct( ws, store, 'tracklist.getRandom' );
|
||||
instruct( ws, store, 'tracklist.getRepeat' );
|
||||
instruct( ws, store, 'tracklist.getTlTracks' );
|
||||
instruct( ws, store, 'playback.getCurrentTlTrack' );
|
||||
instruct( ws, store, 'playback.getCurrentTlTrack' )
|
||||
.then( currentTlTrack => {
|
||||
store.dispatch({ type: 'MOPIDY_HIGHLIGHT_CURRENT_TLTRACK', data: currentTlTrack })
|
||||
});
|
||||
break;
|
||||
|
||||
case 'state:offline':
|
||||
@ -27,14 +30,20 @@ const MopidyMiddleware = (function(){
|
||||
break;
|
||||
|
||||
case 'event:tracklistChanged':
|
||||
instruct( ws, store, 'tracklist.getTlTracks' );
|
||||
instruct( ws, store, 'tracklist.getTlTracks' )
|
||||
.then( currentTlTrack => {
|
||||
store.dispatch({ type: 'MOPIDY_HIGHLIGHT_CURRENT_TLTRACK', data: currentTlTrack })
|
||||
});
|
||||
break;
|
||||
|
||||
//case 'event:trackPlaybackEnded':
|
||||
case 'event:playbackStateChanged':
|
||||
case 'event:trackPlaybackStarted':
|
||||
instruct( ws, store, 'playback.getState' );
|
||||
instruct( ws, store, 'playback.getCurrentTlTrack' );
|
||||
instruct( ws, store, 'playback.getCurrentTlTrack' )
|
||||
.then( currentTlTrack => {
|
||||
store.dispatch({ type: 'MOPIDY_HIGHLIGHT_CURRENT_TLTRACK', data: currentTlTrack })
|
||||
});
|
||||
break;
|
||||
|
||||
case 'event:volumeChanged':
|
||||
@ -122,8 +131,6 @@ const MopidyMiddleware = (function(){
|
||||
|
||||
// send an instruction to the websocket
|
||||
case 'MOPIDY_INSTRUCT':
|
||||
case 'MOPIDY_CHANGE_TRACK':
|
||||
case 'MOPIDY_REMOVE_TRACKS':
|
||||
instruct( socket, store, action.call, action.value )
|
||||
break;
|
||||
|
||||
|
||||
@ -16,9 +16,20 @@ export default function reducer(mopidy = {}, action){
|
||||
|
||||
case 'MOPIDY_CHANGE_TRACK':
|
||||
return Object.assign({}, mopidy, {
|
||||
tlid: action.tlid
|
||||
tlid: action.tlid
|
||||
});
|
||||
|
||||
case 'MOPIDY_HIGHLIGHT_CURRENT_TLTRACK':
|
||||
for( var i = 0; i < mopidy.tracks.length; i++ ){
|
||||
if( mopidy.tracks[i].tlid == action.data.tlid ){
|
||||
action.data.track.playing = true;
|
||||
Object.assign(mopidy.tracks[i], action.data);
|
||||
}else{
|
||||
mopidy.tracks[i].track.playing = false;
|
||||
}
|
||||
}
|
||||
return Object.assign({}, mopidy, { tracks: mopidy.tracks });
|
||||
|
||||
/**
|
||||
* Websocket-initiated actions
|
||||
**/
|
||||
|
||||
@ -74,6 +74,49 @@ export function removeAuthorization(){
|
||||
return { type: 'SPOTIFY_REMOVE_AUTHORIZATION' }
|
||||
}
|
||||
|
||||
export function refreshToken(){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch({ type: 'SPOTIFY_TOKEN_REFRESHING' });
|
||||
|
||||
$.ajax({
|
||||
method: 'GET',
|
||||
url: '//jamesbarnsley.co.nz/spotmop.php?action=refresh&refresh_token='+getState().spotify.refresh_token,
|
||||
dataType: "json",
|
||||
timeout: 10000
|
||||
})
|
||||
.then( response => {
|
||||
|
||||
response.expires = new Date().getTime() + 3600000;
|
||||
|
||||
dispatch({
|
||||
type: 'SPOTIFY_TOKEN_REFRESHED',
|
||||
data: response
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get current user
|
||||
**/
|
||||
export function getMe(){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
// flush out the previous store value
|
||||
dispatch({ type: 'SPOTIFY_ME_LOADED', data: false });
|
||||
|
||||
sendRequest( getState().spotify.access_token, 'me' )
|
||||
.then( response => {
|
||||
dispatch({
|
||||
type: 'SPOTIFY_ME_LOADED',
|
||||
data: response
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a single artist
|
||||
@ -86,7 +129,6 @@ export function getArtist( uri ){
|
||||
// flush out the previous store value
|
||||
dispatch({ type: 'SPOTIFY_ARTIST_LOADED', data: false });
|
||||
|
||||
var id = uri.replace('spotify:artist:','');
|
||||
var artist = {};
|
||||
|
||||
// while we're fiddling about, go get the albums
|
||||
@ -95,12 +137,12 @@ export function getArtist( uri ){
|
||||
// get both the artist and the top tracks
|
||||
$.when(
|
||||
|
||||
sendRequest( false, 'artists/'+id )
|
||||
sendRequest( false, 'artists/'+ getFromUri('artistid', uri) )
|
||||
.then( response => {
|
||||
Object.assign(artist, response);
|
||||
}),
|
||||
|
||||
sendRequest( false, 'artists/'+id+'/top-tracks?country='+getState().spotify.country )
|
||||
sendRequest( false, 'artists/'+ getFromUri('artistid', uri) +'/top-tracks?country='+getState().spotify.country )
|
||||
.then( response => {
|
||||
Object.assign(artist, response);
|
||||
})
|
||||
@ -116,7 +158,7 @@ export function getArtist( uri ){
|
||||
|
||||
export function getArtistAlbums( uri ){
|
||||
return dispatch => {
|
||||
sendRequest( false, 'artists/'+ uri.replace('spotify:artist:','') +'/albums' )
|
||||
sendRequest( false, 'artists/'+ getFromUri('artistid', uri) +'/albums' )
|
||||
.then( response => {
|
||||
dispatch({
|
||||
type: 'SPOTIFY_ARTIST_ALBUMS_LOADED',
|
||||
@ -132,15 +174,15 @@ export function getArtistAlbums( uri ){
|
||||
* @oaram uri string
|
||||
**/
|
||||
export function getAlbum( uri ){
|
||||
return dispatch => {
|
||||
return dispatch => {
|
||||
|
||||
// flush out the previous store value
|
||||
// flush out the previous store value
|
||||
dispatch({ type: 'SPOTIFY_ALBUM_LOADED', data: false });
|
||||
|
||||
sendRequest( false, 'albums/'+ uri.replace('spotify:album:','') )
|
||||
sendRequest( false, 'albums/'+ getFromUri('albumid', uri) )
|
||||
.then( response => {
|
||||
|
||||
// inject the parent album object into each track for consistent track objects
|
||||
// inject the parent album object into each track for consistent track objects
|
||||
for( var i = 0; i < response.tracks.items.length; i++ ){
|
||||
response.tracks.items[i].album = {
|
||||
name: response.name,
|
||||
@ -149,7 +191,28 @@ export function getAlbum( uri ){
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'SPOTIFY_ALBUM_LOADED',
|
||||
type: 'SPOTIFY_ALBUM_LOADED',
|
||||
data: response
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Single playlist
|
||||
*
|
||||
* @oaram uri string
|
||||
**/
|
||||
export function getPlaylist( uri ){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
// flush out the previous store value
|
||||
dispatch({ type: 'SPOTIFY_PLAYLIST_LOADED', data: false });
|
||||
|
||||
sendRequest( getState().spotify.access_token, 'users/'+ getFromUri('userid',uri) +'/playlists/'+ getFromUri('playlistid',uri) +'?market='+getState().spotify.country )
|
||||
.then( response => {
|
||||
dispatch({
|
||||
type: 'SPOTIFY_PLAYLIST_LOADED',
|
||||
data: response
|
||||
});
|
||||
});
|
||||
@ -157,6 +220,22 @@ export function getAlbum( uri ){
|
||||
}
|
||||
|
||||
|
||||
export function getLibraryPlaylists(){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch({ type: 'SPOTIFY_LIBRARY_PLAYLISTS_LOADED', data: false });
|
||||
|
||||
sendRequest( getState().spotify.access_token, 'me/playlists' )
|
||||
.then( response => {
|
||||
dispatch({
|
||||
type: 'SPOTIFY_LIBRARY_PLAYLISTS_LOADED',
|
||||
data: response
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function getLibraryArtists(){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
@ -193,7 +272,7 @@ export function getLibraryTracks(){
|
||||
|
||||
dispatch({ type: 'SPOTIFY_LIBRARY_TRACKS_LOADED', data: false });
|
||||
|
||||
sendRequest( getState().spotify.access_token, 'me/tracks' )
|
||||
sendRequest( getState().spotify.access_token, 'me/tracks?limit=50' )
|
||||
.then( response => {
|
||||
dispatch({
|
||||
type: 'SPOTIFY_LIBRARY_TRACKS_LOADED',
|
||||
@ -202,3 +281,47 @@ export function getLibraryTracks(){
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get an element from a URI
|
||||
* @param element = string, the element we wish to extract
|
||||
* @param uri = string
|
||||
**/
|
||||
function getFromUri( element, uri ){
|
||||
var exploded = uri.split(':');
|
||||
if( element == 'userid' && exploded[1] == 'user' )
|
||||
return exploded[2];
|
||||
if( element == 'playlistid' && exploded[3] == 'playlist' )
|
||||
return exploded[4];
|
||||
if( element == 'artistid' && exploded[1] == 'artist' )
|
||||
return exploded[2];
|
||||
if( element == 'albumid' && exploded[1] == 'album' )
|
||||
return exploded[2];
|
||||
if( element == 'trackid' && exploded[1] == 'track' )
|
||||
return exploded[2];
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify what kind of asset a URI is (playlist, album, etc)
|
||||
* @param uri = string
|
||||
* @return string
|
||||
**/
|
||||
function uriType( uri ){
|
||||
var exploded = uri.split(':');
|
||||
if( exploded[0] == 'spotify' && exploded[1] == 'track' )
|
||||
return 'track';
|
||||
if( exploded[0] == 'spotify' && exploded[1] == 'artist' )
|
||||
return 'artist';
|
||||
if( exploded[0] == 'spotify' && exploded[1] == 'album' )
|
||||
return 'album';
|
||||
if( exploded[0] == 'spotify' && exploded[1] == 'user' && exploded[3] == 'playlist' )
|
||||
return 'playlist';
|
||||
if( exploded[0] == 'spotify' && exploded[1] == 'user' && exploded.length == 3 )
|
||||
return 'user';
|
||||
return null;
|
||||
}
|
||||
@ -3,54 +3,30 @@ import actions from './actions'
|
||||
|
||||
const SpotifyMiddleware = (function(){
|
||||
|
||||
/**
|
||||
* 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 = (token, endpoint, method = 'GET', data = false) => {
|
||||
|
||||
var options = {
|
||||
method: method,
|
||||
cache: true,
|
||||
url: 'https://api.spotify.com/v1/'+endpoint,
|
||||
data: data
|
||||
};
|
||||
|
||||
if( token ){
|
||||
options.headers = {
|
||||
Authorization: 'Bearer '+ token
|
||||
}
|
||||
}
|
||||
|
||||
return $.ajax( options );
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual middleware inteceptor
|
||||
**/
|
||||
return store => next => action => {
|
||||
|
||||
return next(action);
|
||||
/*
|
||||
var state = store.getState();
|
||||
|
||||
switch(action.type){
|
||||
|
||||
case 'SPOTIFY_AUTHORIZATION_GRANTED':
|
||||
sendRequest( action.data.access_token, 'me' )
|
||||
.then( (response) => {
|
||||
var data = action.data;
|
||||
data.me = response;
|
||||
store.dispatch({ type: 'SPOTIFY_AUTHORIZATION_COMPLETE', data: data })
|
||||
});
|
||||
case 'SPOTIFY_AUTHORIZATION_GRANTEDXXX':
|
||||
|
||||
// proceed as usual
|
||||
next(action)
|
||||
|
||||
// now we've been granted,
|
||||
store.dispatch({ type: 'SPOTIFY_AUTHORIZATION_COMPLETE', data: data })
|
||||
break;
|
||||
|
||||
// This action is irrelevant to us, pass it on to the next middleware
|
||||
default:
|
||||
return next(action);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@ -5,16 +5,12 @@ export default function reducer(spotify = {}, action){
|
||||
case 'SPOTIFY_CONNECTING':
|
||||
return Object.assign({}, spotify, { connected: false, connecting: true });
|
||||
|
||||
case 'SPOTIFY_START_AUTHORIZATION':
|
||||
return Object.assign({}, spotify, { authorizing: true });
|
||||
|
||||
case 'SPOTIFY_AUTHORIZATION_COMPLETE':
|
||||
case 'SPOTIFY_AUTHORIZATION_GRANTED':
|
||||
return Object.assign({}, spotify, {
|
||||
authorizing: false,
|
||||
authorized: true,
|
||||
authorization: action.data,
|
||||
me: action.data.me,
|
||||
token: action.data.access_token
|
||||
access_token: action.data.access_token
|
||||
});
|
||||
|
||||
case 'SPOTIFY_REMOVE_AUTHORIZATION':
|
||||
@ -22,16 +18,32 @@ export default function reducer(spotify = {}, action){
|
||||
authorizing: false,
|
||||
authorization: false,
|
||||
authorized: false,
|
||||
token: false,
|
||||
access_token: false,
|
||||
me: false
|
||||
});
|
||||
|
||||
case 'SPOTIFY_CONNECTED':
|
||||
return Object.assign({}, spotify, { connected: true, connecting: false });
|
||||
|
||||
case 'SPOTIFY_TOKEN_REFRESHING':
|
||||
return Object.assign({}, spotify, { refreshing_token: true });
|
||||
|
||||
case 'SPOTIFY_TOKEN_REFRESHED':
|
||||
return Object.assign({}, spotify, {
|
||||
refreshing_token: false,
|
||||
authorization: action.data,
|
||||
access_token: action.data.access_token
|
||||
});
|
||||
|
||||
case 'SPOTIFY_DISCONNECTED':
|
||||
return Object.assign({}, spotify, { connected: false, connecting: false });
|
||||
|
||||
case 'SPOTIFY_ME_LOADED':
|
||||
return Object.assign({}, spotify, { me: action.data });
|
||||
|
||||
case 'SPOTIFY_PLAYLIST_LOADED':
|
||||
return Object.assign({}, spotify, { playlist: action.data });
|
||||
|
||||
case 'SPOTIFY_ALBUM_LOADED':
|
||||
return Object.assign({}, spotify, { album: action.data });
|
||||
|
||||
@ -41,12 +53,18 @@ export default function reducer(spotify = {}, action){
|
||||
case 'SPOTIFY_ARTIST_ALBUMS_LOADED':
|
||||
return Object.assign({}, spotify, { artist_albums: action.data });
|
||||
|
||||
case 'SPOTIFY_LIBRARY_PLAYLISTS_LOADED':
|
||||
return Object.assign({}, spotify, { libraryPlaylists: action.data });
|
||||
|
||||
case 'SPOTIFY_LIBRARY_ARTISTS_LOADED':
|
||||
return Object.assign({}, spotify, { libraryArtists: action.data });
|
||||
|
||||
case 'SPOTIFY_LIBRARY_ALBUMS_LOADED':
|
||||
return Object.assign({}, spotify, { libraryAlbums: action.data });
|
||||
|
||||
case 'SPOTIFY_LIBRARY_TRACKS_LOADED':
|
||||
return Object.assign({}, spotify, { libraryTracks: action.data });
|
||||
|
||||
default:
|
||||
return spotify
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ class App extends React.Component{
|
||||
<li><Link to="/queue">Now playing</Link></li>
|
||||
<li><Link to="/library/artists">Library: My artists</Link></li>
|
||||
<li><Link to="/library/albums">Library: My albums</Link></li>
|
||||
<li><Link to="/library/tracks">Library: My tracks</Link></li>
|
||||
<li><Link to="/library/playlists">Library: My playlists</Link></li>
|
||||
<li><Link to="/settings">Settings</Link></li>
|
||||
</ul>
|
||||
|
||||
59
src/js/views/Playlist.js
Executable file
59
src/js/views/Playlist.js
Executable file
@ -0,0 +1,59 @@
|
||||
|
||||
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'
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
|
||||
class Playlist extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
// on render
|
||||
componentDidMount(){
|
||||
this.props.spotifyActions.getPlaylist( this.props.params.uri );
|
||||
}
|
||||
|
||||
// when props changed
|
||||
componentWillReceiveProps( nextProps ){
|
||||
if( nextProps.params.uri != this.props.params.uri ){
|
||||
this.props.spotifyActions.getPlaylist( nextProps.params.uri );
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
if( this.props.spotify.playlist ){
|
||||
return (
|
||||
<div>
|
||||
<h3>{ this.props.spotify.playlist.name }</h3>
|
||||
<TrackList tracks={this.props.spotify.playlist.tracks.items} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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)(Playlist)
|
||||
@ -4,10 +4,10 @@ import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import AlbumGrid from '../components/AlbumGrid'
|
||||
import AlbumGrid from '../../components/AlbumGrid'
|
||||
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
import * as spotifyActions from '../services/spotify/actions'
|
||||
import * as mopidyActions from '../../services/mopidy/actions'
|
||||
import * as spotifyActions from '../../services/spotify/actions'
|
||||
|
||||
class LibraryAlbums extends React.Component{
|
||||
|
||||
@ -4,8 +4,8 @@ import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
import * as spotifyActions from '../services/spotify/actions'
|
||||
import * as mopidyActions from '../../services/mopidy/actions'
|
||||
import * as spotifyActions from '../../services/spotify/actions'
|
||||
|
||||
class LibraryArtists extends React.Component{
|
||||
|
||||
59
src/js/views/library/LibraryPlaylists.js
Executable file
59
src/js/views/library/LibraryPlaylists.js
Executable file
@ -0,0 +1,59 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import * as mopidyActions from '../../services/mopidy/actions'
|
||||
import * as spotifyActions from '../../services/spotify/actions'
|
||||
|
||||
class LibraryPlaylists extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
// on render
|
||||
componentDidMount(){
|
||||
this.props.spotifyActions.getLibraryPlaylists();
|
||||
}
|
||||
|
||||
render(){
|
||||
if( this.props.spotify.libraryPlaylists ){
|
||||
return (
|
||||
<div>
|
||||
<h3>My playlists</h3>
|
||||
<ul>
|
||||
{
|
||||
this.props.spotify.libraryPlaylists.items.map( (playlist, index) => {
|
||||
var link = '/playlist/' + playlist.uri;
|
||||
return <li key={index}><Link to={link}>{ playlist.name }</Link></li>
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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)(LibraryPlaylists)
|
||||
@ -4,10 +4,10 @@ import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import TrackList from '../components/TrackList'
|
||||
import TrackList from '../../components/TrackList'
|
||||
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
import * as spotifyActions from '../services/spotify/actions'
|
||||
import * as mopidyActions from '../../services/mopidy/actions'
|
||||
import * as spotifyActions from '../../services/spotify/actions'
|
||||
|
||||
class LibraryTracks extends React.Component{
|
||||
|
||||
@ -21,11 +21,11 @@ class LibraryTracks extends React.Component{
|
||||
}
|
||||
|
||||
render(){
|
||||
if( this.props.spotify.libraryAlbums ){
|
||||
if( this.props.spotify.libraryTracks ){
|
||||
return (
|
||||
<div>
|
||||
<h3>My tracks</h3>
|
||||
<TrackList tracks={this.props.spotify.libraryTracks.tracks} />
|
||||
<TrackList tracks={this.props.spotify.libraryTracks.items} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
|
||||
@import 'vendor/font-awesome/index';
|
||||
|
||||
@import 'global/core';
|
||||
@import 'global/reset';
|
||||
@import 'global/forms';
|
||||
|
||||
|
||||
@ -8,13 +8,28 @@
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
.name,
|
||||
.artists,
|
||||
.album,
|
||||
.duration {
|
||||
width: 20%;
|
||||
position: relative;
|
||||
padding: 8px 10px 8px 30px;
|
||||
border-top: 1px solid #EEEEEE;
|
||||
border-bottom: 1px solid #EEEEEE;
|
||||
margin-bottom: -1px;
|
||||
|
||||
.col {
|
||||
width: 10%;
|
||||
min-height: 1px;
|
||||
float: left;
|
||||
|
||||
&.name,
|
||||
&.artists,
|
||||
&.album {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
.select-state {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@ -22,4 +37,17 @@
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.playing {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.selected,
|
||||
&.selected:hover {
|
||||
background: #FFF39C;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
background: #F4F4F4;
|
||||
}
|
||||
}
|
||||
14
src/scss/global/_core.scss
Executable file
14
src/scss/global/_core.scss
Executable file
@ -0,0 +1,14 @@
|
||||
|
||||
|
||||
body,
|
||||
html {
|
||||
background: #FFFFFF;
|
||||
color: #000000;
|
||||
font-family: 'Roboto Condensed', Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
Reference in New Issue
Block a user