Playlist and album views

This commit is contained in:
James Barnsley
2016-10-31 16:49:42 +13:00
parent 869d73231b
commit bab6c66852
20 changed files with 581 additions and 143 deletions

View File

@ -2919,18 +2919,29 @@ main h1 {
font-size: 40px; font-size: 40px;
} }
main h2 {
font-weight: 500;
font-size: 30px;
}
main h3 { main h3 {
font-weight: 100;
font-style: italic;
font-size: 20px;
}
main h4 {
font-size: 14px; font-size: 14px;
margin-bottom: 14px; margin-bottom: 14px;
text-transform: uppercase; text-transform: uppercase;
} }
main h3.underline { main h4.underline {
padding-bottom: 4px; padding-bottom: 4px;
border-bottom: 1px solid #000000; border-bottom: 1px solid #000000;
} }
main h3:not(:first-child) { main h4:not(:first-child) {
padding-top: 40px; padding-top: 40px;
} }
@ -3665,7 +3676,7 @@ main header h1 {
left: 0; left: 0;
bottom: 0; bottom: 0;
width: 300px; width: 300px;
background: #DDDDDD; background: #EEEEEE;
} }
.album-view .intro .thumbnail { .album-view .intro .thumbnail {
@ -3673,12 +3684,132 @@ main header h1 {
margin: 0 auto; margin: 0 auto;
} }
.album-view .intro h1 { .album-view .intro .details {
padding-top: 30px; padding: 30px;
color: #AAAAAA;
text-transform: uppercase;
}
.album-view .intro .artist-grid {
padding: 0;
margin: 0;
}
.album-view .intro .artist-grid .grid-item {
margin: 0;
width: 50%;
position: relative;
}
.album-view .intro .artist-grid .grid-item .thumbnail .image {
background-color: #888888;
}
.album-view .intro .artist-grid .grid-item .name {
-webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
position: absolute;
top: 35%;
left: 0;
right: 0;
text-align: center;
color: #FFFFFF;
opacity: 0;
z-index: 2;
font-weight: bold;
}
.album-view .intro .artist-grid .grid-item:hover .name {
opacity: 1;
} }
.album-view .main { .album-view .main {
padding-left: 300px; padding-left: 300px;
} }
.album-view .main .title {
padding: 30px 40px;
}
.album-view .main .title h3 {
color: #AAAAAA;
}
.album-view .main .title h3 a:hover {
color: #777777;
}
.playlist-view .intro {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 300px;
background: #EEEEEE;
}
.playlist-view .intro .thumbnail {
float: none;
margin: 0 auto;
}
.playlist-view .intro .details {
padding: 30px;
color: #AAAAAA;
text-transform: uppercase;
}
.playlist-view .intro .artist-grid {
padding: 0;
margin: 0;
}
.playlist-view .intro .artist-grid .grid-item {
margin: 0;
width: 50%;
position: relative;
}
.playlist-view .intro .artist-grid .grid-item .thumbnail .image {
background-color: #888888;
}
.playlist-view .intro .artist-grid .grid-item .name {
-webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
position: absolute;
top: 35%;
left: 0;
right: 0;
text-align: center;
color: #FFFFFF;
opacity: 0;
z-index: 2;
font-weight: bold;
}
.playlist-view .intro .artist-grid .grid-item:hover .name {
opacity: 1;
}
.playlist-view .main {
padding-left: 300px;
}
.playlist-view .main .title {
padding: 30px 40px;
}
.playlist-view .main .title h3 {
color: #AAAAAA;
}
.playlist-view .main .title h3 a:hover {
color: #777777;
}
/*# sourceMappingURL=app.css.map*/ /*# sourceMappingURL=app.css.map*/

File diff suppressed because one or more lines are too long

View File

@ -5958,6 +5958,13 @@
_reactRouter.browserHistory.push(this.props.link); _reactRouter.browserHistory.push(this.props.link);
} }
} }
}, {
key: 'renderThumbnail',
value: function renderThumbnail() {
if (this.props.item.images) return _react2.default.createElement(_Thumbnail2.default, { size: 'medium', images: this.props.item.images });
if (this.props.item.icons) return _react2.default.createElement(_Thumbnail2.default, { size: 'medium', images: this.props.item.icons });
return _react2.default.createElement(_Thumbnail2.default, { size: 'medium', images: [] });
}
}, { }, {
key: 'render', key: 'render',
value: function render() { value: function render() {
@ -5973,8 +5980,7 @@
{ className: 'grid-item', onClick: function onClick(e) { { className: 'grid-item', onClick: function onClick(e) {
return _this2.handleClick(e); return _this2.handleClick(e);
} }, } },
item.images ? _react2.default.createElement(_Thumbnail2.default, { size: 'medium', images: item.images }) : null, this.renderThumbnail(),
item.icons ? _react2.default.createElement(_Thumbnail2.default, { size: 'medium', images: item.icons }) : null,
_react2.default.createElement( _react2.default.createElement(
'div', 'div',
{ className: 'name' }, { className: 'name' },
@ -7514,9 +7520,15 @@
'div', 'div',
{ className: className }, { className: className },
this.props.albums.map(function (album, index) { this.props.albums.map(function (album, index) {
var link = album.uri;
if (album.album) link = album.album.uri; // handle nested album objects (as in Album Library)
return _react2.default.createElement(_GridItem2.default, { item: album, key: index, link: '/album/' + link }); if (album.album) {
var flatAlbum = album.album;
flatAlbum.added_at = album.added_at;
album = flatAlbum;
}
return _react2.default.createElement(_GridItem2.default, { item: album, key: index, link: '/album/' + album.uri });
}) })
); );
} }
@ -10581,7 +10593,7 @@
/* 95 */ /* 95 */
/***/ function(module, exports, __webpack_require__) { /***/ function(module, exports, __webpack_require__) {
var __WEBPACK_AMD_DEFINE_RESULT__;var require;/* WEBPACK VAR INJECTION */(function(process) {/** @license MIT License (c) copyright 2010-2014 original author or authors */ var require;var __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(process) {/** @license MIT License (c) copyright 2010-2014 original author or authors */
/** @author Brian Cavalier */ /** @author Brian Cavalier */
/** @author John Hann */ /** @author John Hann */
@ -11288,6 +11300,10 @@
var _AlbumLink2 = _interopRequireDefault(_AlbumLink); var _AlbumLink2 = _interopRequireDefault(_AlbumLink);
var _Dater = __webpack_require__(379);
var _Dater2 = _interopRequireDefault(_Dater);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
@ -11329,36 +11345,16 @@
// notify our tracklist // notify our tracklist
this.props.handleContextMenu(e); this.props.handleContextMenu(e);
} }
}, {
key: 'formatDuration',
value: function formatDuration() {
if (typeof this.props.track.duration_ms !== 'undefined') {
var ms = this.props.track.duration_ms;
} else if (typeof this.props.track.length !== 'undefined') {
var ms = this.props.track.length;
} else {
return null;
}
var time = new Date(ms);
var min = time.getMinutes();
var sec = time.getSeconds();
if (sec < 10) sec = '0' + sec;
return min + ':' + sec;
}
}, { }, {
key: 'render', key: 'render',
value: function render() { value: function render() {
var _this2 = this; var _this2 = this;
var className = 'list-item track'; var track = this.props.track;
if (typeof this.props.track.selected !== 'undefined' && this.props.track.selected) {
className += ' selected';
}
if (this.props.track.playing) { var className = 'list-item track';
className += ' playing'; if (typeof track.selected !== 'undefined' && track.selected) className += ' selected';
} if (track.playing) className += ' playing';
return _react2.default.createElement( return _react2.default.createElement(
'div', 'div',
@ -11377,22 +11373,23 @@
_react2.default.createElement( _react2.default.createElement(
'span', 'span',
{ className: 'col name' }, { className: 'col name' },
this.props.track.name track.name
), ),
_react2.default.createElement( _react2.default.createElement(
'span', 'span',
{ className: 'col artists' }, { className: 'col artists' },
this.props.track.artists ? _react2.default.createElement(_ArtistSentence2.default, { artists: this.props.track.artists }) : null track.artists ? _react2.default.createElement(_ArtistSentence2.default, { artists: track.artists }) : null
), ),
_react2.default.createElement( _react2.default.createElement(
'span', 'span',
{ className: 'col album' }, { className: 'col album' },
this.props.track.album ? _react2.default.createElement(_AlbumLink2.default, { album: this.props.track.album }) : null track.album ? _react2.default.createElement(_AlbumLink2.default, { album: track.album }) : null
), ),
_react2.default.createElement( _react2.default.createElement(
'span', 'span',
{ className: 'col duration' }, { className: 'col duration' },
this.formatDuration() track.duration_ms ? _react2.default.createElement(_Dater2.default, { type: 'length', data: track.duration_ms }) : null,
track.length ? _react2.default.createElement(_Dater2.default, { type: 'length', data: track.length }) : null
) )
); );
} }
@ -19117,6 +19114,8 @@
}); });
case 'MOPIDY_CURRENTTLTRACK': case 'MOPIDY_CURRENTTLTRACK':
if (!action.data) return mopidy;
var tracks = []; var tracks = [];
Object.assign(tracks, mopidy.tracks); Object.assign(tracks, mopidy.tracks);
@ -19566,6 +19565,14 @@
var _ArtistSentence2 = _interopRequireDefault(_ArtistSentence); var _ArtistSentence2 = _interopRequireDefault(_ArtistSentence);
var _ArtistGrid = __webpack_require__(155);
var _ArtistGrid2 = _interopRequireDefault(_ArtistGrid);
var _Dater = __webpack_require__(379);
var _Dater2 = _interopRequireDefault(_Dater);
var _actions = __webpack_require__(11); var _actions = __webpack_require__(11);
var spotifyActions = _interopRequireWildcard(_actions); var spotifyActions = _interopRequireWildcard(_actions);
@ -19611,32 +19618,66 @@
this.props.spotifyActions.getAlbum(nextProps.params.uri); this.props.spotifyActions.getAlbum(nextProps.params.uri);
} }
} }
}, {
key: 'totalTime',
value: function totalTime() {
if (!this.props.spotify.album) return null;
var tracks = this.props.spotify.tracks.items;
var duration = 0;
for (var i = 0; i < tracks.length; i++) {
duration += tracks[i].duration_ms;
}
return duration;
}
}, { }, {
key: 'render', key: 'render',
value: function render() { value: function render() {
if (this.props.spotify.album) { if (this.props.spotify.album) {
var album = this.props.spotify.album;
return _react2.default.createElement( return _react2.default.createElement(
'div', 'div',
{ className: 'view album-view' }, { className: 'view album-view' },
_react2.default.createElement( _react2.default.createElement(
'div', 'div',
{ className: 'intro' }, { className: 'intro' },
_react2.default.createElement(_Thumbnail2.default, { size: 'large', images: this.props.spotify.album.images }) _react2.default.createElement(_Thumbnail2.default, { size: 'large', images: album.images }),
_react2.default.createElement(_ArtistGrid2.default, { artists: album.artists }),
_react2.default.createElement(
'div',
{ className: 'details' },
_react2.default.createElement(
'div',
null,
album.tracks.total,
' tracks, ',
this.totalTime
),
_react2.default.createElement(
'div',
null,
'Released ',
_react2.default.createElement(_Dater2.default, { type: 'date', data: album.release_date })
)
)
), ),
_react2.default.createElement( _react2.default.createElement(
'div', 'div',
{ className: 'main' }, { className: 'main' },
_react2.default.createElement( _react2.default.createElement(
'h1', 'div',
null, { className: 'title' },
this.props.spotify.album.name _react2.default.createElement(
'h1',
null,
album.name
),
_react2.default.createElement(
'h3',
null,
_react2.default.createElement(_ArtistSentence2.default, { artists: album.artists })
)
), ),
_react2.default.createElement( _react2.default.createElement(_TrackList2.default, { tracks: album.tracks.items })
'h3',
null,
_react2.default.createElement(_ArtistSentence2.default, { artists: this.props.spotify.album.artists })
),
_react2.default.createElement(_TrackList2.default, { tracks: this.props.spotify.album.tracks.items })
) )
); );
} }
@ -19805,12 +19846,6 @@
null, null,
this.props.children this.props.children
), ),
_react2.default.createElement(
'footer',
null,
'Iris by James Barnsley \xA0|\xA0 v',
this.props.pusher.version.current
),
this.props.ui.context_menu.test, this.props.ui.context_menu.test,
_react2.default.createElement(_ContextMenu2.default, { state: this.props.ui.context_menu }) _react2.default.createElement(_ContextMenu2.default, { state: this.props.ui.context_menu })
); );
@ -19954,7 +19989,7 @@
'div', 'div',
{ className: 'col w70' }, { className: 'col w70' },
_react2.default.createElement( _react2.default.createElement(
'h3', 'h4',
{ className: 'left-padding' }, { className: 'left-padding' },
'Top tracks' 'Top tracks'
), ),
@ -19965,7 +20000,7 @@
'div', 'div',
{ className: 'col w25' }, { className: 'col w25' },
_react2.default.createElement( _react2.default.createElement(
'h3', 'h4',
null, null,
'Related artists' 'Related artists'
), ),
@ -19973,7 +20008,7 @@
), ),
_react2.default.createElement('div', { className: 'cf' }), _react2.default.createElement('div', { className: 'cf' }),
_react2.default.createElement( _react2.default.createElement(
'h3', 'h4',
{ className: 'left-padding' }, { className: 'left-padding' },
'Albums' 'Albums'
), ),
@ -20029,9 +20064,13 @@
var _TrackList2 = _interopRequireDefault(_TrackList); var _TrackList2 = _interopRequireDefault(_TrackList);
var _Header = __webpack_require__(18); var _Thumbnail = __webpack_require__(31);
var _Header2 = _interopRequireDefault(_Header); var _Thumbnail2 = _interopRequireDefault(_Thumbnail);
var _Dater = __webpack_require__(379);
var _Dater2 = _interopRequireDefault(_Dater);
var _actions = __webpack_require__(11); var _actions = __webpack_require__(11);
@ -20083,14 +20122,42 @@
value: function render() { value: function render() {
if (this.props.spotify.playlist) { if (this.props.spotify.playlist) {
var playlist = this.props.spotify.playlist;
var context = null; var context = null;
if (this.props.spotify.playlist.owner.id == this.props.spotify.me.id) context = 'editable-playlist'; if (playlist.owner.id == this.props.spotify.me.id) context = 'editable-playlist';
return _react2.default.createElement( return _react2.default.createElement(
'div', 'div',
{ className: 'view playlist-view' }, { className: 'view playlist-view' },
_react2.default.createElement(_Header2.default, { icon: 'playlist', title: this.props.spotify.playlist.name }), _react2.default.createElement(
_react2.default.createElement(_TrackList2.default, { context: context, tracks: this.props.spotify.playlist.tracks.items }) 'div',
{ className: 'intro' },
_react2.default.createElement(_Thumbnail2.default, { size: 'large', images: playlist.images }),
_react2.default.createElement(
'div',
{ className: 'details' },
_react2.default.createElement(
'div',
null,
playlist.tracks.total,
' tracks'
)
)
),
_react2.default.createElement(
'div',
{ className: 'main' },
_react2.default.createElement(
'div',
{ className: 'title' },
_react2.default.createElement(
'h1',
null,
playlist.name
)
),
_react2.default.createElement(_TrackList2.default, { context: context, tracks: playlist.tracks.items })
)
); );
} }
return null; return null;
@ -20426,7 +20493,7 @@
'section', 'section',
null, null,
_react2.default.createElement( _react2.default.createElement(
'h3', 'h4',
{ className: 'underline' }, { className: 'underline' },
'Mopidy' 'Mopidy'
), ),
@ -20480,7 +20547,7 @@
) )
), ),
_react2.default.createElement( _react2.default.createElement(
'h3', 'h4',
{ className: 'underline' }, { className: 'underline' },
'Pusher' 'Pusher'
), ),
@ -20533,7 +20600,7 @@
) )
), ),
_react2.default.createElement( _react2.default.createElement(
'h3', 'h4',
{ className: 'underline' }, { className: 'underline' },
'Spotify' 'Spotify'
), ),
@ -20589,7 +20656,7 @@
), ),
_react2.default.createElement(_SpotifyAuthenticationFrame2.default, null), _react2.default.createElement(_SpotifyAuthenticationFrame2.default, null),
_react2.default.createElement( _react2.default.createElement(
'h3', 'h4',
{ className: 'underline' }, { className: 'underline' },
'Advanced' 'Advanced'
), ),
@ -48675,6 +48742,85 @@
module.exports = isPlainObject; module.exports = isPlainObject;
/***/ },
/* 371 */,
/* 372 */,
/* 373 */,
/* 374 */,
/* 375 */,
/* 376 */,
/* 377 */,
/* 378 */,
/* 379 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _react = __webpack_require__(2);
var _react2 = _interopRequireDefault(_react);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var Dater = function (_React$Component) {
_inherits(Dater, _React$Component);
function Dater(props) {
_classCallCheck(this, Dater);
return _possibleConstructorReturn(this, (Dater.__proto__ || Object.getPrototypeOf(Dater)).call(this, props));
}
_createClass(Dater, [{
key: 'calculate',
value: function calculate() {
switch (this.props.type) {
case 'length':
var time = new Date(this.props.data);
var min = time.getMinutes();
var sec = time.getSeconds();
if (sec < 10) sec = '0' + sec;
return min + ':' + sec;
break;
case 'date':
// TODO: nice date formatting
return this.props.data;
break;
default:
return null;
}
}
}, {
key: 'render',
value: function render() {
return _react2.default.createElement(
'span',
{ className: 'dater' },
this.calculate()
);
}
}]);
return Dater;
}(_react2.default.Component);
exports.default = Dater;
/***/ } /***/ }
/******/ ]))); /******/ ])));
//# sourceMappingURL=app.js.map //# sourceMappingURL=app.js.map

File diff suppressed because one or more lines are too long

View File

@ -17,9 +17,15 @@ export default class AlbumGrid extends React.Component{
{ {
this.props.albums.map( this.props.albums.map(
(album, index) => { (album, index) => {
var link = album.uri;
if( album.album ) link = album.album.uri; // handle nested album objects (as in Album Library)
return <GridItem item={album} key={index} link={'/album/'+link} /> if( album.album ){
var flatAlbum = album.album;
flatAlbum.added_at = album.added_at;
album = flatAlbum;
}
return <GridItem item={album} key={index} link={'/album/'+album.uri} />
} }
) )
} }

View File

@ -28,7 +28,7 @@ class Artist extends React.Component{
if( this.props.spotify.artist ){ if( this.props.spotify.artist ){
return ( return (
<div> <div>
<h3>{ this.props.spotify.artist.name }</h3> <h4>{ this.props.spotify.artist.name }</h4>
</div> </div>
); );
} }

34
src/js/components/Dater.js Executable file
View File

@ -0,0 +1,34 @@
import React, { PropTypes } from 'react'
export default class Dater extends React.Component{
constructor(props) {
super(props);
}
calculate(){
switch(this.props.type){
case 'length':
var time = new Date( this.props.data );
var min = time.getMinutes();
var sec = time.getSeconds();
if( sec < 10 ) sec = '0'+sec;
return min+':'+sec;
break
case 'date':
// TODO: nice date formatting
return this.props.data
break
default:
return null
}
}
render(){
return <span className="dater">{ this.calculate() }</span>
}
}

View File

@ -17,6 +17,12 @@ export default class GridItem extends React.Component{
} }
} }
renderThumbnail(){
if( this.props.item.images ) return <Thumbnail size="medium" images={this.props.item.images} />
if( this.props.item.icons ) return <Thumbnail size="medium" images={this.props.item.icons} />
return <Thumbnail size="medium" images={[]} />
}
render(){ render(){
var item = this.props.item; var item = this.props.item;
if( typeof(item.album) !== 'undefined' ){ if( typeof(item.album) !== 'undefined' ){
@ -25,8 +31,7 @@ export default class GridItem extends React.Component{
} }
return ( return (
<div className="grid-item" onClick={ (e) => this.handleClick(e) }> <div className="grid-item" onClick={ (e) => this.handleClick(e) }>
{ item.images ? <Thumbnail size="medium" images={item.images} /> : null } { this.renderThumbnail() }
{ item.icons ? <Thumbnail size="medium" images={item.icons} /> : null }
<div className="name">{ item.name }</div> <div className="name">{ item.name }</div>
<div className="secondary"> <div className="secondary">
{ item.artists ? <ArtistSentence artists={ item.artists } /> : null } { item.artists ? <ArtistSentence artists={ item.artists } /> : null }

View File

@ -3,6 +3,7 @@ import React, { PropTypes } from 'react'
import FontAwesome from 'react-fontawesome' import FontAwesome from 'react-fontawesome'
import ArtistSentence from './ArtistSentence' import ArtistSentence from './ArtistSentence'
import AlbumLink from './AlbumLink' import AlbumLink from './AlbumLink'
import Dater from './Dater'
export default class Track extends React.Component{ export default class Track extends React.Component{
@ -31,32 +32,12 @@ export default class Track extends React.Component{
this.props.handleContextMenu(e); this.props.handleContextMenu(e);
} }
formatDuration(){
if( typeof(this.props.track.duration_ms) !== 'undefined' ){
var ms = this.props.track.duration_ms;
}else if( typeof(this.props.track.length) !== 'undefined' ){
var ms = this.props.track.length;
}else{
return null;
}
var time = new Date(ms);
var min = time.getMinutes();
var sec = time.getSeconds();
if( sec < 10 ) sec = '0'+sec;
return min+':'+sec;
}
render(){ render(){
var track = this.props.track;
var className = 'list-item track'; var className = 'list-item track';
if( typeof(this.props.track.selected) !== 'undefined' && this.props.track.selected ){ if( typeof(track.selected) !== 'undefined' && track.selected ) className += ' selected';
className += ' selected'; if( track.playing ) className += ' playing';
}
if( this.props.track.playing ){
className += ' playing';
}
return ( return (
<div <div
@ -66,16 +47,17 @@ export default class Track extends React.Component{
onContextMenu={ (e) => this.handleContextMenu(e) }> onContextMenu={ (e) => this.handleContextMenu(e) }>
{ this.props.track.selected ? <FontAwesome name="check" className="select-state" fixedWidth /> : null } { this.props.track.selected ? <FontAwesome name="check" className="select-state" fixedWidth /> : null }
<span className="col name"> <span className="col name">
{this.props.track.name} {track.name}
</span> </span>
<span className="col artists"> <span className="col artists">
{ this.props.track.artists ? <ArtistSentence artists={this.props.track.artists} /> : null } { track.artists ? <ArtistSentence artists={track.artists} /> : null }
</span> </span>
<span className="col album"> <span className="col album">
{ this.props.track.album ? <AlbumLink album={this.props.track.album} /> : null } { track.album ? <AlbumLink album={track.album} /> : null }
</span> </span>
<span className="col duration"> <span className="col duration">
{ this.formatDuration() } { track.duration_ms ? <Dater type="length" data={track.duration_ms} /> : null }
{ track.length ? <Dater type="length" data={track.length} /> : null }
</span> </span>
</div> </div>
); );

View File

@ -53,6 +53,8 @@ export default function reducer(mopidy = {}, action){
}); });
case 'MOPIDY_CURRENTTLTRACK': case 'MOPIDY_CURRENTTLTRACK':
if( !action.data ) return mopidy;
var tracks = []; var tracks = [];
Object.assign(tracks, mopidy.tracks); Object.assign(tracks, mopidy.tracks);

View File

@ -7,6 +7,8 @@ import TrackList from '../components/TrackList'
import Thumbnail from '../components/Thumbnail' import Thumbnail from '../components/Thumbnail'
import Parallax from '../components/Parallax' import Parallax from '../components/Parallax'
import ArtistSentence from '../components/ArtistSentence' import ArtistSentence from '../components/ArtistSentence'
import ArtistGrid from '../components/ArtistGrid'
import Dater from '../components/Dater'
import * as spotifyActions from '../services/spotify/actions' import * as spotifyActions from '../services/spotify/actions'
import * as mopidyActions from '../services/mopidy/actions' import * as mopidyActions from '../services/mopidy/actions'
@ -29,17 +31,35 @@ class Album extends React.Component{
} }
} }
totalTime(){
if( !this.props.spotify.album ) return null
var tracks = this.props.spotify.tracks.items;
var duration = 0;
for( var i = 0; i < tracks.length; i++ ){
duration += tracks[i].duration_ms;
}
return duration;
}
render(){ render(){
if( this.props.spotify.album ){ if( this.props.spotify.album ){
var album = this.props.spotify.album;
return ( return (
<div className="view album-view"> <div className="view album-view">
<div className="intro"> <div className="intro">
<Thumbnail size="large" images={ this.props.spotify.album.images } /> <Thumbnail size="large" images={ album.images } />
<ArtistGrid artists={ album.artists } />
<div className="details">
<div>{ album.tracks.total } tracks, { this.totalTime }</div>
<div>Released <Dater type="date" data={ album.release_date } /></div>
</div>
</div> </div>
<div className="main"> <div className="main">
<h1>{ this.props.spotify.album.name }</h1> <div className="title">
<h3><ArtistSentence artists={ this.props.spotify.album.artists } /></h3> <h1>{ album.name }</h1>
<TrackList tracks={this.props.spotify.album.tracks.items} /> <h3><ArtistSentence artists={ album.artists } /></h3>
</div>
<TrackList tracks={ album.tracks.items } />
</div> </div>
</div> </div>
); );

View File

@ -77,9 +77,6 @@ class App extends React.Component{
<main> <main>
{this.props.children} {this.props.children}
</main> </main>
<footer>
Iris by James Barnsley &nbsp;|&nbsp; v{ this.props.pusher.version.current }
</footer>
{ this.props.ui.context_menu.test } { this.props.ui.context_menu.test }
<ContextMenu state={this.props.ui.context_menu} /> <ContextMenu state={this.props.ui.context_menu} />
</div> </div>

View File

@ -43,20 +43,20 @@ class Artist extends React.Component{
</div> </div>
<div className="col w70"> <div className="col w70">
<h3 className="left-padding">Top tracks</h3> <h4 className="left-padding">Top tracks</h4>
{ this.props.spotify.artist.tracks ? <TrackList tracks={ this.props.spotify.artist.tracks } /> : null } { this.props.spotify.artist.tracks ? <TrackList tracks={ this.props.spotify.artist.tracks } /> : null }
</div> </div>
<div className="col w5"></div> <div className="col w5"></div>
<div className="col w25"> <div className="col w25">
<h3>Related artists</h3> <h4>Related artists</h4>
{ this.props.spotify.artist.related_artists ? <ArtistList artists={ this.props.spotify.artist.related_artists.slice(0,6) } /> : null } { this.props.spotify.artist.related_artists ? <ArtistList artists={ this.props.spotify.artist.related_artists.slice(0,6) } /> : null }
</div> </div>
<div className="cf"></div> <div className="cf"></div>
<h3 className="left-padding">Albums</h3> <h4 className="left-padding">Albums</h4>
{ this.props.spotify.artist_albums ? <AlbumGrid className="no-top-padding" albums={ this.props.spotify.artist_albums.items } /> : null } { this.props.spotify.artist_albums ? <AlbumGrid className="no-top-padding" albums={ this.props.spotify.artist_albums.items } /> : null }
</div> </div>
); );

View File

@ -4,7 +4,8 @@ import { connect } from 'react-redux'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import TrackList from '../components/TrackList' import TrackList from '../components/TrackList'
import Header from '../components/Header' import Thumbnail from '../components/Thumbnail'
import Dater from '../components/Dater'
import * as spotifyActions from '../services/spotify/actions' import * as spotifyActions from '../services/spotify/actions'
import * as mopidyActions from '../services/mopidy/actions' import * as mopidyActions from '../services/mopidy/actions'
@ -30,13 +31,24 @@ class Playlist extends React.Component{
render(){ render(){
if( this.props.spotify.playlist ){ if( this.props.spotify.playlist ){
var playlist = this.props.spotify.playlist;
var context = null; var context = null;
if( this.props.spotify.playlist.owner.id == this.props.spotify.me.id ) context = 'editable-playlist' if( playlist.owner.id == this.props.spotify.me.id ) context = 'editable-playlist'
return ( return (
<div className="view playlist-view"> <div className="view playlist-view">
<Header icon="playlist" title={ this.props.spotify.playlist.name } /> <div className="intro">
<TrackList context={context} tracks={this.props.spotify.playlist.tracks.items} /> <Thumbnail size="large" images={ playlist.images } />
<div className="details">
<div>{ playlist.tracks.total } tracks</div>
</div>
</div>
<div className="main">
<div className="title">
<h1>{ playlist.name }</h1>
</div>
<TrackList context={context} tracks={ playlist.tracks.items } />
</div>
</div> </div>
); );
} }

View File

@ -65,7 +65,7 @@ class Settings extends React.Component{
<section> <section>
<h3 className="underline">Mopidy</h3> <h4 className="underline">Mopidy</h4>
<form onSubmit={(e) => this.setMopidyConfig(e)}> <form onSubmit={(e) => this.setMopidyConfig(e)}>
<label> <label>
<div className="label">Host</div> <div className="label">Host</div>
@ -88,7 +88,7 @@ class Settings extends React.Component{
<button type="submit" className="secondary">Apply</button> <button type="submit" className="secondary">Apply</button>
</form> </form>
<h3 className="underline">Pusher</h3> <h4 className="underline">Pusher</h4>
<form> <form>
<label> <label>
<div className="label">Username</div> <div className="label">Username</div>
@ -112,7 +112,7 @@ class Settings extends React.Component{
</label> </label>
</form> </form>
<h3 className="underline">Spotify</h3> <h4 className="underline">Spotify</h4>
<form onSubmit={(e) => this.setSpotifyConfig(e)}> <form onSubmit={(e) => this.setSpotifyConfig(e)}>
<label> <label>
<div className="label">Country</div> <div className="label">Country</div>
@ -138,7 +138,7 @@ class Settings extends React.Component{
<SpotifyAuthenticationFrame /> <SpotifyAuthenticationFrame />
<h3 className="underline">Advanced</h3> <h4 className="underline">Advanced</h4>
<label> <label>
<div className="label">Version</div> <div className="label">Version</div>
<div className="input"> <div className="input">

View File

@ -17,4 +17,5 @@
@import 'views/artist'; @import 'views/artist';
@import 'views/album'; @import 'views/album';
@import 'views/playlist';

View File

@ -41,7 +41,18 @@ main {
font-size: 40px; font-size: 40px;
} }
h2 {
font-weight: 500;
font-size: 30px;
}
h3 { h3 {
font-weight: 100;
font-style: italic;
font-size: 20px;
}
h4 {
font-size: 14px; font-size: 14px;
margin-bottom: 14px; margin-bottom: 14px;
text-transform: uppercase; text-transform: uppercase;
@ -112,4 +123,4 @@ footer {
padding-top: 50px; padding-top: 50px;
padding-bottom: 50px; padding-bottom: 50px;
text-align: center; text-align: center;
} }

View File

@ -1,5 +1,6 @@
$turquoise: #08d58f; $turquoise: #08d58f;
$faint_grey: #EEEEEE;
$light_grey: #DDDDDD; $light_grey: #DDDDDD;
$mid_grey: #AAAAAA; $mid_grey: #AAAAAA;
$dark_grey: #333333; $dark_grey: #333333;

View File

@ -6,19 +6,67 @@
left: 0; left: 0;
bottom: 0; bottom: 0;
width: 300px; width: 300px;
background: $light_grey; background: $faint_grey;
.thumbnail { .thumbnail {
float: none; float: none;
margin: 0 auto; margin: 0 auto;
} }
h1 { .details {
padding-top: 30px; padding: 30px;
color: $mid_grey;
text-transform: uppercase;
}
.artist-grid {
padding: 0;
margin: 0;
.grid-item {
margin: 0;
width: 50%;
position: relative;
.thumbnail .image {
background-color: $secondary_grey;
}
.name {
@include animate();
position: absolute;
top: 35%;
left: 0;
right: 0;
text-align: center;
color: #FFFFFF;
opacity: 0;
z-index: 2;
font-weight: bold;
}
&:hover {
.name {
opacity: 1;
}
}
}
} }
} }
.main { .main {
.title {
padding: 30px 40px;
h3 {
color: $mid_grey;
a:hover {
color: darken($mid_grey, 20%);
}
}
}
padding-left: 300px; padding-left: 300px;
} }
} }

View File

@ -1,30 +1,72 @@
.album-view { .playlist-view {
.intro { .intro {
@include clearfix; position: absolute;
position: relative; top: 0;
text-align: center; left: 0;
color: #FFFFFF; bottom: 0;
padding: 20px; width: 300px;
height: 40vh; background: $faint_grey;
z-index: 2;
.parallax {
position: absolute;
top: 0;
left: 0;
height: 100%;
z-index: -1;
}
.thumbnail { .thumbnail {
float: none; float: none;
margin: 0 auto; margin: 0 auto;
max-width: 160px;
} }
h1 { .details {
padding-top: 30px; padding: 30px;
color: $mid_grey;
text-transform: uppercase;
}
.artist-grid {
padding: 0;
margin: 0;
.grid-item {
margin: 0;
width: 50%;
position: relative;
.thumbnail .image {
background-color: $secondary_grey;
}
.name {
@include animate();
position: absolute;
top: 35%;
left: 0;
right: 0;
text-align: center;
color: #FFFFFF;
opacity: 0;
z-index: 2;
font-weight: bold;
}
&:hover {
.name {
opacity: 1;
}
}
}
} }
} }
.main {
.title {
padding: 30px 40px;
h3 {
color: $mid_grey;
a:hover {
color: darken($mid_grey, 20%);
}
}
}
padding-left: 300px;
}
} }