diff --git a/src/js/components/ItemSource.js b/src/js/components/ItemSource.js
new file mode 100755
index 00000000..f35c6763
--- /dev/null
+++ b/src/js/components/ItemSource.js
@@ -0,0 +1,15 @@
+
+import React, { PropTypes } from 'react'
+
+export default class extends React.Component{
+
+ constructor(props) {
+ super(props);
+ }
+
+ render(){
+ var uriElements = this.props.uri.split(':');
+ if( uriElements.length <= 0 ) return false;
+ return { uriElements[0] };
+ }
+}
\ No newline at end of file
diff --git a/src/js/components/ListItem.js b/src/js/components/ListItem.js
new file mode 100755
index 00000000..92b9d4bf
--- /dev/null
+++ b/src/js/components/ListItem.js
@@ -0,0 +1,26 @@
+
+import React, { PropTypes } from 'react'
+import FontAwesome from 'react-fontawesome'
+import { browserHistory } from 'react-router'
+
+export default class ListItem extends React.Component{
+
+ constructor(props) {
+ super(props);
+ }
+
+ handleClick(e){
+ browserHistory.push( this.props.link );
+ }
+
+ render(){
+ var className = 'list-item';
+ if( this.props.extraClasses ) className += ' '+this.props.extraClasses;
+
+ return (
+
this.handleClick(e) }>
+ { this.props.children }
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/js/components/PlaylistListItem.js b/src/js/components/PlaylistListItem.js
new file mode 100755
index 00000000..1484cd05
--- /dev/null
+++ b/src/js/components/PlaylistListItem.js
@@ -0,0 +1,39 @@
+
+import React, { PropTypes } from 'react'
+import FontAwesome from 'react-fontawesome'
+import { Link } from 'react-router'
+
+import ListItem from './ListItem'
+import ItemSource from './ItemSource'
+
+export default class PlaylistListItem extends React.Component{
+
+ constructor(props) {
+ super(props);
+ }
+
+ renderOwner(){
+ if( !this.props.item.owner ) return null;
+ return (
+
+ { this.props.item.owner.id }
+
+ );
+ }
+
+ render(){
+ return (
+
+
+ {this.props.item.name}
+
+
+ { this.renderOwner() }
+
+
+
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/js/components/Track.js b/src/js/components/Track.js
index c6e97007..375b1b1e 100755
--- a/src/js/components/Track.js
+++ b/src/js/components/Track.js
@@ -39,7 +39,7 @@ export default class Track extends React.Component{
render(){
- var className = 'track';
+ var className = 'list-item track';
var selectedIcon = 'square-o';
if( typeof(this.props.track.selected) !== 'undefined' && this.props.track.selected ){
selectedIcon = 'check-square-o';
diff --git a/src/js/components/TrackList.js b/src/js/components/TrackList.js
index 2f78158a..e81528c0 100755
--- a/src/js/components/TrackList.js
+++ b/src/js/components/TrackList.js
@@ -105,6 +105,12 @@ class TrackList extends React.Component{
return (
+ -
+ Name
+ Artists
+ Album
+ Duration
+
{
this.state.tracks.map(
(track, index) => {
diff --git a/src/js/index.js b/src/js/index.js
index 336043b2..fc8139fa 100755
--- a/src/js/index.js
+++ b/src/js/index.js
@@ -6,7 +6,7 @@ import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import { createStore } from 'redux'
-import { Router, Route, Link, hashHistory } from 'react-router'
+import { Router, Route, Link, browserHistory } from 'react-router'
import store from './bootstrap.js'
@@ -26,7 +26,7 @@ import LibraryPlaylists from './views/library/LibraryPlaylists'
ReactDOM.render(
-
+
diff --git a/src/js/services/mopidy/actions.js b/src/js/services/mopidy/actions.js
index f8c88de5..170f4b54 100755
--- a/src/js/services/mopidy/actions.js
+++ b/src/js/services/mopidy/actions.js
@@ -22,6 +22,19 @@ export function disconnect(){
}
}
+export function instruct( call, value ){
+ return {
+ type: 'MOPIDY_INSTRUCT',
+ call: call,
+ value: value
+ }
+}
+
+
+/**
+ * Playback-oriented actions
+ **/
+
export function changeTrack( tlid ){
return {
type: 'MOPIDY_INSTRUCT',
@@ -56,14 +69,6 @@ export function removeTracks( tlids ){
}
}
-export function instruct( call, value ){
- return {
- type: 'MOPIDY_INSTRUCT',
- call: call,
- value: value
- }
-}
-
export function play(){
return instruct('playback.play');
}
@@ -79,3 +84,14 @@ export function next(){
export function previous(){
return instruct('playback.previous');
}
+
+
+
+/**
+ * Asset-oriented actions
+ **/
+
+export function getPlaylists(){
+ return { type: 'MOPIDY_PLAYLISTS' }
+}
+
diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js
index fec128a2..9892eeb5 100755
--- a/src/js/services/mopidy/middleware.js
+++ b/src/js/services/mopidy/middleware.js
@@ -154,6 +154,13 @@ const MopidyMiddleware = (function(){
})
break;
+ case 'MOPIDY_PLAYLISTS':
+ instruct( socket, store, 'playlists.asList' )
+ .then( response => {
+ store.dispatch({ type: 'MOPIDY_PLAYLISTS_LOADED', data: response });
+ })
+ break;
+
// This action is irrelevant to us, pass it on to the next middleware
default:
return next(action);
diff --git a/src/js/services/mopidy/reducer.js b/src/js/services/mopidy/reducer.js
index f44660c3..0b2e537d 100755
--- a/src/js/services/mopidy/reducer.js
+++ b/src/js/services/mopidy/reducer.js
@@ -31,6 +31,11 @@ export default function reducer(mopidy = {}, action){
}
return Object.assign({}, mopidy, { tracks: mopidy.tracks });
+ case 'MOPIDY_PLAYLISTS_LOADED':
+ if( !action.data ) return mopidy;
+ return Object.assign({}, mopidy, { playlists: action.data });
+
+
/**
* Websocket-initiated actions
**/
diff --git a/src/js/views/library/LibraryPlaylists.js b/src/js/views/library/LibraryPlaylists.js
index 41890ddb..2ad9ced0 100755
--- a/src/js/views/library/LibraryPlaylists.js
+++ b/src/js/views/library/LibraryPlaylists.js
@@ -4,6 +4,8 @@ import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { Link } from 'react-router'
+import PlaylistListItem from '../../components/PlaylistListItem'
+
import * as mopidyActions from '../../services/mopidy/actions'
import * as spotifyActions from '../../services/spotify/actions'
@@ -15,26 +17,60 @@ class LibraryPlaylists extends React.Component{
// on render
componentDidMount(){
+ if( this.props.mopidy.connected ) this.loadPlaylists();
+ }
+
+ componentWillReceiveProps( nextProps ){
+ if( !this.props.mopidy.connected && nextProps.mopidy.connected ) this.loadPlaylists();
+ }
+
+ loadPlaylists(){
this.props.spotifyActions.getLibraryPlaylists();
+ this.props.mopidyActions.getPlaylists();
+ }
+
+ compiledPlaylistSources(){
+ var playlists = [];
+
+ if( this.props.mopidy.playlists ){
+ Object.assign(playlists, this.props.mopidy.playlists)
+ }
+
+ if( this.props.spotify.libraryPlaylists ){
+ Object.assign(playlists, this.props.spotify.libraryPlaylists.items)
+ }
+
+ return playlists;
+ }
+
+ renderPlaylists(){
+ if( !this.compiledPlaylistSources() ) return null;
+
+ return (
+
+ -
+ Name
+ Owner
+ Source
+
+ {
+ this.compiledPlaylistSources().map( (playlist, index) => {
+ return (
+
+ );
+ })
+ }
+
+ );
}
render(){
- if( this.props.spotify.libraryPlaylists ){
- return (
-
-
My playlists
-
- {
- this.props.spotify.libraryPlaylists.items.map( (playlist, index) => {
- var link = '/playlist/' + playlist.uri;
- return - { playlist.name }
- })
- }
-
-
- );
- }
- return null;
+ return (
+
+
My playlists
+ { this.renderPlaylists() }
+
+ )
}
}
diff --git a/src/scss/app.scss b/src/scss/app.scss
index 7a480bca..bc125d4f 100755
--- a/src/scss/app.scss
+++ b/src/scss/app.scss
@@ -5,5 +5,6 @@
@import 'global/reset';
@import 'global/forms';
+@import 'components/lists';
@import 'components/track';
diff --git a/src/scss/components/_lists.scss b/src/scss/components/_lists.scss
new file mode 100755
index 00000000..2b733e1c
--- /dev/null
+++ b/src/scss/components/_lists.scss
@@ -0,0 +1,81 @@
+
+.list-item {
+
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+
+ display: block;
+ 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;
+ }
+
+ &::after {
+ content: '';
+ clear: both;
+ display: block;
+ }
+
+ &.selected {
+ background: #FFF39C !important;
+ }
+
+ &:not(.header):hover {
+ background: #F4F4F4;
+ cursor: pointer;
+ }
+
+ &.header {
+ font-size: 10px;
+ color: #CCCCCC;
+ text-transform: uppercase;
+ border-top: 0;
+ }
+
+
+ &.track {
+
+ .col {
+ &.name,
+ &.artists,
+ &.album {
+ width: 30%;
+ }
+ }
+
+ .select-state {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ }
+
+ &.playing {
+ font-weight: bold;
+ }
+ }
+
+
+ &.playlist {
+
+ .col {
+ &.name {
+ width: 30%;
+ }
+ &.owner {
+ width: 20%;
+ }
+ }
+ }
+}
+
diff --git a/src/scss/components/_track.scss b/src/scss/components/_track.scss
index 3b05b48b..e69de29b 100755
--- a/src/scss/components/_track.scss
+++ b/src/scss/components/_track.scss
@@ -1,53 +0,0 @@
-
-.track {
-
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-
- 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 {
- content: '';
- clear: both;
- display: block;
- }
-
- &.playing {
- font-weight: bold;
- }
-
- &.selected,
- &.selected:hover {
- background: #FFF39C;
- }
-
- &:hover{
- background: #F4F4F4;
- }
-}
\ No newline at end of file