Snapcast groups revamp

This commit is contained in:
James Barnsley
2019-11-05 16:05:49 +13:00
parent 6e70c364d7
commit 9190950f56
18 changed files with 1402 additions and 1195 deletions

View File

@ -385,33 +385,21 @@ class Services extends React.Component {
}
renderService() {
let service = null;
switch (this.props.match.params.service) {
const { match } = this.props;
switch (match.params.service) {
case 'spotify':
service = this.renderSpotify();
break;
return <div className="service">{this.renderSpotify()}</div>;
case 'lastfm':
service = this.renderLastfm();
break;
return <div className="service">{this.renderLastfm()}</div>;
case 'genius':
service = this.renderGenius();
break;
return <div className="service">{this.renderGenius()}</div>;
case 'icecast':
service = this.renderIcecast();
break;
return <div className="service">{this.renderIcecast()}</div>;
case 'snapcast':
service = <Snapcast />;
break;
return <div className="service">{<Snapcast match={this.props.match} />}</div>;
default:
return null;
}
if (service) {
return (
<div className="service">
{service}
</div>
);
}
return null;
}
render() {

View File

@ -18,7 +18,14 @@ class Sidebar extends React.Component {
}
renderStatusIcon() {
if (this.props.update_available) {
const {
update_available,
mopidy_connected,
pusher_connected,
snapcast_connected,
snapcast_enabled,
} = this.props;
if (update_available) {
return (
<span className="status tooltip tooltip--right">
<Icon name="cloud_download" className="green-text" />
@ -38,13 +45,14 @@ class Sidebar extends React.Component {
);
}
if (!this.props.mopidy_connected || !this.props.pusher_connected) {
if (!mopidy_connected || !pusher_connected || (!snapcast_connected && snapcast_enabled)) {
return (
<span className="status tooltip tooltip--right">
<Icon name="warning" className="red-text" />
<span className="tooltip__content">
{!this.props.mopidy_connected && (<span>Mopidy not connected<br /></span>)}
{!this.props.pusher_connected && (<span>Pusher not connected<br /></span>)}
{!mopidy_connected && (<span>Mopidy not connected<br /></span>)}
{!pusher_connected && (<span>Pusher not connected<br /></span>)}
{!snapcast_connected && snapcast_enabled && (<span>Snapcast not connected<br /></span>)}
</span>
</span>
);
@ -54,70 +62,76 @@ class Sidebar extends React.Component {
}
render() {
const {
history,
spotify_enabled,
uiActions,
} = this.props;
return (
<aside className="sidebar">
<div className="sidebar__liner">
<nav className="sidebar__menu">
<section className="sidebar__menu__section">
<Link to="/queue" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Link to="/queue" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="play_arrow" type="material" />
Now playing
</Link>
<Link to="/search" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Link to="/search" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="search" type="material" />
Search
</Link>
</section>
{this.props.spotify_enabled ? (
{spotify_enabled && (
<section className="sidebar__menu__section">
<title className="sidebar__menu__section__title">Discover</title>
<Link to="/discover/recommendations" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Link to="/discover/recommendations" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="explore" type="material" />
Discover
</Link>
<Link to="/discover/categories" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Link to="/discover/categories" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="mood" type="material" />
Genre / Mood
</Link>
<Link to="/discover/featured" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Link to="/discover/featured" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="star" type="material" />
Featured playlists
</Link>
<Link to="/discover/new-releases" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Link to="/discover/new-releases" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="new_releases" type="material" />
New releases
</Link>
</section>
) : null}
)}
<section className="sidebar__menu__section">
<title className="sidebar__menu__section__title">My Music</title>
<Link to="/library/playlists" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Link to="/library/playlists" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="queue_music" type="material" />
Playlists
</Link>
<Link to="/library/artists" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Link to="/library/artists" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="recent_actors" type="material" />
Artists
</Link>
<Link to="/library/albums" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Link to="/library/albums" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="album" type="material" />
Albums
</Link>
<Link to="/library/tracks" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Link to="/library/tracks" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="music_note" type="material" />
Tracks
</Link>
<Link to="/library/browse" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Link to="/library/browse" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="folder" type="material" />
Browse
</Link>
</section>
<section className="sidebar__menu__section">
<Link to="/settings" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Link to="/settings" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="settings" type="material" />
Settings
{this.renderStatusIcon()}
@ -129,7 +143,7 @@ class Sidebar extends React.Component {
<Dropzones />
<div className="close" onClick={(e) => this.props.uiActions.toggleSidebar(false)}>
<div className="close" onClick={(e) => uiActions.toggleSidebar(false)}>
<Icon name="close" />
</div>
@ -143,6 +157,8 @@ const mapStateToProps = (state, ownProps) => ({
pusher_connected: state.pusher.connected,
spotify_enabled: state.spotify.enabled,
spotify_authorized: state.spotify.authorization,
snapcast_connected: state.snapcast.connected,
snapcast_enabled: state.snapcast.enabled,
update_available: (state.pusher.version && state.pusher.version.update_available ? state.pusher.version.update_available : false),
test_mode: (state.ui.test_mode ? state.ui.test_mode : false),
dragger: state.ui.dragger,

View File

@ -3,324 +3,83 @@ import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import VolumeControl from './Fields/VolumeControl';
import MuteControl from './Fields/MuteControl';
import LatencyControl from './Fields/LatencyControl';
import TextField from './Fields/TextField';
import Icon from './Icon';
import SnapcastGroups from './SnapcastGroups';
import * as helpers from '../helpers';
import * as coreActions from '../services/core/actions';
import * as uiActions from '../services/ui/actions';
import * as snapcastActions from '../services/snapcast/actions';
import * as actions from '../services/snapcast/actions';
class Snapcast extends React.Component {
constructor(props) {
super(props);
const Snapcast = (props) => {
const {
actions,
show_disconnected_clients,
uiActions,
match,
history,
snapcast: {
host,
port,
enabled,
connected,
},
} = props;
this.state = {
clients_expanded: [],
};
}
return (
<div className="snapcast">
toggleClientExpanded(client_id) {
const { clients_expanded } = this.state;
const index = clients_expanded.indexOf(client_id);
if (index >= 0) {
clients_expanded.splice(index, 1);
} else {
clients_expanded.push(client_id);
}
this.setState({ clients_expanded });
}
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 {
var { clients } = group;
}
if (!clients || clients.length <= 0) {
return (
<div className="text mid_grey-text">
No clients
</div>
);
}
return (
<div className="list snapcast__clients">
{
clients.map((client) => {
let class_name = 'list__item list__item--no-interaction snapcast__client';
if (client.connected) {
class_name += ' snapcast__client--connected';
} else {
class_name += ' snapcast__client--disconnected';
}
if (this.state.clients_expanded.includes(client.id)) {
return (
<div className={`${class_name} snapcast__client--expanded`} key={client.id}>
<div className="snapcast__client__header" onClick={(e) => this.toggleClientExpanded(client.id)}>
{client.name}
<div className="snapcast__client__header__icons">
{!client.connected ? <Icon name="power_off" className="disconnected" /> : null}
<Icon name="expand_less" />
</div>
</div>
<div className="snapcast__client__details">
<label className="field">
<div className="name">
Name
</div>
<div className="input">
<TextField
onChange={(value) => actions.setClientName(client.id, value)}
value={client.name}
/>
</div>
</label>
<label className="field dropdown">
<div className="name">
Group
</div>
<div className="input">
<select onChange={(e) => actions.setClientGroup(client.id, e.target.value)} value={group.id}>
{
groups.map((group) => (
<option value={group.id} key={group.id}>
{group.name ? group.name : `Group ${group.id.substring(0, 3)}`}
</option>
))
}
<option value={group.id}>
New group
</option>
</select>
</div>
</label>
<div className="snapcast__client__volume field">
<div className="name">
Volume
</div>
<div className="input">
<MuteControl
className="snapcast__client__mute-control"
mute={client.mute}
onMuteChange={(mute) => actions.setClientMute(client.id, mute)}
/>
<VolumeControl
className="snapcast__client__volume-control"
volume={client.volume}
onVolumeChange={(percent) => actions.setClientVolume(client.id, percent)}
/>
</div>
</div>
<div className="snapcast__client__latency field">
<div className="name">
Latency
</div>
<div className="input">
<LatencyControl
max="150"
value={client.latency}
onChange={(value) => actions.setClientLatency(client.id, parseInt(value))}
/>
<TextField
className="tiny"
type="number"
onChange={(value) => actions.setClientLatency(client.id, parseInt(value))}
value={String(client.latency)}
/>
</div>
</div>
</div>
</div>
);
}
return (
<div className={`${class_name} snapcast__client--collapsed`} key={client.id}>
<div className="snapcast__client__header" onClick={(e) => this.toggleClientExpanded(client.id)}>
{client.name}
<div className="snapcast__client__header__icons">
{!client.connected ? <Icon name="power_off" className="disconnected" /> : null}
<Icon name="expand_more" />
</div>
</div>
</div>
);
})
}
</div>
);
}
render() {
const {
snapcastActions: actions,
uiActions,
snapcast: {
host,
port,
enabled,
connected,
},
} = this.props;
const streams = [];
for (var id in this.props.streams) {
if (this.props.streams.hasOwnProperty(id)) {
streams.push(this.props.streams[id]);
}
}
const groups = [];
for (var id in this.props.groups) {
if (this.props.groups.hasOwnProperty(id)) {
groups.push(this.props.groups[id]);
}
}
return (
<div className="snapcast">
<div className="field checkbox">
<div className="name">Enabled</div>
<div className="input">
<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={() => uiActions.set({ snapcast_show_disconnected_clients: !this.props.show_disconnected_clients })}
/>
<span className="label">
Show disconnected clients
</span>
</label>
</div>
</div>
<div className="field">
<div className="name">Host</div>
<div className="input">
<TextField
value={host}
onChange={value => actions.set({ host: value })}
<div className="field checkbox">
<div className="name">Enabled</div>
<div className="input">
<label>
<input
type="checkbox"
name="enabled"
checked={enabled}
onChange={() => actions.setEnabled(!enabled)}
/>
</div>
</div>
<div className="field">
<div className="name">Port</div>
<div className="input">
<TextField
value={port}
onChange={value => actions.set({ port: value })}
<span className="label">
Enabled
</span>
</label>
<label>
<input
type="checkbox"
name="show_disconnected_clients"
checked={show_disconnected_clients}
onChange={() => uiActions.set({ snapcast_show_disconnected_clients: !show_disconnected_clients })}
/>
</div>
<span className="label">
Show disconnected clients
</span>
</label>
</div>
{connected && enabled && (
<div className="snapcast__groups">
{
groups.map((group) => {
group = helpers.collate(group, { clients: this.props.clients });
// Average our clients' volume for an overall group volume
let group_volume = 0;
for (let i = 0; i < group.clients.length; i++) {
const client = group.clients[i];
group_volume += client.volume;
}
group_volume /= group.clients.length;
return (
<div className="snapcast__group" key={group.id}>
<div className="field text">
<div className="name">
Name
</div>
<div className="input">
<TextField
value={group.name}
onChange={value => actions.setGroupName(group.id, value)}
/>
</div>
</div>
<div className="field dropdown">
<div className="name">
Stream
</div>
<div className="input">
<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}
)
</option>
))
}
</select>
</div>
</div>
<div className="field">
<div className="name">
Volume
</div>
<div className="input">
<MuteControl
className="snapcast__group__mute-control"
mute={group.muted}
onMuteChange={(mute) => actions.setGroupMute(group.id, mute)}
/>
<VolumeControl
className="snapcast__group__volume-control"
volume={group_volume}
onVolumeChange={(percent, previousPercent) => actions.setGroupVolume(group.id, percent, previousPercent)}
/>
</div>
</div>
<div className="field">
<div className="name">
Clients
</div>
<div className="input">
{this.renderClientsList(group, groups)}
</div>
</div>
</div>
);
})
}
</div>
)}
</div>
);
}
<div className="field">
<div className="name">Host</div>
<div className="input">
<TextField
value={host}
onChange={value => actions.setConnection({ host: value })}
/>
</div>
</div>
<div className="field">
<div className="name">Port</div>
<div className="input">
<TextField
value={port}
onChange={value => actions.setConnection({ port: value })}
/>
</div>
</div>
{connected && enabled && <SnapcastGroups match={match} history={history} />}
</div>
);
}
const mapStateToProps = (state, ownProps) => ({
@ -330,15 +89,11 @@ const mapStateToProps = (state, ownProps) => ({
? 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),
});
const mapDispatchToProps = (dispatch) => ({
coreActions: bindActionCreators(coreActions, dispatch),
actions: bindActionCreators(actions, dispatch),
uiActions: bindActionCreators(uiActions, dispatch),
snapcastActions: bindActionCreators(snapcastActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Snapcast);

View File

@ -0,0 +1,117 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import VolumeControl from './Fields/VolumeControl';
import MuteControl from './Fields/MuteControl';
import LatencyControl from './Fields/LatencyControl';
import TextField from './Fields/TextField';
import Icon from './Icon';
import * as helpers from '../helpers';
const SnapcastClients = ({ actions, group, groups, show_disconnected_clients }) => {
if (!show_disconnected_clients && group.clients) {
var clients = helpers.applyFilter('connected', true, group.clients);
} else {
var { clients } = group;
}
if (!clients || clients.length <= 0) {
return (
<div className="text mid_grey-text">
No clients
</div>
);
}
return (
<div className="list snapcast__clients">
{
clients.map((client) => {
let class_name = 'list__item list__item--no-interaction snapcast__client';
if (client.connected) {
class_name += ' snapcast__client--connected';
} else {
class_name += ' snapcast__client--disconnected';
}
return (
<div className={class_name} key={client.id}>
<div className="snapcast__client__details">
<label className="field">
<div className="name">
Name
</div>
<div className="input">
<TextField
onChange={(value) => actions.setClientName(client.id, value)}
value={client.name}
/>
</div>
</label>
<label className="field dropdown">
<div className="name">
Group
</div>
<div className="input">
<select onChange={(e) => actions.setClientGroup(client.id, e.target.value)} value={group.id}>
{
groups.map((group) => (
<option value={group.id} key={group.id}>
{group.name ? group.name : `Group ${group.id.substring(0, 3)}`}
</option>
))
}
<option value={group.id}>
New group
</option>
</select>
</div>
</label>
<div className="snapcast__client__volume field">
<div className="name">
Volume
</div>
<div className="input">
<MuteControl
className="snapcast__mute-control snapcast__client__mute-control"
mute={client.mute}
onMuteChange={(mute) => actions.setClientMute(client.id, mute)}
/>
<VolumeControl
className="snapcast__volume-control snapcast__client__volume-control"
volume={client.volume}
onVolumeChange={(percent) => actions.setClientVolume(client.id, percent)}
/>
</div>
</div>
<div className="snapcast__client__latency field">
<div className="name">
Latency
</div>
<div className="input">
<LatencyControl
max="150"
value={client.latency}
onChange={(value) => actions.setClientLatency(client.id, parseInt(value))}
/>
<TextField
className="tiny"
type="number"
onChange={(value) => actions.setClientLatency(client.id, parseInt(value))}
value={String(client.latency)}
/>
</div>
</div>
</div>
</div>
);
})
}
</div>
);
}
export default SnapcastClients;

View File

@ -0,0 +1,169 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import VolumeControl from './Fields/VolumeControl';
import MuteControl from './Fields/MuteControl';
import TextField from './Fields/TextField';
import SnapcastClients from './SnapcastClients';
import Icon from './Icon';
import Link from './Link';
import * as helpers from '../helpers';
import * as actions from '../services/snapcast/actions';
const SnapcastGroups = (props) => {
const {
actions,
show_disconnected_clients,
streams,
groups,
clients,
history,
match: { params: { id: groupId } },
} = props;
const streamsArray = Object.keys(streams).map((id) => streams[id]);
const groupsArray = Object.keys(groups).map((id) => groups[id]);
if (!groups || groupsArray.length <= 0) {
return null;
}
const renderGroup = () => {
if (!groupId || !groups[groupId]) {
return null;
}
const group = helpers.collate(groups[groupId], { clients });
// Average our clients' volume for an overall group volume
let group_volume = 0;
for (let i = 0; i < group.clients.length; i++) {
const client = group.clients[i];
group_volume += client.volume;
}
group_volume /= group.clients.length;
return (
<div className="snapcast__group" key={group.id}>
<div className="field text">
<div className="name">
Name
</div>
<div className="input">
<TextField
value={group.name}
onChange={value => actions.setGroupName(group.id, value)}
/>
</div>
</div>
<div className="field dropdown">
<div className="name">
Stream
</div>
<div className="input">
<select onChange={(e) => actions.setGroupStream(group.id, e.target.value)} value={group.stream_id}>
{
streamsArray.map((stream) => (
<option value={stream.id} key={stream.id}>
{stream.id}
{' '}
(
{stream.status}
)
</option>
))
}
</select>
</div>
</div>
<div className="field">
<div className="name">
Volume
</div>
<div className="input">
<MuteControl
className="snapcast__group__mute-control"
mute={group.muted}
onMuteChange={(mute) => actions.setGroupMute(group.id, mute)}
/>
<VolumeControl
className="snapcast__group__volume-control"
volume={group_volume}
onVolumeChange={(percent, previousPercent) => actions.setGroupVolume(group.id, percent, previousPercent)}
/>
</div>
</div>
<div className="field">
<div className="name">
Clients
</div>
<div className="input">
<SnapcastClients
group={group}
groups={groupsArray}
actions={actions}
show_disconnected_clients={show_disconnected_clients}
/>
</div>
</div>
</div>
);
}
const renderMenuItem = (group) => {
return (
<Link
className="snapcast__groups__menu-item menu-item"
activeClassName="menu-item--active"
key={group.id}
history={history}
to={`/settings/snapcast/${group.id}`}
scrollTo="#services-snapcast-groups"
>
<div className="snapcast__groups__menu-item__inner menu-item__inner">
<Icon className="menu-item__icon" name="wifi_tethering" />
<div className="menu-item__title">
{group.name}
</div>
<MuteControl
className="snapcast__mute-control snapcast__group__menu-item__mute-control"
mute={group.mute}
onMuteChange={(mute) => actions.setGroupMute(group.id, mute)}
/>
</div>
</Link>
);
}
return (
<div className="snapcast__groups">
<a name="services-snapcast-groups" />
<div className="snapcast__groups__menu menu">
<div className="menu__inner">
{groupsArray.map((group) => renderMenuItem(group))}
</div>
</div>
{renderGroup()}
</div>
);
}
const mapStateToProps = (state, ownProps) => ({
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),
});
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(SnapcastGroups);

View File

@ -8,6 +8,13 @@ export function set(data) {
};
}
export function setConnection(data) {
return {
type: 'MOPIDY_SET_CONNECTION',
data,
};
}
export function request(method, params = {}, response_callback = null, error_callback = null) {
return {
type: 'MOPIDY_REQUEST',

View File

@ -224,7 +224,7 @@ const MopidyMiddleware = (function () {
switch (action.type) {
case 'MOPIDY_CONNECT':
if (socket != null) {
socket.close();
socket.close();
}
store.dispatch({ type: 'MOPIDY_CONNECTING' });
@ -240,10 +240,12 @@ const MopidyMiddleware = (function () {
case 'MOPIDY_CONNECTED':
if (store.getState().ui.allow_reporting) {
const hashed_hostname = sha256(window.location.hostname);
ReactGA.event({ category: 'Mopidy', action: 'Connected', label: hashed_hostname });
}
store.dispatch(uiActions.createNotification({ content: 'Mopidy connected' }));
ReactGA.event({
category: 'Mopidy',
action: 'Connected',
label: sha256(window.location.hostname),
});
}
next(action);
break;
@ -254,7 +256,6 @@ const MopidyMiddleware = (function () {
break;
case 'MOPIDY_DISCONNECTED':
store.dispatch(uiActions.createNotification({ type: 'bad', content: 'Mopidy disconnected' }));
helpers.setFavicon('favicon_error.png');
break;
@ -265,6 +266,19 @@ const MopidyMiddleware = (function () {
});
break;
case 'MOPIDY_SET_CONNECTION':
store.dispatch(mopidyActions.set(action.data));
// Wait 250 ms and then retry connection
setTimeout(
() => {
store.dispatch(mopidyActions.connect());
store.dispatch(pusherActions.connect());
},
250,
);
break;
case 'SET_WINDOW_FOCUS':
// Focus has just been regained
@ -284,21 +298,21 @@ const MopidyMiddleware = (function () {
request(socket, store, action.method, action.params)
.then(
(response) => {
if (action.response_callback) {
store.dispatch(action.response_callback.call(this, response));
}
if (action.response_callback) {
store.dispatch(action.response_callback.call(this, response));
}
},
(error) => {
if (action.error_callback) {
store.dispatch(action.error_callback.call(this, error));
} else {
store.dispatch(coreActions.handleException(
'Mopidy request failed',
error,
action.method,
action,
));
}
if (action.error_callback) {
store.dispatch(action.error_callback.call(this, error));
} else {
store.dispatch(coreActions.handleException(
'Mopidy request failed',
error,
action.method,
action,
));
}
},
);
break;

View File

@ -12,9 +12,12 @@ const spotifyActions = require('../spotify/actions');
const snapcastActions = require('../snapcast/actions');
const PusherMiddleware = (function () {
// container for the actual websocket
let socket = null;
let reconnectTimer = null;
// requests pending
const deferredRequests = [];
@ -183,7 +186,7 @@ const PusherMiddleware = (function () {
store.dispatch(uiActions.startLoading(id, `pusher_${method}`));
// Start our 30 second timeout
const timeout = setTimeout(
setTimeout(
() => {
store.dispatch(uiActions.stopLoading(id));
reject({
@ -203,7 +206,7 @@ const PusherMiddleware = (function () {
});
return (store) => (next) => (action) => {
const { pusher } = store.getState();
const { pusher } = store.getState();
switch (action.type) {
case 'PUSHER_CONNECT':
@ -211,12 +214,11 @@ const PusherMiddleware = (function () {
socket.close();
}
clearTimeout(reconnectTimer);
store.dispatch({ type: 'PUSHER_CONNECTING' });
var state = store.getState();
socket = new WebSocket(
`ws${window.location.protocol === 'https:' ? 's' : ''}://${state.mopidy.host}:${state.mopidy.port}/iris/ws/`,
`ws${window.location.protocol === 'https:' ? 's' : ''}://${store.getState().mopidy.host}:${store.getState().mopidy.port}/iris/ws/`,
);
socket.onopen = () => {
@ -231,7 +233,7 @@ const PusherMiddleware = (function () {
});
// attempt to reconnect every 5 seconds
setTimeout(() => {
reconnectTimer = setTimeout(() => {
store.dispatch(pusherActions.connect());
}, 5000);
};
@ -247,17 +249,17 @@ const PusherMiddleware = (function () {
};
socket.onmessage = (message) => {
var message = JSON.parse(message.data);
handleMessage(socket, store, message);
handleMessage(socket, store, JSON.parse(message.data));
};
break;
case 'PUSHER_CONNECTED':
if (store.getState().ui.allow_reporting) {
ReactGA.event({ category: 'Pusher', action: 'Connected', label: action.username });
}
ReactGA.event({ category: 'Pusher', action: 'Connected', label: action.username });
}
clearTimeout(reconnectTimer);
store.dispatch(pusherActions.updateConnection());
store.dispatch(pusherActions.getConfig());
store.dispatch(pusherActions.getRadio());
@ -280,21 +282,21 @@ const PusherMiddleware = (function () {
request(store, action.method, action.params)
.then(
(response) => {
if (action.response_callback) {
action.response_callback.call(this, response);
}
if (action.response_callback) {
action.response_callback.call(this, response);
}
},
(error) => {
if (action.error_callback) {
action.error_callback.call(this, error);
} else {
store.dispatch(coreActions.handleException(
'Pusher request failed',
error,
action.method,
action,
));
}
if (action.error_callback) {
action.error_callback.call(this, error);
} else {
store.dispatch(coreActions.handleException(
'Pusher request failed',
error,
action.method,
action,
));
}
},
);
break;

View File

@ -5,6 +5,14 @@ export function set(data) {
data,
};
}
export function setConnection(data) {
return {
type: 'SNAPCAST_SET_CONNECTION',
data,
};
}
export function setEnabled(enabled) {
return {
type: 'SNAPCAST_SET_ENABLED',

View File

@ -10,6 +10,7 @@ const snapcastActions = require('./actions');
const SnapcastMiddleware = (function () {
let socket = null;
let reconnectTimer = null;
// requests pending
const deferredRequests = [];
@ -141,9 +142,8 @@ const SnapcastMiddleware = (function () {
switch (action.type) {
case 'SNAPCAST_CONNECT':
if (socket) {
socket.close();
}
if (socket) socket.close();
clearTimeout(reconnectTimer);
store.dispatch({ type: 'SNAPCAST_CONNECTING' });
@ -164,7 +164,8 @@ const SnapcastMiddleware = (function () {
// attempt to reconnect every 5 seconds
if (store.getState().snapcast.enabled) {
setTimeout(() => {
clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => {
store.dispatch(snapcastActions.connect());
}, 5000);
}
@ -181,17 +182,18 @@ const SnapcastMiddleware = (function () {
};
socket.onmessage = (message) => {
var message = JSON.parse(message.data);
handleMessage(socket, store, message);
handleMessage(socket, store, JSON.parse(message.data));
};
break;
case 'SNAPCAST_CONNECTED':
if (store.getState().ui.allow_reporting) {
const hashed_hostname = sha256(window.location.hostname);
ReactGA.event({ category: 'Snapcast', action: 'Connected', label: hashed_hostname });
ReactGA.event({
category: 'Snapcast',
action: 'Connected',
label: sha256(window.location.hostname),
});
}
store.dispatch(uiActions.createNotification({ content: 'Snapcast connected' }));
store.dispatch(snapcastActions.getServer());
next(action);
break;
@ -199,11 +201,24 @@ const SnapcastMiddleware = (function () {
case 'SNAPCAST_DISCONNECT':
if (socket != null) socket.close();
socket = null;
clearTimeout(reconnectTimer);
break;
case 'SNAPCAST_DISCONNECTED':
case 'SNAPCAST_SET_CONNECTION':
store.dispatch(snapcastActions.serverLoaded({}));
store.dispatch(snapcastActions.clientsLoaded([]));
store.dispatch(snapcastActions.groupsLoaded([]));
store.dispatch(snapcastActions.streamsLoaded([]));
store.dispatch(snapcastActions.set(action.data));
// Wait 250 ms and then retry connection
if (store.getState().snapcast.enabled) {
store.dispatch(uiActions.createNotification({ type: 'bad', content: 'Snapcast disconnected' }));
setTimeout(
() => {
store.dispatch(snapcastActions.connect());
},
250,
);
}
break;

View File

@ -86,19 +86,6 @@ class Settings extends React.Component {
}
}
setConfig(e) {
this.setState({ input_in_focus: null });
e.preventDefault();
this.props.mopidyActions.set({
host: this.state.mopidy_host,
port: this.state.mopidy_port,
});
window.location.reload(true);
return false;
}
handleBlur(service, name, value) {
this.setState({ input_in_focus: null });
const data = {};
@ -116,37 +103,25 @@ class Settings extends React.Component {
}
}
renderApplyButton() {
if (this.props.mopidy.host == this.state.mopidy_host
&& this.props.mopidy.port == this.state.mopidy_port) {
return null;
}
return (
<div className="field">
<div className="name" />
<div className="input">
<button type="submit" className="button button--secondary">Apply and reload</button>
</div>
</div>
);
}
renderServerStatus() {
const {
mopidy,
pusher,
} = this.props;
let colour = 'grey';
let icon = 'help';
let status = 'Unknown';
let className = null;
if (this.props.mopidy.connecting || this.props.pusher.connecting) {
if (mopidy.connecting || pusher.connecting) {
icon = 'autorenew';
status = 'Connecting...';
className = 'icon--spin';
} else if (!this.props.mopidy.connected || !this.props.pusher.connected) {
} else if (!mopidy.connected || !pusher.connected) {
colour = 'red';
icon = 'close';
status = 'Disconnected';
} else if (this.props.mopidy.connected && this.props.pusher.connected) {
} else if (mopidy.connected && pusher.connected) {
colour = 'green';
icon = 'check';
status = 'Connected';
@ -162,30 +137,40 @@ class Settings extends React.Component {
}
render() {
const {
mopidyActions,
mopidy,
pusherActions,
pusher,
history,
uiActions,
ui,
} = this.props;
const options = (
<span>
<a className="button button--default button--no-hover" onClick={(e) => this.props.history.push('/settings/debug')}>
<a className="button button--default button--no-hover" onClick={(e) => history.push('/settings/debug')}>
<Icon name="code" />
Debug
Debug
</a>
<a className="button button--default button--no-hover" href="https://github.com/jaedb/Iris/wiki" target="_blank">
<Icon name="help" />
Help
Help
</a>
</span>
);
return (
<div className="view settings-view">
<Header options={options} uiActions={this.props.uiActions}>
<Header options={options} uiActions={uiActions}>
<Icon name="settings" type="material" />
Settings
Settings
</Header>
<section className="content-wrapper">
<h4 className="underline">
Server
Server
<a name="server" />
</h4>
@ -202,11 +187,11 @@ Server
<div className="name">Username</div>
<div className="input">
<TextField
onChange={(value) => this.props.pusherActions.setUsername(value.replace(/\W/g, ''))}
onChange={(value) => pusherActions.setUsername(value.replace(/\W/g, ''))}
value={this.state.pusher_username}
/>
<div className="description">
A non-unique string used to identify this client (no special characters)
A non-unique string used to identify this client (no special characters)
</div>
</div>
</label>
@ -215,39 +200,33 @@ Server
<label className="field">
<div className="name">Host</div>
<div className="input">
<input
type="text"
onChange={(e) => this.setState({ mopidy_host: e.target.value })}
onFocus={(e) => this.setState({ input_in_focus: 'mopidy_host' })}
onBlur={(e) => this.setState({ input_in_focus: null })}
value={this.state.mopidy_host}
<TextField
value={mopidy.host}
onChange={value => mopidyActions.setConnection({ host: value })}
/>
</div>
</label>
<label className="field">
<div className="name">Port</div>
<div className="input">
<input
<TextField
type="text"
onChange={(e) => this.setState({ mopidy_port: e.target.value })}
onFocus={(e) => this.setState({ input_in_focus: 'mopidy_port' })}
onBlur={(e) => this.setState({ input_in_focus: null })}
value={this.state.mopidy_port}
value={mopidy.port}
onChange={value => mopidyActions.setConnection({ port: value })}
/>
</div>
</label>
{this.renderApplyButton()}
</form>
<h4 className="underline">
Services
Services
<a name="services" />
</h4>
<Route path="/settings/:service?" component={Services} />
<Route path="/settings/:service?/:id?" component={Services} />
<h4 className="underline">
Interface
Interface
<a name="interface" />
</h4>
@ -261,8 +240,8 @@ Interface
type="radio"
name="theme"
value="dark"
checked={this.props.ui.theme == 'dark'}
onChange={(e) => this.props.uiActions.set({ theme: e.target.value })}
checked={ui.theme == 'dark'}
onChange={(e) => uiActions.set({ theme: e.target.value })}
/>
<span className="label">Dark</span>
</label>
@ -271,8 +250,8 @@ Interface
type="radio"
name="theme"
value="light"
checked={this.props.ui.theme == 'light'}
onChange={(e) => this.props.uiActions.set({ theme: e.target.value })}
checked={ui.theme == 'light'}
onChange={(e) => uiActions.set({ theme: e.target.value })}
/>
<span className="label">Light</span>
</label>
@ -286,8 +265,8 @@ Interface
<input
type="checkbox"
name="log_actions"
checked={this.props.ui.clear_tracklist_on_play}
onChange={(e) => this.props.uiActions.set({ clear_tracklist_on_play: !this.props.ui.clear_tracklist_on_play })}
checked={ui.clear_tracklist_on_play}
onChange={(e) => uiActions.set({ clear_tracklist_on_play: !ui.clear_tracklist_on_play })}
/>
<span className="label tooltip">
Clear tracklist on play of URI(s)
@ -298,8 +277,8 @@ Interface
<input
type="checkbox"
name="hotkeys_enabled"
checked={this.props.ui.hotkeys_enabled}
onChange={(e) => this.props.uiActions.set({ hotkeys_enabled: !this.props.ui.hotkeys_enabled })}
checked={ui.hotkeys_enabled}
onChange={(e) => uiActions.set({ hotkeys_enabled: !ui.hotkeys_enabled })}
/>
<span className="label">
Enable hotkeys
@ -309,8 +288,8 @@ Interface
<input
type="checkbox"
name="smooth_scrolling_enabled"
checked={this.props.ui.smooth_scrolling_enabled}
onChange={(e) => this.props.uiActions.set({ smooth_scrolling_enabled: !this.props.ui.smooth_scrolling_enabled })}
checked={ui.smooth_scrolling_enabled}
onChange={(e) => uiActions.set({ smooth_scrolling_enabled: !ui.smooth_scrolling_enabled })}
/>
<span className="label">
Enable smooth scrolling
@ -320,8 +299,8 @@ Interface
<input
type="checkbox"
name="playback_controls_touch_enabled"
checked={this.props.ui.playback_controls_touch_enabled}
onChange={(e) => this.props.uiActions.set({ playback_controls_touch_enabled: !this.props.ui.playback_controls_touch_enabled })}
checked={ui.playback_controls_touch_enabled}
onChange={(e) => uiActions.set({ playback_controls_touch_enabled: !ui.playback_controls_touch_enabled })}
/>
<span className="label tooltip">
Enable touch events on play controls
@ -332,8 +311,8 @@ Interface
<input
type="checkbox"
name="wide_scrollbar_enabled"
checked={this.props.ui.wide_scrollbar_enabled}
onChange={(e) => this.props.uiActions.set({ wide_scrollbar_enabled: !this.props.ui.wide_scrollbar_enabled })}
checked={ui.wide_scrollbar_enabled}
onChange={(e) => uiActions.set({ wide_scrollbar_enabled: !ui.wide_scrollbar_enabled })}
/>
<span className="label">
Use wide scrollbars
@ -348,9 +327,9 @@ Interface
</div>
<div className="input">
<SourcesPriority
uri_schemes={this.props.mopidy.uri_schemes ? this.props.mopidy.uri_schemes : []}
uri_schemes_priority={this.props.ui.uri_schemes_priority ? this.props.ui.uri_schemes_priority : []}
uiActions={this.props.uiActions}
uri_schemes={mopidy.uri_schemes ? mopidy.uri_schemes : []}
uri_schemes_priority={ui.uri_schemes_priority ? ui.uri_schemes_priority : []}
uiActions={uiActions}
/>
<div className="description">
Drag-and-drop to prioritize search providers and results
@ -366,8 +345,8 @@ Interface
<input
type="checkbox"
name="allow_reporting"
checked={this.props.ui.allow_reporting}
onChange={(e) => this.props.uiActions.set({ allow_reporting: !this.props.ui.allow_reporting })}
checked={ui.allow_reporting}
onChange={(e) => uiActions.set({ allow_reporting: !ui.allow_reporting })}
/>
<span className="label">
Allow reporting of anonymous usage statistics