WIP: Snapcast

This commit is contained in:
James Barnsley
2019-10-25 15:47:25 +13:00
parent b75c2a67f8
commit 88a5efaa05
10 changed files with 10442 additions and 5607 deletions

View File

@ -20,7 +20,7 @@ import * as spotifyActions from '../services/spotify/actions';
import * as lastfmActions from '../services/lastfm/actions';
import * as geniusActions from '../services/genius/actions';
class Services extends React.Component {
class Services extends React.Component {
constructor(props) {
super(props);
this.state = {
@ -40,7 +40,7 @@ class Services extends React.Component {
if (this.props.genius.authorization && (!this.props.genius.me || this.props.core.users[`genius:user:${this.props.genius.me.id}`] === undefined)) {
this.props.geniusActions.getMe();
}
}
}
componentWillReceiveProps(newProps) {
let changed = false;
@ -96,10 +96,9 @@ class Services extends React.Component {
if (!this.props.mopidy.uri_schemes || !this.props.mopidy.uri_schemes.includes('spotify:')) {
not_installed = (
<div>
<p className="message warning">
<em>Mopidy-Spotify</em>
{' '}
extension is not running - you will not be able to play any Spotify tracks
<p className="message warning">
<em>Mopidy-Spotify</em>
{' extension is not running - you will not be able to play any Spotify tracks'}
</p>
<br />
</div>
@ -117,17 +116,14 @@ extension is not running - you will not be able to play any Spotify tracks
onChange={(e) => this.setState({ country: e.target.value })}
onFocus={(e) => this.setState({ input_in_focus: 'country' })}
onBlur={(e) => this.props.spotifyActions.set({ country: e.target.value })}
value={this.state.country}
value={this.state.country}
/>
<div className="description">
An
{' '}
<a href="http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2" target="_blank">ISO 3166-1 alpha-2</a>
{' '}
country code (eg
{' '}
<em>NZ</em>
)
{'An '}
<a href="http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2" target="_blank">ISO 3166-1 alpha-2</a>
{' country code (eg '}
<em>NZ</em>
{')'}
</div>
</div>
</label>
@ -139,20 +135,16 @@ country code (eg
onChange={(e) => this.setState({ locale: e.target.value })}
onFocus={(e) => this.setState({ input_in_focus: 'locale' })}
onBlur={(e) => this.props.spotifyActions.set({ locale: e.target.value })}
value={this.state.locale}
value={this.state.locale}
/>
<div className="description">
Lowercase
{' '}
<a href="http://en.wikipedia.org/wiki/ISO_639" target="_blank">ISO 639 language code</a>
{' '}
and an uppercase
{' '}
<a href="http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2" target="_blank">ISO 3166-1 alpha-2 country code</a>
, joined by an underscore (eg
{' '}
<em>en_NZ</em>
)
{'Lowercase '}
<a href="http://en.wikipedia.org/wiki/ISO_639" target="_blank">ISO 639 language code</a>
{' and an uppercase '}
<a href="http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2" target="_blank">ISO 3166-1 alpha-2 country code</a>
{', joined by an underscore (eg'}
<em>en_NZ</em>
{')'}
</div>
</div>
</label>
@ -170,7 +162,16 @@ and an uppercase
<div className="name">Authorization</div>
<div className="input">
<SpotifyAuthenticationFrame />
{this.props.spotify.refreshing_token ? <a className="button button--working">Refreshing...</a> : <a className="button button--default" onClick={(e) => this.props.spotifyActions.refreshingToken()}>Force token refresh</a>}
{this.props.spotify.refreshing_token ? (
<a className="button button--working">Refreshing...</a>
) : (
<a
className="button button--default"
onClick={(e) => this.props.spotifyActions.refreshingToken()}
>
Force token refresh
</a>
)}
</div>
</div>
</div>
@ -201,7 +202,7 @@ and an uppercase
return (
<div>
{this.props.lastfm.authorization ? (
{this.props.lastfm.authorization ? (
<div className="field current-user">
<div className="name">Current user</div>
<div className="input">
@ -209,7 +210,7 @@ and an uppercase
{user}
</div>
</div>
</div>
</div>
) : null}
<div className="field">
@ -246,7 +247,7 @@ and an uppercase
return (
<div>
{this.props.genius.authorization ? (
{this.props.genius.authorization ? (
<div className="field current-user">
<div className="name">Current user</div>
<div className="input">
@ -254,7 +255,7 @@ and an uppercase
{user}
</div>
</div>
</div>
</div>
) : null}
<div className="field">
@ -278,7 +279,7 @@ and an uppercase
type="checkbox"
name="ssl"
checked={this.props.core.http_streaming_enabled}
onChange={(e) => this.props.coreActions.set({ http_streaming_enabled: !this.props.core.http_streaming_enabled })}
onChange={(e) => this.props.coreActions.set({ http_streaming_enabled: !this.props.core.http_streaming_enabled })}
/>
<span className="label">
Stream audio to this browser
@ -292,7 +293,7 @@ and an uppercase
<input
type="text"
onChange={(e) => this.props.coreActions.set({ http_streaming_url: e.target.value })}
value={this.props.core.http_streaming_url}
value={this.props.core.http_streaming_url}
/>
<div className="description">
The full URL to your stream endpoint
@ -303,7 +304,7 @@ and an uppercase
);
}
renderMenu() {
renderMenu() {
if (this.props.spotify.me && this.props.core.users[this.props.spotify.me.uri]) {
var spotify_icon = <Thumbnail className="menu-item__thumbnail" circle size="small" images={this.props.core.users[this.props.spotify.me.uri].images} />;
} else {
@ -402,7 +403,7 @@ and an uppercase
</div>
);
}
return null;
return null;
}
render() {

View File

@ -2,22 +2,19 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Link from './Link';
import VolumeControl from './Fields/VolumeControl';
import MuteControl from './Fields/MuteControl';
import LatencyControl from './Fields/LatencyControl';
import TextField from './Fields/TextField';
import DropdownField from './Fields/DropdownField';
import Icon from './Icon';
import * as helpers from '../helpers';
import * as coreActions from '../services/core/actions';
import * as uiActions from '../services/ui/actions';
import * as pusherActions from '../services/pusher/actions';
import * as snapcastActions from '../services/snapcast/actions';
class Snapcast extends React.Component {
class Snapcast extends React.Component {
constructor(props) {
super(props);
@ -27,21 +24,16 @@ class Snapcast extends React.Component {
}
componentDidMount() {
if (this.props.pusher_connected && this.props.snapcast_enabled) {
this.props.snapcastActions.getServer();
const { connected, actions: snapcastActions } = this.props;
if (this.props.connected) {
actions.getServer();
}
}
componentWillReceiveProps(newProps) {
// Just connected
if (newProps.snapcast_enabled && !this.props.pusher_connected && newProps.pusher_connected) {
this.props.snapcastActions.getServer();
// Just enabled
// This is the more probable scenario as we don't know if we're enabled until pusher connects
// and then gets the config from the server
} else if (!this.props.snapcast_enabled && newProps.snapcast_enabled && newProps.pusher_connected) {
this.props.snapcastActions.getServer();
componentWillReceiveProps(newProps) {
const { connected, actions: snapcastActions } = this.props;
if (newProps.connected && !connected) {
actions.getServer();
}
}
@ -58,7 +50,11 @@ class Snapcast extends React.Component {
this.setState({ clients_expanded });
}
renderClientsList(group, groups) {
renderClientsList(group, groups) {
const {
snapcastActions: actions,
} = this.props;
if (!this.props.show_disconnected_clients && group.clients) {
var clients = helpers.applyFilter('connected', true, group.clients);
} else {
@ -101,8 +97,8 @@ class Snapcast extends React.Component {
</div>
<div className="input">
<TextField
onChange={(value) => this.props.snapcastActions.setClientName(client.id, value)}
value={client.name}
onChange={(value) => actions.setClientName(client.id, value)}
value={client.name}
/>
</div>
</label>
@ -111,7 +107,7 @@ class Snapcast extends React.Component {
Group
</div>
<div className="input">
<select onChange={(e) => this.props.snapcastActions.setClientGroup(client.id, e.target.value)} value={group.id}>
<select onChange={(e) => actions.setClientGroup(client.id, e.target.value)} value={group.id}>
{
groups.map((group) => (
<option value={group.id} key={group.id}>
@ -133,12 +129,12 @@ class Snapcast extends React.Component {
<MuteControl
className="snapcast__client__mute-control"
mute={client.mute}
onMuteChange={(mute) => this.props.snapcastActions.setClientMute(client.id, mute)}
onMuteChange={(mute) => actions.setClientMute(client.id, mute)}
/>
<VolumeControl
className="snapcast__client__volume-control"
volume={client.volume}
onVolumeChange={(percent) => this.props.snapcastActions.setClientVolume(client.id, percent)}
onVolumeChange={(percent) => actions.setClientVolume(client.id, percent)}
/>
</div>
</div>
@ -150,13 +146,13 @@ class Snapcast extends React.Component {
<LatencyControl
max="150"
value={client.latency}
onChange={(value) => this.props.snapcastActions.setClientLatency(client.id, parseInt(value))}
onChange={(value) => actions.setClientLatency(client.id, parseInt(value))}
/>
<TextField
className="tiny"
type="number"
onChange={(value) => this.props.snapcastActions.setClientLatency(client.id, parseInt(value))}
value={String(client.latency)}
onChange={(value) => actions.setClientLatency(client.id, parseInt(value))}
value={String(client.latency)}
/>
</div>
</div>
@ -174,7 +170,7 @@ class Snapcast extends React.Component {
</div>
</div>
</div>
);
);
})
}
</div>
@ -182,16 +178,11 @@ class Snapcast extends React.Component {
}
render() {
if (!this.props.snapcast_enabled) {
return (
<p className="message warning">
To enable Snapcast, edit your
<code>mopidy.conf</code>
{' '}
file
</p>
);
}
const {
enabled,
snapcastActions: actions,
uiActions,
} = this.props;
const streams = [];
for (var id in this.props.streams) {
@ -211,22 +202,24 @@ file
<div className="snapcast">
<div className="field checkbox">
<div className="name">
Display
</div>
<div className="input">
<button
className="button button--default button--small"
onClick={(e) => this.props.snapcastActions.getServer()}
>
Refresh
</button>
<label>
<input
type="checkbox"
name="enabled"
checked={enabled}
onChange={() => actions.setEnabled(!enabled)}
/>
<span className="label">
Enabled
</span>
</label>
<label>
<input
type="checkbox"
name="show_disconnected_clients"
checked={this.props.show_disconnected_clients}
onChange={(e) => this.props.uiActions.set({ snapcast_show_disconnected_clients: !this.props.show_disconnected_clients })}
onChange={() => uiActions.set({ snapcast_show_disconnected_clients: !this.props.show_disconnected_clients })}
/>
<span className="label">
Show disconnected clients
@ -237,7 +230,7 @@ file
<div className="snapcast__groups">
{
groups.map((group) => {
groups.map((group) => {
group = helpers.collate(group, { clients: this.props.clients });
// Average our clients' volume for an overall group volume
@ -256,13 +249,13 @@ file
</div>
<div className="input">
<div className="text">
{group.name}
{' '}
{group.name}
{' '}
&nbsp;
<span className="mid_grey-text">
(
{group.id}
)
<span className="mid_grey-text">
(
{group.id}
)
</span>
</div>
</div>
@ -272,14 +265,14 @@ file
Stream
</div>
<div className="input">
<select onChange={(e) => this.props.snapcastActions.setGroupStream(group.id, e.target.value)} value={group.stream_id}>
<select onChange={(e) => actions.setGroupStream(group.id, e.target.value)} value={group.stream_id}>
{
streams.map((stream) => (
<option value={stream.id} key={stream.id}>
{stream.id}
{' '}
(
{stream.status}
{stream.id}
{' '}
(
{stream.status}
)
</option>
))
@ -295,12 +288,12 @@ file
<MuteControl
className="snapcast__group__mute-control"
mute={group.muted}
onMuteChange={(mute) => this.props.snapcastActions.setGroupMute(group.id, mute)}
onMuteChange={(mute) => actions.setGroupMute(group.id, mute)}
/>
<VolumeControl
className="snapcast__group__volume-control"
volume={group_volume}
onVolumeChange={(percent, old_percent) => this.props.snapcastActions.setGroupVolume(group.id, percent, old_percent)}
onVolumeChange={(percent, old_percent) => actions.setGroupVolume(group.id, percent, old_percent)}
/>
</div>
</div>
@ -323,9 +316,13 @@ file
}
const mapStateToProps = (state, ownProps) => ({
snapcast_enabled: state.pusher.config.snapcast_enabled,
pusher_connected: state.pusher.connected,
show_disconnected_clients: (state.ui.snapcast_show_disconnected_clients !== undefined ? state.ui.snapcast_show_disconnected_clients : false),
enabled: state.snapcast.enabled,
connected: state.snapcast.connected,
show_disconnected_clients: (
state.ui.snapcast_show_disconnected_clients !== undefined
? state.ui.snapcast_show_disconnected_clients
: false
),
streams: (state.snapcast.streams ? state.snapcast.streams : null),
groups: (state.snapcast.groups ? state.snapcast.groups : null),
clients: (state.snapcast.clients ? state.snapcast.clients : null),
@ -335,7 +332,6 @@ const mapDispatchToProps = (dispatch) => ({
coreActions: bindActionCreators(coreActions, dispatch),
uiActions: bindActionCreators(uiActions, dispatch),
snapcastActions: bindActionCreators(snapcastActions, dispatch),
pusherActions: bindActionCreators(pusherActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Snapcast);
export default connect(mapStateToProps, mapDispatchToProps)(Snapcast);