Listening to client (dis)connections and auto-updating Snapcast group availability
This commit is contained in:
@ -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 : {}),
|
||||
|
||||
@ -11,7 +11,7 @@ export default ({
|
||||
retainScroll,
|
||||
scrollTo: scrollToProp,
|
||||
onContextMenu,
|
||||
className,
|
||||
className = '',
|
||||
activeClassName,
|
||||
to,
|
||||
exact,
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 || {},
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
@ -85,11 +85,11 @@ const SnapcastMiddleware = (function () {
|
||||
break;
|
||||
|
||||
case 'Client.OnLatencyChanged':
|
||||
store.dispatch(snapcastActions.clientLoaded(message.params));
|
||||
store.dispatch(snapcastActions.clientLoaded(message.params));
|
||||
break;
|
||||
|
||||
case 'Client.OnNameChanged':
|
||||
store.dispatch(snapcastActions.clientLoaded(message.params));
|
||||
store.dispatch(snapcastActions.clientLoaded(message.params));
|
||||
break;
|
||||
|
||||
case 'Group.OnMute':
|
||||
@ -99,6 +99,9 @@ const SnapcastMiddleware = (function () {
|
||||
case 'Server.OnUpdate':
|
||||
store.dispatch(snapcastActions.serverLoaded(message.param));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -60,6 +60,7 @@ class QueueHistory extends React.Component {
|
||||
</Header>
|
||||
<section className="content-wrapper">
|
||||
<TrackList
|
||||
uri="iris:queue-history"
|
||||
className="queue-history-track-list"
|
||||
track_context="history"
|
||||
tracks={tracks}
|
||||
|
||||
Reference in New Issue
Block a user