Slider; Queue

This commit is contained in:
James Barnsley
2016-11-03 18:22:26 +13:00
parent 8f5e61a8f9
commit 6cee0a9daf
26 changed files with 799 additions and 299 deletions

BIN
src/assets/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/favicon.psd Executable file

Binary file not shown.

View File

@ -2,7 +2,16 @@
<head>
<title>Iris</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<meta name="mobile-web-app-capable" content="yes" >
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#222222" />
<link rel="shortcut icon" type="image/ico" href="/assets/favicon.ico" />
<link rel="shortcut icon" type="image/x-icon" href="/assets/favicon.ico" />
<link rel="shortcut-icon" href="/assets/favicon.ico" />
<link href="https://fonts.googleapis.com/css?family=Archivo+Narrow:300,400,700" rel="stylesheet">
<link href="/app.css" rel="stylesheet" />

View File

@ -2,9 +2,10 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { Link } from 'react-router'
import FontAwesome from 'react-fontawesome'
import VolumeSlider from './VolumeSlider'
import ProgressSlider from './ProgressSlider'
import ArtistSentence from './ArtistSentence'
import Thumbnail from './Thumbnail'
@ -16,19 +17,6 @@ class Player extends React.Component{
super(props);
}
renderTrack(){
if( this.props.mopidy && this.props.mopidy.current_tltrack ){
return (
<div className="track-in-focus">
{ this.props.spotify.track && !this.props.mini ? <Thumbnail size="large" images={this.props.spotify.track.album.images} /> : null }
<div className="title">{ this.props.mopidy.current_tltrack.track.name }</div>
<ArtistSentence artists={ this.props.mopidy.current_tltrack.track.artists } />
</div>
);
}
return null;
}
renderPlayButton(){
var button = <a onClick={() => this.props.mopidyActions.play()}><FontAwesome name="play" /> </a>
if( this.props.mopidy.state == 'playing' ){
@ -38,7 +26,6 @@ class Player extends React.Component{
}
renderConsumeButton(){
if( this.props.mini ) return null;
var button = <a onClick={() => this.props.mopidyActions.instruct('tracklist.setConsume', [true])}><FontAwesome name="fire" /></a>
if( this.props.mopidy.consume ){
button = <a className="active" onClick={() => this.props.mopidyActions.instruct('tracklist.setConsume', [false])}><FontAwesome name="fire" /></a>
@ -47,7 +34,6 @@ class Player extends React.Component{
}
renderRandomButton(){
if( this.props.mini ) return null;
var button = <a onClick={() => this.props.mopidyActions.instruct('tracklist.setRandom', [true])}><FontAwesome name="random" /></a>
if( this.props.mopidy.random ){
button = <a className="active" onClick={() => this.props.mopidyActions.instruct('tracklist.setRandom', [false])}><FontAwesome name="random" /></a>
@ -56,7 +42,6 @@ class Player extends React.Component{
}
renderRepeatButton(){
if( this.props.mini ) return null;
var button = <a onClick={() => this.props.mopidyActions.instruct('tracklist.setRepeat', [true])}><FontAwesome name="repeat" /></a>
if( this.props.mopidy.repeat ){
button = <a className="active" onClick={() => this.props.mopidyActions.instruct('tracklist.setRepeat', [false])}><FontAwesome name="repeat" /></a>
@ -64,30 +49,76 @@ class Player extends React.Component{
return button;
}
render(){
renderMiniPlayer(){
var mopidy_track = false;
if( typeof(this.props.mopidy.current_tltrack) !== 'undefined' && typeof(this.props.mopidy.current_tltrack.track) !== 'undefined' ) mopidy_track = this.props.mopidy.current_tltrack;
return (
<div className="player">
{ this.renderTrack() }
<div className="current-track">
<div className="title">{ mopidy_track ? mopidy_track.track.name : null }</div>
{ mopidy_track ? <ArtistSentence artists={ mopidy_track.track.artists } /> : null }
</div>
<div className="controls">
{ this.renderPlayButton() }
<a onClick={() => this.props.mopidyActions.previous()}>
<FontAwesome name="step-backward" />
</a>&nbsp;
{ this.renderPlayButton() }
<a onClick={() => this.props.mopidyActions.next()}>
<FontAwesome name="step-forward" />
</a>&nbsp;
{ this.renderConsumeButton() }
{ this.renderRandomButton() }
{ this.renderRepeatButton() }
<VolumeSlider
volume={ this.props.mopidy.volume }
onChange={(volume) => this.props.mopidyActions.instruct('playback.setVolume', { volume: volume })} />
<ProgressSlider />
</div>
</div>
);
}
renderFullPlayer(){
var mopidy_track = false;
if( typeof(this.props.mopidy.current_tltrack) !== 'undefined' && typeof(this.props.mopidy.current_tltrack.track) !== 'undefined' ) mopidy_track = this.props.mopidy.current_tltrack;
return (
<div className="player">
{ this.props.spotify.track && !this.props.mini ? <Link className="artwork" to={'/album/'+this.props.spotify.track.album.uri}><Thumbnail size="huge" images={this.props.spotify.track.album.images} /></Link> : null }
<div className="controls cf">
<div className="pull-left">
<a onClick={() => this.props.mopidyActions.previous()}>
<FontAwesome name="step-backward" />
</a>&nbsp;
{ this.renderPlayButton() }
<a onClick={() => this.props.mopidyActions.stop()}>
<FontAwesome name="stop" />
</a>&nbsp;
<a onClick={() => this.props.mopidyActions.next()}>
<FontAwesome name="step-forward" />
</a>&nbsp;
</div>
<div className="pull-right">
{ this.renderConsumeButton() }
{ this.renderRandomButton() }
{ this.renderRepeatButton() }
</div>
</div>
<div className="current-track">
<div className="title">{ mopidy_track ? mopidy_track.track.name : null }</div>
{ mopidy_track ? <ArtistSentence artists={ mopidy_track.track.artists } /> : null }
</div>
</div>
);
}
render(){
if( this.props.mini ){
return this.renderMiniPlayer()
}else{
return this.renderFullPlayer()
}
}
}

View File

@ -0,0 +1,93 @@
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'
class ProgressSlider extends React.Component{
constructor(props) {
super(props);
this.state = {
animating: false
}
}
componentDidMount(){
if( this.props.mopidy.connected && this.props.mopidy.state == 'playing' ){
this.setState({ animating: true })
}
setInterval(() => {
this.updateProgress()
}, 10000);
}
componentWillReceiveProps( nextProps ){
if( this.state.animating && nextProps.mopidy.state != 'playing' ){
this.setState({ animating: false })
console.log('stop animating') // doesn't work?
}
}
updateProgress(){
if( this.props.mopidy.connected && this.props.mopidy.state == 'playing' ){
this.setState({ animating: true })
this.props.mopidyActions.getTimePosition()
}
}
handleClick(e){
var slider = e.target;
if( slider.className != 'slider' ) slider = slider.parentElement;
var sliderX = e.clientX - slider.getBoundingClientRect().left;
var sliderWidth = slider.getBoundingClientRect().width;
var percent = ( sliderX / sliderWidth ).toFixed(2);
if( this.props.mopidy.connected && typeof(this.props.mopidy.current_tltrack) !== 'undefined' && typeof(this.props.mopidy.current_tltrack.track) !== 'undefined' ){
var destination_time = this.props.mopidy.current_tltrack.track.length * percent
this.props.mopidyActions.seek( destination_time )
this.setState({ animating: false })
}
}
render(){
var className = 'slider'
if( this.state.animating ) className += ' animating'
var percent = 0
if( this.props.mopidy.connected && typeof(this.props.mopidy.current_tltrack) !== 'undefined' && typeof(this.props.mopidy.current_tltrack.track) !== 'undefined' ){
percent = this.props.mopidy.time_position / this.props.mopidy.current_tltrack.track.length
percent = percent * 100;
if( percent > 100 ) percent = 100
}
return (
<div className={className} onClick={ (e) => this.handleClick(e) } >
<div className="progress" style={{ width: (percent)+'%' }}></div>
</div>
);
}
}
/**
* 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)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ProgressSlider)

View File

@ -24,7 +24,7 @@ export default class VolumeSlider extends React.Component{
var percent = parseInt( sliderX / sliderWidth * 100 );
if( this.props.volume != percent ){
this.props.onChange( percent )
this.props.onChange( volume )
}
}

View File

@ -77,6 +77,10 @@ export function pause(){
return instruct('playback.pause');
}
export function stop(){
return instruct('playback.stop');
}
export function next(){
return instruct('playback.next');
}
@ -85,6 +89,21 @@ export function previous(){
return instruct('playback.previous');
}
export function seek( time_position ){
return {
type: 'MOPIDY_INSTRUCT',
call: 'playback.seek',
value: { time_position: time_position }
}
}
export function getTimePosition(){
return {
type: 'MOPIDY_INSTRUCT',
call: 'playback.getTimePosition'
}
}
/**

View File

@ -20,6 +20,7 @@ const MopidyMiddleware = (function(){
instruct( ws, store, 'tracklist.getRepeat' );
instruct( ws, store, 'tracklist.getTlTracks' );
instruct( ws, store, 'playback.getCurrentTlTrack' );
instruct( ws, store, 'playback.getTimePosition' );
break;
case 'state:offline':
@ -34,7 +35,14 @@ const MopidyMiddleware = (function(){
instruct( ws, store, 'playback.getState' );
break;
//case 'event:trackPlaybackEnded':
case 'event:seeked':
store.dispatch({ type: 'MOPIDY_TIMEPOSITION', data: data.time_position });
break;
case 'event:trackPlaybackEnded':
instruct( ws, store, 'playback.getTimePosition' );
break;
case 'event:trackPlaybackStarted':
instruct( ws, store, 'playback.getCurrentTlTrack' );
break;

View File

@ -72,6 +72,11 @@ export default function reducer(mopidy = {}, action){
volume: action.data
});
case 'MOPIDY_TIMEPOSITION':
return Object.assign({}, mopidy, {
time_position: action.data
});
case 'MOPIDY_DIRECTORY_LOADED':
return Object.assign({}, mopidy, {
directory: action.data

View File

@ -12,14 +12,14 @@ const SpotifyMiddleware = (function(){
switch(action.type){
// when our mopidy server current track changes
case 'MOPIDY_CURRENTTLTRACK_XX':
case 'MOPIDY_CURRENTTLTRACK':
// DISABLED AS IT CAUSES ISSUE
// proceed as usual so we don't inhibit default functionality
next(action)
// if the current track is a spotify track
if( action.data.track.uri.substring(0,14) == 'spotify:track:' ){
if( action.data && action.data.track.uri.substring(0,14) == 'spotify:track:' ){
store.dispatch( actions.getTrack( action.data.track.uri ) )
}

View File

@ -11,6 +11,8 @@ import ArtistSentence from '../components/ArtistSentence'
import AlbumLink from '../components/AlbumLink'
import Header from '../components/Header'
import * as uiActions from '../services/ui/actions'
import * as spotifyActions from '../services/spotify/actions'
import * as mopidyActions from '../services/mopidy/actions'
class Queue extends React.Component{
@ -19,19 +21,6 @@ class Queue extends React.Component{
super(props);
}
renderTrackInFocus(){
if( this.props.mopidy && this.props.mopidy.trackInFocus ){
return (
<div>
<div>{ this.props.mopidy.trackInFocus.track.name }</div>
<div><ArtistSentence artists={ this.props.mopidy.trackInFocus.track.artists } /></div>
<div><AlbumLink album={ this.props.mopidy.trackInFocus.track.album } /></div>
</div>
);
}
return null;
}
renderTrackList(){
if( this.props.mopidy && this.props.mopidy.tracks ){
return (
@ -66,11 +55,7 @@ class Queue extends React.Component{
render(){
return (
<div className="view queue-view">
<Header
icon="play"
title="Now playing"
/>
{ this.renderTrackInFocus() }
<Player />
{ this.renderTrackList() }
</div>
);
@ -90,6 +75,8 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = (dispatch) => {
return {
uiActions: bindActionCreators(uiActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch)
}
}

View File

@ -12,6 +12,7 @@
@import 'components/images';
@import 'components/icon';
@import 'components/search-form';
@import 'components/player';
@import 'components/sidebar';
@import 'components/lists';
@import 'components/grid';
@ -20,4 +21,5 @@
@import 'views/artist';
@import 'views/album';
@import 'views/playlist';
@import 'views/queue';

View File

@ -0,0 +1,51 @@
.player {
.current-track {
font-size: 16px;
text-transform: uppercase;
text-align: center;
.title {
@include one_line_text;
}
.artist-sentence {
opacity: 0.5;
display: block;
@include one_line_text;
.artist {
color: inherit;
text-decoration: none;
border-bottom: 1px dotted transparent;
}
}
}
.controls {
text-align: center;
a {
cursor: pointer;
font-size: 14px;
padding: 8px;
display: inline-block;
opacity: 0.8;
&.active {
color: $turquoise;
}
&:hover {
opacity: 1.0;
}
}
}
.slider.animating {
.progress{
/* @include animate(10s); */
}
}
}

View File

@ -83,52 +83,11 @@ aside{
left: 0;
right: 0;
padding: 14px;
color: #FFFFFF;
.track-in-focus {
font-size: 16px;
text-transform: uppercase;
text-align: center;
color: #FFFFFF;
.title {
@include one_line_text;
}
.artist-sentence {
opacity: 0.5;
display: block;
@include one_line_text;
.artist {
color: inherit;
text-decoration: none;
border-bottom: 1px dotted transparent;
&:hover {
border-color: #FFFFFF !important;
}
}
}
}
.controls {
text-align: center;
a {
color: #FFFFFF;
cursor: pointer;
font-size: 14px;
padding: 8px;
display: inline-block;
opacity: 0.8;
&.active {
color: $turquoise;
}
&:hover {
opacity: 1.0;
}
.artist-sentence {
.artist:hover {
border-color: #FFFFFF !important;
}
}
}

View File

@ -106,7 +106,15 @@ main {
&.w100 { width: 100%; }
}
.cf{
.pull-left {
float: left;
}
.pull-right {
float: right;
}
.cf {
@include clearfix;
}

View File

@ -49,7 +49,7 @@
.name {
@include animate();
position: absolute;
top: 35%;
top: 40%;
left: 0;
right: 0;
text-align: center;
@ -59,6 +59,10 @@
font-weight: bold;
}
.secondary {
display: none;
}
&:hover {
.name {
opacity: 1;

View File

@ -59,6 +59,10 @@
font-weight: bold;
}
.secondary {
display: none;
}
&:hover {
.name {
opacity: 1;

35
src/scss/views/_queue.scss Executable file
View File

@ -0,0 +1,35 @@
.queue-view {
.player {
text-align: center;
padding: 40px 0;
.artwork,
.controls a {
border: 0;
}
.artwork,
.controls {
display: block;
max-width: 400px;
margin: 0 auto;
}
.controls {
padding: 20px 0;
a {
font-size: 18px;
}
}
.thumbnail {
max-width: none;
}
.current-track {
font-weight: 700;
}
}
}