Playlist list... not sure if cleanest way to build components

This commit is contained in:
James Barnsley
2016-10-21 23:27:50 +13:00
parent 667efba1f7
commit 5b7f4f5831
13 changed files with 259 additions and 80 deletions

15
src/js/components/ItemSource.js Executable file
View File

@ -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 <span>{ uriElements[0] }</span>;
}
}

26
src/js/components/ListItem.js Executable file
View File

@ -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 (
<li className={className} onClick={ (e) => this.handleClick(e) }>
{ this.props.children }
</li>
);
}
}

View File

@ -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 (
<Link to={ '/user/'+this.props.item.owner.uri }>
{ this.props.item.owner.id }
</Link>
);
}
render(){
return (
<ListItem extraClasses="playlist" link={ '/playlist/'+this.props.item.uri }>
<span className="col name">
{this.props.item.name}
</span>
<span className="col owner">
{ this.renderOwner() }
</span>
<span className="col source">
<ItemSource uri={this.props.item.uri} />
</span>
</ListItem>
);
}
}

View File

@ -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';

View File

@ -105,6 +105,12 @@ class TrackList extends React.Component{
return (
<div>
<ul>
<li className="list-item header track">
<span className="col name">Name</span>
<span className="col artists">Artists</span>
<span className="col album">Album</span>
<span className="col duration">Duration</span>
</li>
{
this.state.tracks.map(
(track, index) => {

View File

@ -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(
<Provider store={store}>
<Router history={hashHistory}>
<Router history={browserHistory}>
<Route path="/" component={App}>
<Route path="library/artists" component={LibraryArtists} />

View File

@ -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' }
}

View File

@ -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);

View File

@ -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
**/

View File

@ -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 (
<ul>
<li className="list-item header playlist">
<span className="col name">Name</span>
<span className="col owner">Owner</span>
<span className="col source">Source</span>
</li>
{
this.compiledPlaylistSources().map( (playlist, index) => {
return (
<PlaylistListItem key={index} item={playlist} />
);
})
}
</ul>
);
}
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;
return (
<div>
<h3>My playlists</h3>
{ this.renderPlaylists() }
</div>
)
}
}

View File

@ -5,5 +5,6 @@
@import 'global/reset';
@import 'global/forms';
@import 'components/lists';
@import 'components/track';

81
src/scss/components/_lists.scss Executable file
View File

@ -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%;
}
}
}
}

View File

@ -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;
}
}