import React 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{ constructor(props){ super(props) this.stream = null; this.state = { expanded: false, current_track: null, transition_track: null, transition_direction: null } } componentDidMount(){ if (this.props.http_streaming_enabled){ // 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; } if (!props.http_streaming_enabled || !props.http_streaming_url){ return false; } 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(); console.log("Playing stream: "+this.stream.src); } componentWillReceiveProps(nextProps){ // 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); } // 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 if (!nextProps.http_streaming_enabled){ this.stream = null; } } if ((!this.props.current_track && nextProps.current_track) || (this.props.current_track && !nextProps.current_track)){ console.log("One was null, the other wasn't", {old: this.props.current_track, next: nextProps.current_track}); this.setState({current_track: nextProps.current_track}); } if (this.props.current_track && nextProps.current_track && this.props.current_track.uri !== nextProps.current_track.uri){ console.log("URI different", {old: this.props.current_track.uri, next: nextProps.current_track.uri}); this.setState({current_track: nextProps.current_track}); } } 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){ // Scroll to top (without smooth_scroll) helpers.scrollTo(null, false); this.props.history.push('/queue'); } else { // Swipe to the left = previous track if (this.start_position.x < end_position.x){ this.setTransition('previous'); this.props.mopidyActions.previous(); // Swipe to the right = skip track } else if (this.start_position.x > end_position.x){ this.setTransition('next'); this.props.mopidyActions.next(); } } this.end_time = timestamp; } setTransition(direction){ this.setState({ current_track: null, transition_track: this.state.current_track, transition_direction: direction }); var self = this; setTimeout(() => { this.setState({ transition_track: null, transition_direction: null }); }, 200 ); } renderPlayButton(){ var button = this.props.mopidyActions.play()}> if (this.props.play_state == 'playing'){ button = this.props.mopidyActions.pause()}> } return button; } renderConsumeButton(){ var button = this.props.mopidyActions.setConsume(true)}>Consume if (this.props.consume){ button = this.props.mopidyActions.setConsume(false)}>Consume } return button; } renderRandomButton(){ var button = this.props.mopidyActions.setRandom(true)}>Shuffle if (this.props.random){ button = this.props.mopidyActions.setRandom(false)}>Shuffle } return button; } renderRepeatButton(){ var button = this.props.mopidyActions.setRepeat(true)}>Repeat if (this.props.repeat){ button = this.props.mopidyActions.setRepeat(false)}>Repeat } return button; } render(){ var images = false if (this.state.current_track && this.state.current_track.images){ images = this.state.current_track.images } return (
{this.props.next_track && this.props.next_track.images ? : null} {this.state.transition_track && this.state.transition_direction ?
{this.state.transition_track.name}
: null}
this.handleTouchStart(e)} onTouchEnd={e => this.handleTouchEnd(e)}>
{this.state.current_track ? this.state.current_track.name : -}
{this.state.current_track ? : }
this.props.mopidyActions.previous()}> { this.renderPlayButton() } this.props.mopidyActions.stop()}> this.props.mopidyActions.next()}>
{this.renderConsumeButton()} {this.renderRandomButton()} {this.renderRepeatButton()}
{ this.props.time_position ? : '-' } { this.state.current_track ? : '-' }
this.props.mopidyActions.setMute(mute)} /> this.props.mopidyActions.setVolume(percent)} />
this.setState({expanded: !this.state.expanded})}> {this.state.expanded ? : } this.props.uiActions.toggleSidebar()}>
); } } const mapStateToProps = (state, ownProps) => { return { snapcast_enabled: state.pusher.config.snapcast_enabled, 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), 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, volume: state.mopidy.volume, mute: state.mopidy.mute, sidebar_open: state.ui.sidebar_open, slim_mode: state.ui.slim_mode } } const mapDispatchToProps = (dispatch) => { return { coreActions: bindActionCreators(coreActions, dispatch), uiActions: bindActionCreators(uiActions, dispatch), mopidyActions: bindActionCreators(mopidyActions, dispatch) } } export default connect(mapStateToProps, mapDispatchToProps)(PlaybackControls)