Slider; Queue
This commit is contained in:
BIN
src/assets/favicon.ico
Executable file
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
BIN
src/assets/favicon.psd
Executable file
Binary file not shown.
@ -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" />
|
||||
|
||||
@ -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>
|
||||
{ this.renderPlayButton() }
|
||||
<a onClick={() => this.props.mopidyActions.next()}>
|
||||
<FontAwesome name="step-forward" />
|
||||
</a>
|
||||
{ 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>
|
||||
{ this.renderPlayButton() }
|
||||
<a onClick={() => this.props.mopidyActions.stop()}>
|
||||
<FontAwesome name="stop" />
|
||||
</a>
|
||||
<a onClick={() => this.props.mopidyActions.next()}>
|
||||
<FontAwesome name="step-forward" />
|
||||
</a>
|
||||
</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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
93
src/js/components/ProgressSlider.js
Executable file
93
src/js/components/ProgressSlider.js
Executable 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)
|
||||
@ -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 )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 ) )
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
51
src/scss/components/_player.scss
Executable file
51
src/scss/components/_player.scss
Executable 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); */
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,7 +106,15 @@ main {
|
||||
&.w100 { width: 100%; }
|
||||
}
|
||||
|
||||
.cf{
|
||||
.pull-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.cf {
|
||||
@include clearfix;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -59,6 +59,10 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.name {
|
||||
opacity: 1;
|
||||
|
||||
35
src/scss/views/_queue.scss
Executable file
35
src/scss/views/_queue.scss
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user