Better UI for snapcast
This commit is contained in:
48
src/js/components/Forms/TextField.js
Executable file
48
src/js/components/Forms/TextField.js
Executable file
@ -0,0 +1,48 @@
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export default class TextField extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
in_focus: false,
|
||||
value: (this.props.value ? this.props.value : '')
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps){
|
||||
if (!this.state.in_focus){
|
||||
this.setState({value: newProps.value})
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(e){
|
||||
this.setState({value: e.target.value});
|
||||
}
|
||||
|
||||
handleFocus(e){
|
||||
this.setState({in_focus: true});
|
||||
}
|
||||
|
||||
handleBlur(e){
|
||||
this.setState({in_focus: false});
|
||||
if (this.state.value !== this.props.value){
|
||||
this.props.onChange(this.state.value);
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<input
|
||||
className={this.props.className ? this.props.className : ''}
|
||||
type="text"
|
||||
onChange={e => this.handleChange(e)}
|
||||
onFocus={e => this.handleFocus(e)}
|
||||
onBlur={e => this.handleBlur(e)}
|
||||
value={this.state.value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
107
src/js/components/LatencyControl.js
Executable file
107
src/js/components/LatencyControl.js
Executable file
@ -0,0 +1,107 @@
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
export default class LatencyControl extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleClick(e){
|
||||
var slider = e.target;
|
||||
if (slider.className != 'slider'){
|
||||
slider = slider.parentElement;
|
||||
}
|
||||
|
||||
var sliderX = e.clientX - slider.getBoundingClientRect().left;
|
||||
var sliderWidth = slider.getBoundingClientRect().width;
|
||||
|
||||
// Clicked left half
|
||||
if (sliderX < (sliderWidth/2)){
|
||||
var percent = Math.round((sliderX / (sliderWidth/2) ) * 100);
|
||||
|
||||
// Invert our percentage
|
||||
percent = 100 - percent;
|
||||
|
||||
var value = -(this.props.max * (percent / 100));
|
||||
|
||||
// Handle maximum value limits
|
||||
if (value < -this.props.max){
|
||||
value = -this.props.max;
|
||||
}
|
||||
|
||||
// Second half
|
||||
} else {
|
||||
|
||||
// Subtract half the slider's width from our click position
|
||||
sliderX = sliderX - (sliderWidth/2);
|
||||
var percent = Math.round((sliderX / (sliderWidth/2) ) * 100);
|
||||
|
||||
var value = this.props.max * (percent / 100);
|
||||
|
||||
// Handle maximum value limits
|
||||
if (value > this.props.max){
|
||||
value = this.props.max;
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
handleWheel(e){
|
||||
|
||||
// Identify which direction we've scrolled (inverted)
|
||||
// This is simplified and doesn't consider momentum as it varies wildly
|
||||
// between browsers and devices
|
||||
var direction = (e.deltaY > 0 ? -1 : 1)
|
||||
var value = this.props.value;
|
||||
|
||||
value += direction * 5
|
||||
|
||||
if (value > this.props.max){
|
||||
value = this.props.max;
|
||||
} else if (value < -this.props.max){
|
||||
value = -this.props.max;
|
||||
}
|
||||
|
||||
this.props.onChange(value);
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
render(){
|
||||
|
||||
// Zero, or positive value
|
||||
if (this.props.value >= 0){
|
||||
var value = this.props.value;
|
||||
if (value > this.props.max){
|
||||
value = this.props.max;
|
||||
}
|
||||
var percentage = Math.round((value / this.props.max) * 100 / 2);
|
||||
var left = 50;
|
||||
var width = percentage;
|
||||
|
||||
// Negative value
|
||||
// We reverse it to a positive for easier maths and style rules
|
||||
} else {
|
||||
var value = -this.props.value;
|
||||
if (value < -this.props.max){
|
||||
value = -this.props.max;
|
||||
}
|
||||
var percentage = Math.round((value / this.props.max) * 100 / 2);
|
||||
var left = 50 - percentage;
|
||||
var width = percentage;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="latency-control" onWheel={e => this.handleWheel(e)}>
|
||||
<div className="slider-wrapper">
|
||||
<div className="slider horizontal" onClick={e => this.handleClick(e)}>
|
||||
<div className="track">
|
||||
<div className="progress" style={{ width: width+'%', left: left+'%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,8 @@ import FontAwesome from 'react-fontawesome'
|
||||
|
||||
import ArtistSentence from './ArtistSentence'
|
||||
import VolumeControl from './VolumeControl'
|
||||
import LatencyControl from './LatencyControl'
|
||||
import TextField from './Forms/TextField'
|
||||
|
||||
import * as helpers from '../helpers'
|
||||
import * as coreActions from '../services/core/actions'
|
||||
@ -17,10 +19,6 @@ class Snapcast extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
editing_client: null
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
@ -37,11 +35,6 @@ class Snapcast extends React.Component{
|
||||
}
|
||||
}
|
||||
|
||||
saveEditingClient(){
|
||||
this.props.pusherActions.setSnapcastClientName(this.state.editing_client.id, this.state.editing_client.name);
|
||||
this.setState({editing_client: null});
|
||||
}
|
||||
|
||||
render(){
|
||||
if (!this.props.snapcast_enabled){
|
||||
return (
|
||||
@ -88,39 +81,59 @@ class Snapcast extends React.Component{
|
||||
{
|
||||
groups.map(group => {
|
||||
return (
|
||||
<div className="group" key={group.id}>
|
||||
<div className="inner">
|
||||
<div className="list group" key={group.id}>
|
||||
<div className="list-item header">
|
||||
<div className="name">
|
||||
{group.name ? group.name : 'Untitled group'}
|
||||
</div>
|
||||
<div className="volume">
|
||||
Volume
|
||||
</div>
|
||||
<div className="latency">
|
||||
Latency
|
||||
</div>
|
||||
</div>
|
||||
<div className="clients">
|
||||
{
|
||||
group.clients.map(client => {
|
||||
var name = client.config.name ? client.config.name : client.host.name;
|
||||
return (
|
||||
<div className={"client "+(client.connected ? 'connected' : 'disconnected')} key={client.id}>
|
||||
<div className="inner">
|
||||
<div className="name">
|
||||
{name} {!client.connected ? '(disconnected)' : null}
|
||||
</div>
|
||||
<div className="controls">
|
||||
<div className="control edit" onClick={e => this.props.uiActions.openModal('edit_snapcast_client',{id: client.id})}>
|
||||
<FontAwesome name="cog" />
|
||||
</div>
|
||||
<VolumeControl
|
||||
volume={client.config.volume.percent}
|
||||
mute={client.config.volume.muted}
|
||||
onVolumeChange={percent => this.props.pusherActions.setSnapcastClientVolume(client.id, client.config.volume.muted, percent)}
|
||||
onMuteChange={mute => this.props.pusherActions.setSnapcastClientVolume(client.id, mute, client.config.volume.percent)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
group.clients.map(client => {
|
||||
var name = client.config.name ? client.config.name : client.host.name;
|
||||
return (
|
||||
<div className={"list-item client "+(client.connected ? 'connected' : 'disconnected')} key={client.id}>
|
||||
<div className="name">
|
||||
<TextField
|
||||
onChange={value => this.props.pusherActions.setSnapcastClientName(client.id, value)}
|
||||
value={name}
|
||||
/>
|
||||
{!client.connected ? <span className="disconnected flag grey">DISCONNECTED</span> : null}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div className="volume">
|
||||
<VolumeControl
|
||||
volume={client.config.volume.percent}
|
||||
mute={client.config.volume.muted}
|
||||
onVolumeChange={percent => this.props.pusherActions.setSnapcastClientVolume(client.id, percent)}
|
||||
onMuteChange={mute => this.props.pusherActions.setSnapcastClientMute(client.id, mute)}
|
||||
/>
|
||||
<TextField
|
||||
className="tiny"
|
||||
onChange={value => this.props.pusherActions.setSnapcastClientVolume(client.id, parseInt(value))}
|
||||
value={client.config.volume.percent}
|
||||
/>
|
||||
</div>
|
||||
<div className="latency">
|
||||
<LatencyControl
|
||||
max="100"
|
||||
value={client.config.latency}
|
||||
onChange={value => this.props.pusherActions.setSnapcastClientLatency(client.id, parseInt(value))}
|
||||
/>
|
||||
<TextField
|
||||
className="tiny"
|
||||
onChange={value => this.props.pusherActions.setSnapcastClientLatency(client.id, parseInt(value))}
|
||||
value={client.config.latency}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
@ -197,57 +197,42 @@ export function getSnapcast(){
|
||||
}
|
||||
}
|
||||
|
||||
export function setSnapcastClientVolume(id, muted, percent){
|
||||
return {
|
||||
type: 'PUSHER_SET_SNAPCAST_CLIENT_VOLUME',
|
||||
data: {
|
||||
method: 'Client.SetVolume',
|
||||
params: {
|
||||
id: id,
|
||||
volume: {
|
||||
muted: muted,
|
||||
percent: percent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setSnapcastClientName(id, name){
|
||||
return {
|
||||
type: 'PUSHER_SET_SNAPCAST_CLIENT_NAME',
|
||||
data: {
|
||||
method: 'Client.SetName',
|
||||
params: {
|
||||
id: id,
|
||||
name: name
|
||||
}
|
||||
}
|
||||
id: id,
|
||||
name: name
|
||||
}
|
||||
}
|
||||
|
||||
export function setSnapcastClientMute(id, mute){
|
||||
return {
|
||||
type: 'PUSHER_SET_SNAPCAST_CLIENT_MUTE',
|
||||
id: id,
|
||||
mute: mute
|
||||
}
|
||||
}
|
||||
|
||||
export function setSnapcastClientVolume(id, percent){
|
||||
return {
|
||||
type: 'PUSHER_SET_SNAPCAST_CLIENT_VOLUME',
|
||||
id: id,
|
||||
percent: percent
|
||||
}
|
||||
}
|
||||
|
||||
export function setSnapcastClientLatency(id, latency){
|
||||
return {
|
||||
type: 'PUSHER_SET_SNAPCAST_CLIENT_LATENCY',
|
||||
data: {
|
||||
method: 'Client.SetLatency',
|
||||
params: {
|
||||
id: id,
|
||||
latency: latency
|
||||
}
|
||||
}
|
||||
id: id,
|
||||
latency: latency
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteSnapcastClient(id){
|
||||
return {
|
||||
type: 'PUSHER_DELETE_SNAPCAST_CLIENT',
|
||||
data: {
|
||||
method: 'Server.DeleteClient',
|
||||
params: {
|
||||
id: id
|
||||
}
|
||||
}
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -603,37 +603,22 @@ const PusherMiddleware = (function(){
|
||||
);
|
||||
break
|
||||
|
||||
case 'PUSHER_SET_SNAPCAST_CLIENT_VOLUME':
|
||||
request(store, 'snapcast_instruct', action.data)
|
||||
.then(
|
||||
response => {
|
||||
store.dispatch({
|
||||
type: 'PUSHER_SNAPCAST_CLIENT_UPDATED',
|
||||
key: action.data.params.id,
|
||||
client: {
|
||||
config: {
|
||||
volume: response.volume
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
error => {
|
||||
store.dispatch(coreActions.handleException(
|
||||
'Error',
|
||||
error,
|
||||
error.message
|
||||
));
|
||||
}
|
||||
);
|
||||
break
|
||||
|
||||
case 'PUSHER_SET_SNAPCAST_CLIENT_NAME':
|
||||
request(store, 'snapcast_instruct', action.data)
|
||||
var client = store.getState().pusher.snapcast_clients[action.id];
|
||||
var data = {
|
||||
method: 'Client.SetName',
|
||||
params: {
|
||||
id: action.id,
|
||||
name: action.name
|
||||
}
|
||||
}
|
||||
|
||||
request(store, 'snapcast_instruct', data)
|
||||
.then(
|
||||
response => {
|
||||
store.dispatch({
|
||||
type: 'PUSHER_SNAPCAST_CLIENT_UPDATED',
|
||||
key: action.data.params.id,
|
||||
key: action.id,
|
||||
client: {
|
||||
config: {
|
||||
name: response.name
|
||||
@ -651,13 +636,94 @@ const PusherMiddleware = (function(){
|
||||
);
|
||||
break
|
||||
|
||||
case 'PUSHER_SET_SNAPCAST_CLIENT_LATENCY':
|
||||
request(store, 'snapcast_instruct', action.data)
|
||||
case 'PUSHER_SET_SNAPCAST_CLIENT_MUTE':
|
||||
var client = store.getState().pusher.snapcast_clients[action.id];
|
||||
var data = {
|
||||
method: 'Client.SetVolume',
|
||||
params: {
|
||||
id: action.id,
|
||||
volume: {
|
||||
muted: action.mute,
|
||||
percent: client.config.volume.percent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request(store, 'snapcast_instruct', data)
|
||||
.then(
|
||||
response => {
|
||||
store.dispatch({
|
||||
type: 'PUSHER_SNAPCAST_CLIENT_UPDATED',
|
||||
key: action.data.params.id,
|
||||
key: action.id,
|
||||
client: {
|
||||
config: {
|
||||
volume: response.volume
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
error => {
|
||||
store.dispatch(coreActions.handleException(
|
||||
'Error',
|
||||
error,
|
||||
error.message
|
||||
));
|
||||
}
|
||||
);
|
||||
break
|
||||
|
||||
case 'PUSHER_SET_SNAPCAST_CLIENT_VOLUME':
|
||||
var client = store.getState().pusher.snapcast_clients[action.id];
|
||||
var data = {
|
||||
method: 'Client.SetVolume',
|
||||
params: {
|
||||
id: action.id,
|
||||
volume: {
|
||||
muted: client.config.volume.muted,
|
||||
percent: action.percent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request(store, 'snapcast_instruct', data)
|
||||
.then(
|
||||
response => {
|
||||
store.dispatch({
|
||||
type: 'PUSHER_SNAPCAST_CLIENT_UPDATED',
|
||||
key: action.id,
|
||||
client: {
|
||||
config: {
|
||||
volume: response.volume
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
error => {
|
||||
store.dispatch(coreActions.handleException(
|
||||
'Error',
|
||||
error,
|
||||
error.message
|
||||
));
|
||||
}
|
||||
);
|
||||
break
|
||||
|
||||
case 'PUSHER_SET_SNAPCAST_CLIENT_LATENCY':
|
||||
var client = store.getState().pusher.snapcast_clients[action.id];
|
||||
var data = {
|
||||
method: 'Client.SetLatency',
|
||||
params: {
|
||||
id: action.id,
|
||||
latency: action.latency
|
||||
}
|
||||
}
|
||||
|
||||
request(store, 'snapcast_instruct', data)
|
||||
.then(
|
||||
response => {
|
||||
store.dispatch({
|
||||
type: 'PUSHER_SNAPCAST_CLIENT_UPDATED',
|
||||
key: action.id,
|
||||
client: {
|
||||
config: {
|
||||
latency: response.latency
|
||||
@ -676,7 +742,14 @@ const PusherMiddleware = (function(){
|
||||
break
|
||||
|
||||
case 'PUSHER_DELETE_SNAPCAST_CLIENT':
|
||||
request(store, 'snapcast_instruct', action.data)
|
||||
var data = {
|
||||
method: 'Server.DeleteClient',
|
||||
params: {
|
||||
id: action.id
|
||||
}
|
||||
}
|
||||
|
||||
request(store, 'snapcast_instruct', data)
|
||||
.then(
|
||||
response => {
|
||||
store.dispatch({
|
||||
|
||||
@ -1,75 +1,105 @@
|
||||
|
||||
.snapcast {
|
||||
|
||||
.group {
|
||||
& > .inner {
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid $grey;
|
||||
|
||||
& > .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.group.list {
|
||||
padding: 0 10px;
|
||||
|
||||
&:not(:first-child){
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.clients {
|
||||
.client {
|
||||
& > .inner {
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid $grey;
|
||||
.list-item {
|
||||
padding: 20px 0 0 0;
|
||||
|
||||
&:not(.header){
|
||||
border: 0;
|
||||
}
|
||||
|
||||
& > .name {
|
||||
width: 40%;
|
||||
float: left;
|
||||
padding-bottom: 4px;
|
||||
position: relative;
|
||||
|
||||
.disconnected {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 5px;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
& > .volume {
|
||||
float: left;
|
||||
width: 28%;
|
||||
padding-left: 2%;
|
||||
}
|
||||
|
||||
& > .latency {
|
||||
float: left;
|
||||
width: 28%;
|
||||
padding-left: 2%;
|
||||
}
|
||||
|
||||
&.header {
|
||||
padding-top: 0;
|
||||
vertical-align: bottom;
|
||||
|
||||
.name {
|
||||
padding: 6px 0 5px 0;
|
||||
font-size: 14px;
|
||||
color: $white;
|
||||
text-transform: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&.client {
|
||||
@include clearfix();
|
||||
padding: 20px 0 0 0;
|
||||
|
||||
& > .name {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 0;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.volume-control {
|
||||
display: block;
|
||||
padding-top: 14px;
|
||||
float: left;
|
||||
width: 80%;
|
||||
padding-right: 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.controls {
|
||||
|
||||
.edit {
|
||||
.mute-control {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.volume-control {
|
||||
.mute-control {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
}
|
||||
.slider-wrapper {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.slider-wrapper {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
&.muted {
|
||||
.slider .progress {
|
||||
background: $mid_grey;
|
||||
}
|
||||
&.muted {
|
||||
.slider .progress {
|
||||
background: $mid_grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disconnected {
|
||||
.name {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.controls {
|
||||
.volume-control {
|
||||
.slider .progress {
|
||||
background: $mid_grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
.latency-control {
|
||||
display: block;
|
||||
padding-top: 14px;
|
||||
float: left;
|
||||
width: 80%;
|
||||
padding-right: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,13 @@ select {
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.tiny {
|
||||
width: 50px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
|
||||
Reference in New Issue
Block a user