2017-06-16 09:37:40 +12:00
|
|
|
|
2019-02-10 19:51:33 +13:00
|
|
|
import React from 'react';
|
2018-12-19 21:47:02 +13:00
|
|
|
import { connect } from 'react-redux';
|
|
|
|
|
import { bindActionCreators } from 'redux';
|
2017-06-16 09:37:40 +12:00
|
|
|
|
2018-12-19 21:47:02 +13:00
|
|
|
import Link from './Link';
|
2018-06-26 15:37:50 +12:00
|
|
|
import ProgressSlider from './Fields/ProgressSlider'
|
|
|
|
|
import VolumeControl from './Fields/VolumeControl'
|
2018-10-02 16:58:15 +13:00
|
|
|
import MuteControl from './Fields/MuteControl'
|
2018-07-01 21:08:35 +12:00
|
|
|
import OutputControl from './Fields/OutputControl'
|
2018-06-26 15:37:50 +12:00
|
|
|
import Dater from './Dater'
|
|
|
|
|
import ArtistSentence from './ArtistSentence'
|
|
|
|
|
import Thumbnail from './Thumbnail'
|
|
|
|
|
import Icon from './Icon'
|
2017-06-16 09:37:40 +12:00
|
|
|
|
2018-06-26 15:37:50 +12:00
|
|
|
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'
|
2017-06-16 09:37:40 +12:00
|
|
|
|
|
|
|
|
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)
|
2018-06-30 21:37:55 +12:00
|
|
|
this.stream = null;
|
2017-06-22 08:59:40 +12:00
|
|
|
this.state = {
|
2019-01-06 14:12:57 +13:00
|
|
|
expanded: false,
|
2019-02-12 19:30:35 +13:00
|
|
|
current_track: null,
|
2019-02-11 20:14:25 +13:00
|
|
|
transition_track: null,
|
|
|
|
|
transition_direction: null
|
2017-06-22 08:59:40 +12:00
|
|
|
}
|
2017-06-16 09:37:40 +12:00
|
|
|
}
|
|
|
|
|
|
2018-06-30 21:37:55 +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();
|
2018-06-30 21:37:55 +12:00
|
|
|
}
|
2019-03-19 21:24:31 +13:00
|
|
|
|
|
|
|
|
if (this.props.current_track){
|
|
|
|
|
this.setState({current_track: this.props.current_track});
|
|
|
|
|
}
|
2018-06-30 21:37:55 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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){
|
2018-06-30 21:37:55 +12:00
|
|
|
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;
|
2018-06-30 21:37:55 +12:00
|
|
|
this.stream.play();
|
2018-07-02 21:37:24 +12:00
|
|
|
|
|
|
|
|
console.log("Playing stream: "+this.stream.src);
|
2018-06-30 21:37:55 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentWillReceiveProps(nextProps){
|
2018-07-01 21:08:35 +12:00
|
|
|
|
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-06-30 21:37:55 +12:00
|
|
|
|
2018-07-02 21:37:24 +12:00
|
|
|
// Just been enabled
|
|
|
|
|
if (!this.props.http_streaming_enabled && nextProps.http_streaming_enabled){
|
2018-06-30 21:37:55 +12:00
|
|
|
this.playStream(nextProps);
|
|
|
|
|
}
|
2018-07-01 21:08:35 +12:00
|
|
|
|
|
|
|
|
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){
|
2018-07-01 21:08:35 +12:00
|
|
|
this.stream = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-02-12 19:30:35 +13:00
|
|
|
|
2019-02-12 20:20:25 +13:00
|
|
|
// Started a track or changed to no track
|
2019-02-12 19:30:35 +13:00
|
|
|
if ((!this.props.current_track && nextProps.current_track) || (this.props.current_track && !nextProps.current_track)){
|
|
|
|
|
this.setState({current_track: nextProps.current_track});
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-12 20:20:25 +13:00
|
|
|
// Direct swap-out of a track (with no 'null' intermediate state)
|
|
|
|
|
// This is precautionary and I've never been able to trigger it, but it's a good safety
|
2019-02-12 19:30:35 +13:00
|
|
|
if (this.props.current_track && nextProps.current_track && this.props.current_track.uri !== nextProps.current_track.uri){
|
|
|
|
|
this.setState({current_track: nextProps.current_track});
|
|
|
|
|
}
|
2019-02-12 20:20:25 +13:00
|
|
|
|
|
|
|
|
// Images have just loaded
|
|
|
|
|
// A bit niggly to have to deeply check this...
|
|
|
|
|
if (this.props.current_track && nextProps.current_track){
|
|
|
|
|
|
|
|
|
|
if ((this.props.current_track.images && !nextProps.current_track.images) || (!this.props.current_track.images && nextProps.current_track.images)){
|
|
|
|
|
this.setState({current_track: nextProps.current_track});
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-06-30 21:37:55 +12:00
|
|
|
}
|
|
|
|
|
|
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){
|
|
|
|
|
|
2019-01-30 13:22:36 +13:00
|
|
|
// Scroll to top (without smooth_scroll)
|
|
|
|
|
helpers.scrollTo(null, false);
|
2019-01-22 16:57:43 +13:00
|
|
|
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){
|
2019-02-11 20:14:25 +13:00
|
|
|
this.setTransition('previous');
|
2018-12-10 16:04:40 +13:00
|
|
|
this.props.mopidyActions.previous();
|
|
|
|
|
|
|
|
|
|
// Swipe to the right = skip track
|
|
|
|
|
} else if (this.start_position.x > end_position.x){
|
2019-02-11 20:14:25 +13:00
|
|
|
this.setTransition('next');
|
2018-12-10 16:04:40 +13:00
|
|
|
this.props.mopidyActions.next();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.end_time = timestamp;
|
2019-07-23 08:41:00 +12:00
|
|
|
e.preventDefault();
|
2018-12-10 16:04:40 +13:00
|
|
|
}
|
|
|
|
|
|
2019-02-11 20:14:25 +13:00
|
|
|
setTransition(direction){
|
|
|
|
|
this.setState({
|
2019-02-12 19:30:35 +13:00
|
|
|
current_track: null,
|
|
|
|
|
transition_track: this.state.current_track,
|
2019-02-11 20:14:25 +13:00
|
|
|
transition_direction: direction
|
|
|
|
|
});
|
2019-01-06 14:12:57 +13:00
|
|
|
|
2019-02-17 20:37:21 +13:00
|
|
|
// Allow time for the animation to complete, then remove
|
|
|
|
|
// the transitioning track from state
|
2019-01-06 14:12:57 +13:00
|
|
|
var self = this;
|
|
|
|
|
setTimeout(() => {
|
2019-02-11 20:14:25 +13:00
|
|
|
this.setState({
|
|
|
|
|
transition_track: null,
|
|
|
|
|
transition_direction: null
|
|
|
|
|
});
|
2019-01-06 14:12:57 +13:00
|
|
|
},
|
2019-02-18 08:47:15 +13:00
|
|
|
250
|
2019-01-06 14:12:57 +13:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-16 09:37:40 +12:00
|
|
|
renderPlayButton(){
|
2019-05-10 10:34:58 +12:00
|
|
|
var button = <button className="control play" onClick={() => this.props.mopidyActions.play()}><Icon name="play_circle_filled" type="material" /></button>
|
2017-10-10 13:05:28 +13:00
|
|
|
if (this.props.play_state == 'playing'){
|
2019-05-10 10:34:58 +12:00
|
|
|
button = <button className="control play" onClick={() => this.props.mopidyActions.pause()}><Icon name="pause_circle_filled" type="material" /></button>
|
2017-06-16 09:37:40 +12:00
|
|
|
}
|
|
|
|
|
return button;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderConsumeButton(){
|
2019-05-10 10:34:58 +12:00
|
|
|
var button = <button className="control tooltip" onClick={() => this.props.mopidyActions.setConsume(true)}><Icon name="restaurant" type="material" /><span className="tooltip__content">Consume</span></button>
|
2017-10-10 13:05:28 +13:00
|
|
|
if (this.props.consume){
|
2019-05-10 10:34:58 +12:00
|
|
|
button = <button className="control control--active tooltip" onClick={() => this.props.mopidyActions.setConsume(false)}><Icon name="restaurant" type="material" /><span className="tooltip__content">Consume</span></button>
|
2017-06-16 09:37:40 +12:00
|
|
|
}
|
|
|
|
|
return button;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderRandomButton(){
|
2019-05-10 10:34:58 +12:00
|
|
|
var button = <button className="control tooltip" onClick={() => this.props.mopidyActions.setRandom(true)}><Icon name="shuffle" type="material" /><span className="tooltip__content">Shuffle</span></button>
|
2017-10-10 13:05:28 +13:00
|
|
|
if (this.props.random){
|
2019-05-10 10:34:58 +12:00
|
|
|
button = <button className="control control--active tooltip" onClick={() => this.props.mopidyActions.setRandom(false)}><Icon name="shuffle" type="material" /><span className="tooltip__content">Shuffle</span></button>
|
2017-06-16 09:37:40 +12:00
|
|
|
}
|
|
|
|
|
return button;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderRepeatButton(){
|
2019-05-10 10:34:58 +12:00
|
|
|
var button = <button className="control tooltip" onClick={() => this.props.mopidyActions.setRepeat(true)}><Icon name="repeat" /><span className="tooltip__content">Repeat</span></button>
|
2017-10-10 13:05:28 +13:00
|
|
|
if (this.props.repeat){
|
2019-05-10 10:34:58 +12:00
|
|
|
button = <button className="control control--active tooltip" onClick={() => this.props.mopidyActions.setRepeat(false)}><Icon name="repeat" /><span className="tooltip__content">Repeat</span></button>
|
2017-06-16 09:37:40 +12:00
|
|
|
}
|
|
|
|
|
return button;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render(){
|
2017-07-15 23:49:17 +12:00
|
|
|
var images = false
|
2019-02-12 19:30:35 +13:00
|
|
|
if (this.state.current_track && this.state.current_track.images){
|
|
|
|
|
images = this.state.current_track.images
|
2017-06-16 09:37:40 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2018-11-18 12:54:13 +13:00
|
|
|
<div className={(this.state.expanded ? "playback-controls--expanded playback-controls" : "playback-controls")}>
|
2018-02-01 08:52:15 +13:00
|
|
|
|
2018-04-12 15:55:39 +12:00
|
|
|
{this.props.next_track && this.props.next_track.images ? <Thumbnail className="hide" size="large" images={this.props.next_track.images} /> : null}
|
2017-06-16 09:37:40 +12:00
|
|
|
|
2019-02-11 20:14:25 +13:00
|
|
|
{this.state.transition_track && this.state.transition_direction ? <div
|
|
|
|
|
className={"current-track current-track__transition current-track__transition--"+this.state.transition_direction}>
|
2019-02-17 20:37:21 +13:00
|
|
|
<div className="text">
|
|
|
|
|
<div className="title">
|
|
|
|
|
{this.state.transition_track.name}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="artist">
|
|
|
|
|
<ArtistSentence artists={this.state.transition_track.artists} nolinks={true} />
|
|
|
|
|
</div>
|
2019-02-11 20:14:25 +13:00
|
|
|
</div>
|
|
|
|
|
</div> : null}
|
|
|
|
|
|
2018-12-10 16:04:40 +13:00
|
|
|
<div
|
2019-02-11 20:14:25 +13:00
|
|
|
className={this.state.transition_track && this.state.transition_direction ? "current-track current-track--transitioning" : "current-track"}
|
2018-12-10 16:04:40 +13:00
|
|
|
onTouchStart={e => this.handleTouchStart(e)}
|
2019-06-26 14:46:56 +12:00
|
|
|
onTouchEnd={e => this.handleTouchEnd(e)}
|
|
|
|
|
tabIndex="-1">
|
|
|
|
|
<Link className="thumbnail-wrapper" to={'/kiosk-mode'} tabIndex="-1">
|
2018-12-10 16:04:40 +13:00
|
|
|
<Thumbnail size="small" images={images} />
|
|
|
|
|
</Link>
|
2019-02-17 20:37:21 +13:00
|
|
|
<div className="text">
|
|
|
|
|
<div className="title">
|
|
|
|
|
{this.state.current_track ? this.state.current_track.name : <span>-</span>}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="artist">
|
2019-07-23 08:41:00 +12:00
|
|
|
{this.state.current_track ? <ArtistSentence artists={this.state.current_track.artists} /> : <ArtistSentence />}
|
2019-02-17 20:37:21 +13:00
|
|
|
</div>
|
2018-12-10 16:04:40 +13:00
|
|
|
</div>
|
2017-07-26 15:24:18 +12:00
|
|
|
</div>
|
2017-06-16 09:37:40 +12:00
|
|
|
|
|
|
|
|
<section className="playback">
|
2019-05-10 10:34:58 +12:00
|
|
|
<button className="control previous" onClick={() => this.props.mopidyActions.previous()}>
|
2019-07-25 17:00:49 +12:00
|
|
|
<Icon name="navigate_before" type="material" />
|
2019-05-10 10:34:58 +12:00
|
|
|
</button>
|
2017-06-16 09:37:40 +12:00
|
|
|
{ this.renderPlayButton() }
|
2019-05-10 10:34:58 +12:00
|
|
|
<button className="control next" onClick={() => this.props.mopidyActions.next()}>
|
2019-07-25 17:00:49 +12:00
|
|
|
<Icon name="navigate_next" type="material" />
|
2019-05-10 10:34:58 +12:00
|
|
|
</button>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="progress">
|
|
|
|
|
<ProgressSlider />
|
|
|
|
|
<span className="current">{ this.props.time_position ? <Dater type="length" data={this.props.time_position} /> : '-' }</span>
|
|
|
|
|
<span className="total">{ this.state.current_track ? <Dater type="length" data={this.state.current_track.duration} /> : '-' }</span>
|
2017-06-16 09:37:40 +12:00
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="settings">
|
2017-10-21 18:45:55 +13:00
|
|
|
{this.renderConsumeButton()}
|
|
|
|
|
{this.renderRandomButton()}
|
|
|
|
|
{this.renderRepeatButton()}
|
2018-10-02 16:58:15 +13:00
|
|
|
<OutputControl force_expanded={this.state.expanded} />
|
2017-06-16 09:37:40 +12:00
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="volume">
|
2018-10-02 16:58:15 +13:00
|
|
|
<MuteControl
|
|
|
|
|
mute={this.props.mute}
|
|
|
|
|
onMuteChange={mute => this.props.mopidyActions.setMute(mute)}
|
|
|
|
|
/>
|
2018-03-23 16:54:56 +13:00
|
|
|
<VolumeControl
|
2018-04-19 08:05:42 +12:00
|
|
|
scrollWheel
|
2018-03-23 16:54:56 +13:00
|
|
|
volume={this.props.volume}
|
|
|
|
|
mute={this.props.mute}
|
|
|
|
|
onVolumeChange={percent => this.props.mopidyActions.setVolume(percent)}
|
|
|
|
|
/>
|
2017-06-16 09:37:40 +12:00
|
|
|
</section>
|
2017-06-22 08:59:40 +12:00
|
|
|
|
|
|
|
|
<section className="triggers">
|
2019-05-10 10:34:58 +12:00
|
|
|
<button 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" />}
|
2019-05-10 10:34:58 +12:00
|
|
|
</button>
|
|
|
|
|
<button 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" />
|
2019-05-10 10:34:58 +12:00
|
|
|
</button>
|
2017-06-22 08:59:40 +12:00
|
|
|
</section>
|
2017-06-16 09:37:40 +12:00
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mapStateToProps = (state, ownProps) => {
|
|
|
|
|
return {
|
2018-05-30 21:39:38 +12:00
|
|
|
snapcast_enabled: state.pusher.config.snapcast_enabled,
|
2018-02-02 08:39:37 +13:00
|
|
|
http_streaming_enabled: state.core.http_streaming_enabled,
|
2018-07-01 21:08:35 +12:00
|
|
|
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,
|
2017-11-17 20:25:43 +13:00
|
|
|
current_track: (state.core.current_track && state.core.tracks[state.core.current_track.uri] !== undefined ? state.core.tracks[state.core.current_track.uri] : null),
|
2018-04-12 15:55:39 +12:00
|
|
|
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),
|
2017-06-16 09:37:40 +12:00
|
|
|
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,
|
2017-10-21 18:45:55 +13:00
|
|
|
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
|
2017-06-16 09:37:40 +12:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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),
|
2017-06-16 09:37:40 +12:00
|
|
|
mopidyActions: bindActionCreators(mopidyActions, dispatch)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(PlaybackControls)
|