Snapcast groups revamp
This commit is contained in:
@ -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() {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
117
src/js/components/SnapcastClients.js
Normal file
117
src/js/components/SnapcastClients.js
Normal 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;
|
||||
169
src/js/components/SnapcastGroups.js
Normal file
169
src/js/components/SnapcastGroups.js
Normal 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);
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user