Better UI for snapcast

This commit is contained in:
James Barnsley
2018-04-18 17:01:57 +12:00
parent e1b8836c53
commit 4e049d403e
10 changed files with 26297 additions and 23735 deletions

View 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}
/>
);
}
}

View 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>
);
}
}

View File

@ -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>
);
})

View File

@ -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
}
}

View File

@ -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({

View File

@ -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;
}
}
}

View File

@ -30,6 +30,13 @@ select {
&:disabled {
opacity: 0.5;
}
&.tiny {
width: 50px;
padding-left: 4px;
padding-right: 4px;
text-align: center;
}
}
select {