Files
Iris/src/js/components/PlaybackControls.js

304 lines
11 KiB
JavaScript
Raw Normal View History

import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Link from './Link';
import ProgressSlider from './Fields/ProgressSlider'
import VolumeControl from './Fields/VolumeControl'
import MuteControl from './Fields/MuteControl'
import OutputControl from './Fields/OutputControl'
import Dater from './Dater'
import ArtistSentence from './ArtistSentence'
import Thumbnail from './Thumbnail'
import Icon from './Icon'
import * as helpers from '../helpers'
import * as uiActions from '../services/ui/actions'
import * as coreActions from '../services/core/actions'
import * as mopidyActions from '../services/mopidy/actions'
class PlaybackControls extends React.Component{
2017-10-10 13:05:28 +13:00
constructor(props){
2017-06-22 08:59:40 +12:00
super(props)
this.stream = null;
2017-06-22 08:59:40 +12:00
this.state = {
2019-01-06 14:12:57 +13:00
expanded: false,
current_track_transition: null
2017-06-22 08:59:40 +12:00
}
}
componentDidMount(){
if (this.props.http_streaming_enabled){
2018-07-02 21:37:24 +12:00
// Bust our cache, and by consequence, play our stream
this.props.coreActions.cachebustHttpStream();
}
}
playStream(props = this.props){
if (!this.stream){
this.stream = new Audio();
} else {
this.stream.src = null;
}
2018-07-02 21:37:24 +12:00
if (!props.http_streaming_enabled || !props.http_streaming_url){
return false;
}
2018-07-02 21:37:24 +12:00
this.stream.src = props.http_streaming_url+"?cb="+props.http_streaming_cachebuster;
this.stream.muted = props.http_streaming_mute;
this.stream.volume = props.http_streaming_volume / 100;
this.stream.play();
2018-07-02 21:37:24 +12:00
console.log("Playing stream: "+this.stream.src);
}
componentWillReceiveProps(nextProps){
2018-07-02 21:37:24 +12:00
// Cachebuster changed
// This happens when playback changes, so that the stream is "new", rather
// than the original stream. This prevents the browser cache from starting
// the stream right from the beginning (which could be hours of continuous playback).
if (this.props.http_streaming_cachebuster !== nextProps.http_streaming_cachebuster){
this.playStream(nextProps);
}
2018-07-02 21:37:24 +12:00
// Just been enabled
if (!this.props.http_streaming_enabled && nextProps.http_streaming_enabled){
this.playStream(nextProps);
}
if (this.stream){
// Just been muted
if (this.props.http_streaming_mute !== nextProps.http_streaming_mute){
this.stream.muted = nextProps.http_streaming_mute;
}
// Just had volume changed
if (this.props.http_streaming_volume !== nextProps.http_streaming_volume){
this.stream.volume = nextProps.http_streaming_volume / 100;
}
// Just been disabled
2018-07-02 21:37:24 +12:00
if (!nextProps.http_streaming_enabled){
this.stream = null;
}
}
}
2018-12-10 16:04:40 +13:00
handleTouchStart(e){
var target = $(e.target);
var timestamp = Math.floor(Date.now());
// Save touch start details
this.start_time = timestamp;
this.start_position = {
x: e.touches[0].clientX
}
return false;
}
handleTouchEnd(e){
var target = $(e.target);
var timestamp = Math.floor(Date.now());
var tap_distance_threshold = 10; // Max distance (px) between touchstart and touchend to qualify as a tap
var tap_time_threshold = 200; // Max time (ms) between touchstart and touchend to qualify as a tap
var end_position = {
x: e.changedTouches[0].clientX
}
// Too long between touchstart and touchend
if (this.start_time + tap_time_threshold < timestamp){
return false;
}
// Make sure there's enough distance between start and end before we handle
// this event as a 'tap'
if (this.start_position.x + tap_distance_threshold > end_position.x &&
this.start_position.x - tap_distance_threshold < end_position.x){
this.props.history.push('/queue');
2018-12-10 16:04:40 +13:00
} else {
// Swipe to the left = previous track
if (this.start_position.x < end_position.x){
this.props.mopidyActions.previous();
2019-01-06 14:12:57 +13:00
this.currentTrackTransition('previous');
2018-12-10 16:04:40 +13:00
// Swipe to the right = skip track
} else if (this.start_position.x > end_position.x){
this.props.mopidyActions.next();
2019-01-06 14:12:57 +13:00
this.currentTrackTransition('next');
2018-12-10 16:04:40 +13:00
}
}
this.end_time = timestamp;
}
2019-01-06 14:12:57 +13:00
currentTrackTransition(transition){
this.setState({current_track_transition: transition});
var self = this;
setTimeout(() => {
self.setState({current_track_transition: null})
},
100
);
}
renderPlayButton(){
2018-05-15 21:30:57 +12:00
var button = <a className="control play" onClick={() => this.props.mopidyActions.play()}><Icon name="play_circle_filled" type="material" /></a>
2017-10-10 13:05:28 +13:00
if (this.props.play_state == 'playing'){
2018-05-15 21:30:57 +12:00
button = <a className="control play" onClick={() => this.props.mopidyActions.pause()}><Icon name="pause_circle_filled" type="material" /></a>
}
return button;
}
renderConsumeButton(){
2018-09-21 11:58:54 +12:00
var button = <a className="control tooltip" onClick={() => this.props.mopidyActions.setConsume(true)}><Icon name="restaurant" type="material" /><span className="tooltip__content">Consume</span></a>
2017-10-10 13:05:28 +13:00
if (this.props.consume){
button = <a className="control control--active tooltip" onClick={() => this.props.mopidyActions.setConsume(false)}><Icon name="restaurant" type="material" /><span className="tooltip__content">Consume</span></a>
}
return button;
}
renderRandomButton(){
2018-09-21 11:58:54 +12:00
var button = <a className="control tooltip" onClick={() => this.props.mopidyActions.setRandom(true)}><Icon name="shuffle" type="material" /><span className="tooltip__content">Shuffle</span></a>
2017-10-10 13:05:28 +13:00
if (this.props.random){
button = <a className="control control--active tooltip" onClick={() => this.props.mopidyActions.setRandom(false)}><Icon name="shuffle" type="material" /><span className="tooltip__content">Shuffle</span></a>
}
return button;
}
renderRepeatButton(){
2018-09-21 11:58:54 +12:00
var button = <a className="control tooltip" onClick={() => this.props.mopidyActions.setRepeat(true)}><Icon name="repeat" /><span className="tooltip__content">Repeat</span></a>
2017-10-10 13:05:28 +13:00
if (this.props.repeat){
button = <a className="control control--active tooltip" onClick={() => this.props.mopidyActions.setRepeat(false)}><Icon name="repeat" /><span className="tooltip__content">Repeat</span></a>
}
return button;
}
render(){
2017-07-15 23:49:17 +12:00
var images = false
if (this.props.current_track && this.props.current_track.images){
images = this.props.current_track.images
}
return (
<div className={(this.state.expanded ? "playback-controls--expanded playback-controls" : "playback-controls")}>
{this.props.next_track && this.props.next_track.images ? <Thumbnail className="hide" size="large" images={this.props.next_track.images} /> : null}
2018-12-10 16:04:40 +13:00
<div
2019-01-06 14:12:57 +13:00
className={"current-track"+(this.state.current_track_transition ? " current-track--transition current-track--transition-"+this.state.current_track_transition : "")}
2018-12-10 16:04:40 +13:00
onTouchStart={e => this.handleTouchStart(e)}
onTouchEnd={e => this.handleTouchEnd(e)}>
<Link className="thumbnail-wrapper" to={'/kiosk-mode'}>
2018-12-10 16:04:40 +13:00
<Thumbnail size="small" images={images} />
</Link>
<div className="title">
{ this.props.current_track ? this.props.current_track.name : <span>-</span> }
</div>
<div className="artist">
{this.props.current_track ? <ArtistSentence artists={ this.props.current_track.artists } nolinks={this.props.slim_mode} /> : <ArtistSentence />}
</div>
2017-07-26 15:24:18 +12:00
</div>
<section className="playback">
2017-06-22 08:59:40 +12:00
<a className="control previous" onClick={() => this.props.mopidyActions.previous()}>
2018-05-15 21:30:57 +12:00
<Icon name="skip_previous" type="material" />
</a>
{ this.renderPlayButton() }
2017-06-22 08:59:40 +12:00
<a className="control stop" onClick={() => this.props.mopidyActions.stop()}>
2018-05-15 21:30:57 +12:00
<Icon name="stop" type="material" />
</a>
2017-06-22 08:59:40 +12:00
<a className="control next" onClick={() => this.props.mopidyActions.next()}>
2018-05-15 21:30:57 +12:00
<Icon name="skip_next" type="material" />
</a>
</section>
<section className="settings">
{this.renderConsumeButton()}
{this.renderRandomButton()}
{this.renderRepeatButton()}
<OutputControl force_expanded={this.state.expanded} />
</section>
<section className="progress">
<ProgressSlider />
<span className="current">{ this.props.time_position ? <Dater type="length" data={this.props.time_position} /> : '-' }</span>
2018-09-06 20:58:08 +12:00
<span className="total">{ this.props.current_track ? <Dater type="length" data={this.props.current_track.duration} /> : '-' }</span>
</section>
<section className="volume">
<MuteControl
mute={this.props.mute}
onMuteChange={mute => this.props.mopidyActions.setMute(mute)}
/>
2018-03-23 16:54:56 +13:00
<VolumeControl
scrollWheel
2018-03-23 16:54:56 +13:00
volume={this.props.volume}
mute={this.props.mute}
onVolumeChange={percent => this.props.mopidyActions.setVolume(percent)}
/>
</section>
2017-06-22 08:59:40 +12:00
<section className="triggers">
2018-12-08 11:59:32 +13:00
<a className="control expanded-controls" onClick={e => this.setState({expanded: !this.state.expanded})}>
2018-05-16 16:50:46 +12:00
{this.state.expanded ? <Icon name="expand_more" type="material" /> : <Icon name="expand_less" type="material" />}
2017-06-22 08:59:40 +12:00
</a>
2018-12-08 11:59:32 +13:00
<a className={"control sidebar-toggle"+(this.props.sidebar_open ? ' open' : '')} onClick={e => this.props.uiActions.toggleSidebar()}>
2018-05-16 16:50:46 +12:00
<Icon className="open" name="menu" type="material" />
</a>
2017-06-22 08:59:40 +12:00
</section>
</div>
);
}
}
/**
* Export our component
*
* We also integrate our global store, using connect()
**/
const mapStateToProps = (state, ownProps) => {
return {
snapcast_enabled: state.pusher.config.snapcast_enabled,
2018-02-02 08:39:37 +13:00
http_streaming_enabled: state.core.http_streaming_enabled,
http_streaming_volume: (state.core.http_streaming_volume ? state.core.http_streaming_volume : 50),
http_streaming_mute: (state.core.http_streaming_mute ? state.core.http_streaming_mute : false),
http_streaming_url: (state.core.http_streaming_url ? state.core.http_streaming_url : null),
2018-07-02 21:37:24 +12:00
http_streaming_cachebuster: state.core.http_streaming_cachebuster,
current_track: (state.core.current_track && state.core.tracks[state.core.current_track.uri] !== undefined ? state.core.tracks[state.core.current_track.uri] : null),
next_track: (state.core.next_track_uri && state.core.tracks[state.core.next_track_uri] !== undefined ? state.core.tracks[state.core.next_track_uri] : null),
radio_enabled: (state.ui.radio && state.ui.radio.enabled ? true : false),
play_state: state.mopidy.play_state,
time_position: state.mopidy.time_position,
consume: state.mopidy.consume,
repeat: state.mopidy.repeat,
random: state.mopidy.random,
2018-03-23 16:54:56 +13:00
volume: state.mopidy.volume,
mute: state.mopidy.mute,
2018-12-10 16:04:40 +13:00
sidebar_open: state.ui.sidebar_open,
slim_mode: state.ui.slim_mode
}
}
const mapDispatchToProps = (dispatch) => {
return {
2018-02-05 17:04:31 +13:00
coreActions: bindActionCreators(coreActions, dispatch),
2017-06-22 09:22:10 +12:00
uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(PlaybackControls)