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';
|
2019-09-05 15:36:11 +12:00
|
|
|
import ProgressSlider from './Fields/ProgressSlider';
|
|
|
|
|
import VolumeControl from './Fields/VolumeControl';
|
|
|
|
|
import MuteControl from './Fields/MuteControl';
|
|
|
|
|
import OutputControl from './Fields/OutputControl';
|
|
|
|
|
import Dater from './Dater';
|
2019-10-05 09:10:34 +13:00
|
|
|
import LinksSentence from './LinksSentence';
|
2019-09-05 15:36:11 +12:00
|
|
|
import Thumbnail from './Thumbnail';
|
|
|
|
|
import Icon from './Icon';
|
2020-03-20 08:15:15 +13:00
|
|
|
import Stream from './Stream';
|
2020-03-03 16:39:37 +13:00
|
|
|
import { scrollTo } from '../util/helpers';
|
2019-09-05 15:36:11 +12:00
|
|
|
import * as uiActions from '../services/ui/actions';
|
|
|
|
|
import * as coreActions from '../services/core/actions';
|
|
|
|
|
import * as mopidyActions from '../services/mopidy/actions';
|
|
|
|
|
|
2019-10-05 09:10:34 +13:00
|
|
|
class PlaybackControls extends React.Component {
|
2019-09-05 15:36:11 +12:00
|
|
|
constructor(props) {
|
|
|
|
|
super(props);
|
|
|
|
|
this.stream = null;
|
|
|
|
|
this.state = {
|
|
|
|
|
expanded: false,
|
|
|
|
|
current_track: null,
|
|
|
|
|
transition_track: null,
|
|
|
|
|
transition_direction: null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-12 11:45:02 +13:00
|
|
|
static getDerivedStateFromProps({ current_track }, state) {
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
current_track,
|
|
|
|
|
};
|
2019-09-05 15:36:11 +12:00
|
|
|
}
|
|
|
|
|
|
2020-04-08 09:15:45 +12:00
|
|
|
handleTouchStart = (e) => {
|
|
|
|
|
const { touch_enabled } = this.props;
|
|
|
|
|
if (!touch_enabled) return;
|
|
|
|
|
|
2019-09-05 15:36:11 +12:00
|
|
|
const timestamp = Math.floor(Date.now());
|
|
|
|
|
|
|
|
|
|
// Save touch start details
|
|
|
|
|
this.start_time = timestamp;
|
|
|
|
|
this.start_position = {
|
|
|
|
|
x: e.touches[0].clientX,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-08 09:15:45 +12:00
|
|
|
handleTouchEnd = (e) => {
|
|
|
|
|
const { touch_enabled } = this.props;
|
|
|
|
|
if (!touch_enabled) return;
|
|
|
|
|
|
2019-09-05 15:36:11 +12:00
|
|
|
const timestamp = Math.floor(Date.now());
|
|
|
|
|
const tap_distance_threshold = 10; // Max distance (px) between touchstart and touchend to qualify as a tap
|
|
|
|
|
const tap_time_threshold = 200; // Max time (ms) between touchstart and touchend to qualify as a tap
|
|
|
|
|
const 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
|
2019-10-05 09:10:34 +13:00
|
|
|
&& this.start_position.x - tap_distance_threshold < end_position.x) {
|
2019-09-05 15:36:11 +12:00
|
|
|
// Scroll to top (without smooth_scroll)
|
2020-03-03 16:39:37 +13:00
|
|
|
scrollTo(null, false);
|
2019-09-05 15:36:11 +12:00
|
|
|
this.props.history.push('/queue');
|
2019-10-05 09:10:34 +13:00
|
|
|
} else {
|
2019-09-05 15:36:11 +12:00
|
|
|
// 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;
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setTransition(direction) {
|
|
|
|
|
this.setState({
|
|
|
|
|
current_track: null,
|
|
|
|
|
transition_track: this.state.current_track,
|
|
|
|
|
transition_direction: direction,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Allow time for the animation to complete, then remove
|
|
|
|
|
// the transitioning track from state
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.setState({
|
|
|
|
|
transition_track: null,
|
|
|
|
|
transition_direction: null,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
250);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderPlayButton() {
|
|
|
|
|
let button = <button className="control play" onClick={() => this.props.mopidyActions.play()}><Icon name="play_circle_filled" type="material" /></button>;
|
|
|
|
|
if (this.props.play_state == 'playing') {
|
|
|
|
|
button = <button className="control play" onClick={() => this.props.mopidyActions.pause()}><Icon name="pause_circle_filled" type="material" /></button>;
|
|
|
|
|
}
|
|
|
|
|
return button;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderConsumeButton() {
|
2019-10-05 09:10:34 +13:00
|
|
|
let button = (
|
|
|
|
|
<button className="control tooltip" onClick={() => this.props.mopidyActions.setConsume(true)}>
|
|
|
|
|
<Icon name="restaurant" type="material" />
|
|
|
|
|
<span className="tooltip__content">Consume</span>
|
|
|
|
|
</button>
|
2019-09-05 15:36:11 +12:00
|
|
|
);
|
|
|
|
|
if (this.props.consume) {
|
2019-10-05 09:10:34 +13: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>
|
2019-09-05 15:36:11 +12:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return button;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderRandomButton() {
|
2019-10-05 09:10:34 +13:00
|
|
|
let button = (
|
|
|
|
|
<button className="control tooltip" onClick={() => this.props.mopidyActions.setRandom(true)}>
|
|
|
|
|
<Icon name="shuffle" type="material" />
|
|
|
|
|
<span className="tooltip__content">Shuffle</span>
|
|
|
|
|
</button>
|
2019-09-05 15:36:11 +12:00
|
|
|
);
|
|
|
|
|
if (this.props.random) {
|
2019-10-05 09:10:34 +13: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>
|
2019-09-05 15:36:11 +12:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return button;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderRepeatButton() {
|
2019-10-05 09:10:34 +13:00
|
|
|
let button = (
|
|
|
|
|
<button className="control tooltip" onClick={() => this.props.mopidyActions.setRepeat(true)}>
|
|
|
|
|
<Icon name="repeat" />
|
|
|
|
|
<span className="tooltip__content">Repeat</span>
|
|
|
|
|
</button>
|
2019-09-05 15:36:11 +12:00
|
|
|
);
|
|
|
|
|
if (this.props.repeat) {
|
2019-10-05 09:10:34 +13:00
|
|
|
button = (
|
|
|
|
|
<button className="control control--active tooltip" onClick={() => this.props.mopidyActions.setRepeat(false)}>
|
|
|
|
|
<Icon name="repeat" />
|
|
|
|
|
<span className="tooltip__content">Repeat</span>
|
|
|
|
|
</button>
|
2019-09-05 15:36:11 +12:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return button;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
const { next_track, touch_enabled, time_position } = this.props;
|
|
|
|
|
const { current_track, expanded } = this.state;
|
|
|
|
|
|
|
|
|
|
let images = false;
|
|
|
|
|
if (current_track && current_track.images) {
|
|
|
|
|
images = current_track.images;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={`playback-controls${expanded ? ' playback-controls--expanded' : ''}${touch_enabled ? ' playback-controls--touch-enabled' : ''}`}>
|
|
|
|
|
|
2020-04-22 21:07:20 +12:00
|
|
|
<div className="playback-controls__background" />
|
|
|
|
|
|
2020-03-20 08:15:15 +13:00
|
|
|
<Stream />
|
|
|
|
|
|
2019-09-05 15:36:11 +12:00
|
|
|
{next_track && next_track.images ? <Thumbnail className="hide" size="large" images={next_track.images} /> : null}
|
|
|
|
|
|
2019-10-05 09:10:34 +13:00
|
|
|
{this.state.transition_track && this.state.transition_direction ? (
|
2019-09-05 15:36:11 +12:00
|
|
|
<div
|
2019-10-05 09:10:34 +13:00
|
|
|
className={`current-track current-track__transition current-track__transition--${this.state.transition_direction}`}
|
2019-09-05 15:36:11 +12:00
|
|
|
>
|
|
|
|
|
<div className="text">
|
|
|
|
|
<div className="title">
|
|
|
|
|
{this.state.transition_track.name}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="artist">
|
2019-10-05 09:10:34 +13:00
|
|
|
<LinksSentence items={this.state.transition_track.artists} nolinks />
|
2019-09-05 15:36:11 +12:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2019-10-05 09:10:34 +13:00
|
|
|
</div>
|
2019-09-05 15:36:11 +12:00
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
className={this.state.transition_track && this.state.transition_direction ? 'current-track current-track--transitioning' : 'current-track'}
|
2020-04-08 09:15:45 +12:00
|
|
|
onTouchStart={this.handleTouchStart}
|
|
|
|
|
onTouchEnd={this.handleTouchEnd}
|
2019-10-05 09:10:34 +13:00
|
|
|
tabIndex="-1"
|
2019-09-05 15:36:11 +12:00
|
|
|
>
|
2020-02-18 08:32:22 +13:00
|
|
|
<Link className="thumbnail-wrapper" to="/kiosk-mode" tabIndex="-1">
|
2020-04-17 21:44:49 +12:00
|
|
|
<Thumbnail size="small" images={images} type="track" />
|
2020-02-18 08:32:22 +13:00
|
|
|
</Link>
|
2019-09-05 15:36:11 +12:00
|
|
|
<div className="text">
|
|
|
|
|
<div className="title">
|
|
|
|
|
{current_track ? current_track.name : <span>-</span>}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="artist">
|
2019-10-05 09:10:34 +13:00
|
|
|
{current_track ? <LinksSentence items={current_track.artists} /> : <LinksSentence />}
|
2019-09-05 15:36:11 +12:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<section className="playback">
|
|
|
|
|
<button className="control previous" onClick={() => this.props.mopidyActions.previous()}>
|
|
|
|
|
<Icon name="navigate_before" type="material" />
|
|
|
|
|
</button>
|
|
|
|
|
{ this.renderPlayButton() }
|
|
|
|
|
<button className="control next" onClick={() => this.props.mopidyActions.next()}>
|
|
|
|
|
<Icon name="navigate_next" type="material" />
|
|
|
|
|
</button>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="progress">
|
2020-04-22 21:07:20 +12:00
|
|
|
<div className="time time--current">
|
|
|
|
|
{time_position ? <Dater type="length" data={time_position} /> : '-'}
|
|
|
|
|
</div>
|
2019-09-05 15:36:11 +12:00
|
|
|
<ProgressSlider />
|
2020-04-22 21:07:20 +12:00
|
|
|
<div className="time time--total">
|
|
|
|
|
{current_track ? <Dater type="length" data={current_track.duration} /> : '-'}
|
|
|
|
|
</div>
|
2019-09-05 15:36:11 +12:00
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="settings">
|
|
|
|
|
{this.renderConsumeButton()}
|
|
|
|
|
{this.renderRandomButton()}
|
|
|
|
|
{this.renderRepeatButton()}
|
|
|
|
|
<OutputControl force_expanded={this.state.expanded} />
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="volume">
|
|
|
|
|
<MuteControl
|
|
|
|
|
mute={this.props.mute}
|
2019-10-05 09:10:34 +13:00
|
|
|
onMuteChange={(mute) => this.props.mopidyActions.setMute(mute)}
|
2019-09-05 15:36:11 +12:00
|
|
|
/>
|
|
|
|
|
<VolumeControl
|
|
|
|
|
scrollWheel
|
|
|
|
|
volume={this.props.volume}
|
|
|
|
|
mute={this.props.mute}
|
2019-10-05 09:10:34 +13:00
|
|
|
onVolumeChange={(percent) => this.props.mopidyActions.setVolume(percent)}
|
2019-09-05 15:36:11 +12:00
|
|
|
/>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="triggers">
|
|
|
|
|
<button className="control expanded-controls" onClick={(e) => this.setState({ expanded: !this.state.expanded })}>
|
|
|
|
|
{this.state.expanded ? <Icon name="expand_more" type="material" /> : <Icon name="expand_less" type="material" />}
|
|
|
|
|
</button>
|
|
|
|
|
<button className={`control sidebar-toggle${this.props.sidebar_open ? ' open' : ''}`} onClick={(e) => this.props.uiActions.toggleSidebar()}>
|
|
|
|
|
<Icon className="open" name="menu" type="material" />
|
|
|
|
|
</button>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2017-06-16 09:37:40 +12:00
|
|
|
}
|
|
|
|
|
|
2020-03-20 08:15:15 +13:00
|
|
|
const mapStateToProps = (state) => ({
|
2019-09-05 15:36:11 +12:00
|
|
|
snapcast_enabled: state.pusher.config.snapcast_enabled,
|
|
|
|
|
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)),
|
|
|
|
|
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,
|
|
|
|
|
touch_enabled: state.ui.playback_controls_touch_enabled,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const mapDispatchToProps = (dispatch) => ({
|
|
|
|
|
coreActions: bindActionCreators(coreActions, dispatch),
|
|
|
|
|
uiActions: bindActionCreators(uiActions, dispatch),
|
|
|
|
|
mopidyActions: bindActionCreators(mopidyActions, dispatch),
|
|
|
|
|
});
|
|
|
|
|
|
2019-10-05 09:10:34 +13:00
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(PlaybackControls);
|