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>
|
||||
);
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user