Listening to client (dis)connections and auto-updating Snapcast group availability

This commit is contained in:
James Barnsley
2020-04-08 09:15:45 +12:00
parent 0d3ac044b8
commit af43131a81
18 changed files with 309 additions and 195 deletions

View File

@ -10,6 +10,7 @@ import * as coreActions from '../../services/core/actions';
import * as pusherActions from '../../services/pusher/actions';
import * as snapcastActions from '../../services/snapcast/actions';
import { sortItems, indexToArray } from '../../util/arrays';
import { collate } from '../../util/format';
class OutputControl extends React.Component {
constructor(props) {
@ -57,6 +58,7 @@ class OutputControl extends React.Component {
snapcast_streams,
snapcastActions,
snapcast_groups,
snapcast_clients: clients,
} = this.props;
const groups = indexToArray(snapcast_groups);
@ -66,39 +68,48 @@ class OutputControl extends React.Component {
(id) => ({ value: id, label: id }),
);
return (
<div>
{
groups.map((group) => (
<div className="output-control__item outputs__item--snapcast" key={group.id}>
<div className="output-control__item__name">
{group.name}
groups.map((simpleGroup) => {
const group = collate(simpleGroup, { clients });
if (
!group.clients ||
!group.clients.length ||
!group.clients.filter((client) => client.connected).length
) {
return null;
}
return (
<div className="output-control__item outputs__item--snapcast" key={group.id}>
<div className="output-control__item__name">
{group.name}
</div>
<div className="output-control__item__controls">
<DropdownField
name="Source"
value={group.stream_id}
icon="settings_input_component"
options={streams}
noLabel
handleChange={(value) => snapcastActions.setGroupStream(group.id, value)}
/>
<MuteControl
className="output-control__item__mute"
noTooltip
mute={group.mute}
onMuteChange={(mute) => snapcastActions.setGroupMute(group.id, mute)}
/>
<VolumeControl
className="output-control__item__volume"
volume={group.volume}
mute={group.mute}
onVolumeChange={(percent, previousPercent) => snapcastActions.setGroupVolume(group.id, percent, previousPercent)}
/>
</div>
</div>
<div className="output-control__item__controls">
<DropdownField
name="Source"
value={group.stream_id}
icon="settings_input_component"
options={streams}
noLabel
handleChange={(value) => snapcastActions.setGroupStream(group.id, value)}
/>
<MuteControl
className="output-control__item__mute"
noTooltip
mute={group.mute}
onMuteChange={(mute) => snapcastActions.setGroupMute(group.id, mute)}
/>
<VolumeControl
className="output-control__item__volume"
volume={group.volume}
mute={group.mute}
onVolumeChange={(percent, previousPercent) => snapcastActions.setGroupVolume(group.id, percent, previousPercent)}
/>
</div>
</div>
))
)
})
}
</div>
);
@ -223,6 +234,7 @@ const mapStateToProps = (state, ownProps) => ({
pusher_connected: state.pusher.connected,
snapcast_enabled: (state.pusher.config ? state.pusher.config.snapcast_enabled : null),
show_disconnected_clients: (state.ui.snapcast_show_disconnected_clients !== undefined ? state.ui.snapcast_show_disconnected_clients : false),
snapcast_clients: state.snapcast.clients,
snapcast_groups: state.snapcast.groups,
snapcast_streams: state.snapcast.streams,
pusher_commands: (state.pusher.commands ? state.pusher.commands : {}),

View File

@ -11,7 +11,7 @@ export default ({
retainScroll,
scrollTo: scrollToProp,
onContextMenu,
className,
className = '',
activeClassName,
to,
exact,

View File

@ -30,17 +30,6 @@ class PlaybackControls extends React.Component {
};
}
componentDidMount() {
if (this.props.http_streaming_enabled) {
// Bust our cache, and by consequence, play our stream
this.props.coreActions.cachebustHttpStream();
}
if (this.props.current_track) {
this.setState({ current_track: this.props.current_track });
}
}
static getDerivedStateFromProps({ current_track }, state) {
return {
...state,
@ -48,7 +37,10 @@ class PlaybackControls extends React.Component {
};
}
handleTouchStart(e) {
handleTouchStart = (e) => {
const { touch_enabled } = this.props;
if (!touch_enabled) return;
const timestamp = Math.floor(Date.now());
// Save touch start details
@ -60,7 +52,10 @@ class PlaybackControls extends React.Component {
return false;
}
handleTouchEnd(e) {
handleTouchEnd = (e) => {
const { touch_enabled } = this.props;
if (!touch_enabled) return;
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
@ -210,8 +205,8 @@ class PlaybackControls extends React.Component {
<div
className={this.state.transition_track && this.state.transition_direction ? 'current-track current-track--transitioning' : 'current-track'}
onTouchStart={(e) => touch_enabled && this.handleTouchStart(e)}
onTouchEnd={(e) => touch_enabled && this.handleTouchEnd(e)}
onTouchStart={this.handleTouchStart}
onTouchEnd={this.handleTouchEnd}
tabIndex="-1"
>
<Link className="thumbnail-wrapper" to="/kiosk-mode" tabIndex="-1">

View File

@ -31,12 +31,12 @@ const SnapcastGroups = (props) => {
return null;
}
const renderGroup = () => {
if (!groupId || !groups[groupId]) {
return null;
}
const group = groupId && groups[groupId]
? collate(groups[groupId], { clients })
: null;
const group = collate(groups[groupId], { clients });
const renderGroup = () => {
if (!group) return null;
return (
<div className="snapcast__group" key={group.id}>
@ -97,20 +97,29 @@ const SnapcastGroups = (props) => {
);
}
const renderMenuItem = (group) => {
const renderMenuItem = (simpleGroup) => {
const group = collate(simpleGroup, { clients });
const anyClients = (
!show_disconnected_clients && (
!group.clients ||
!group.clients.length ||
!group.clients.filter((client) => client.connected).length
)
);
const icon = () => {
const iconWords = [
{ icon: 'business', words: ['office', 'work'] },
{ icon: 'king_bed', words: ['bed'] },
{ icon: 'tv', words: ['lounge', 'tv'] },
{ icon: 'weekend', words: ['lounge', 'tv', 'sitting room'] },
{ icon: 'directions_car', words: ['garage', 'laundry'] },
{ icon: 'fitness_center', words: ['gym'] },
{ icon: 'emoji_food_beverage', words: ['kitchen'] },
{ icon: 'kitchen', words: ['kitchen'] },
{ icon: 'deck', words: ['deck', 'outside'] },
{ icon: 'restaurant_menu', words: ['dining', 'dinner'] },
{ icon: 'laptop', words: ['laptop'] },
{ icon: 'bug_report', words: ['test', 'debug'] },
{ icon: 'child_care', words: ['kids', 'baby'] },
{ icon: 'smartphone', words: ['phone', 'mobile'] },
];
const name = group.name.toLowerCase();
for (let item of iconWords) {
@ -124,7 +133,7 @@ const SnapcastGroups = (props) => {
}
return (
<Link
className="snapcast__groups__menu-item menu-item"
className={`snapcast__groups__menu-item menu-item${anyClients ? ' menu-item--no-clients' : ''}`}
activeClassName="menu-item--active"
key={group.id}
history={history}

View File

@ -17,7 +17,7 @@ class Stream extends React.Component {
}
static getDerivedStateFromProps({ play_state, current_track: { uri } = {} }, state) {
if (uri === state.uri && play_state === state.play_state) return null;
if (uri && uri === state.uri && play_state === state.play_state) return null;
return {
...state,
uri,
@ -32,22 +32,26 @@ class Stream extends React.Component {
enabled,
volume,
url,
current_track: { uri } = {},
} = this.props;
const { cachebuster } = this.state;
if (!enabled || !url) return null;
if (!uri || !enabled || !url) return null;
console.debug(`Attempting to play stream ${url}?cb=${cachebuster}`);
return (
<ReactHowler
src={`${url}?cb=${cachebuster}`}
playing={play_state === 'playing'}
volume={volume / 100}
onPlay={() => console.debug(`Playing stream ${url}?cb=${cachebuster}`)}
onLoad={() => console.debug(`Loaded stream ${url}?cb=${cachebuster}`)}
html5
onError={() => console.error(`Failed to play stream ${url}?cb=${cachebuster}`)}
/>
);
}
};
}
const mapStateToProps = (state) => ({
current_track: state.core.current_track || {},

View File

@ -236,6 +236,10 @@ export default class Track extends React.Component {
var link = <span>Radio</span>;
break;
case 'queue-history':
var link = <span>Queue history</span>;
break;
default:
var link = <URILink type={type} uri={track.added_from}>{titleCase(type)}</URILink>;
}