Moving commands out of Snapcast

This commit is contained in:
James Barnsley
2018-10-09 21:39:38 +13:00
parent 9f0961f593
commit fc3f5e2d2f
20 changed files with 11052 additions and 14208 deletions

2
src/js/bootstrap.js vendored
View File

@ -89,7 +89,7 @@ var initialState = {
groups: {},
clients: {},
server: null,
client_commands: {}
commands: {}
}
};

View File

@ -76,21 +76,16 @@ class OutputControl extends React.Component{
clients.map(client => {
var commands = {};
if (this.props.snapcast_client_commands[client.id]){
commands = this.props.snapcast_client_commands[client.id];
if (this.props.snapcast_commands[client.id]){
commands = this.props.snapcast_commands[client.id];
}
return (
<div className={"output-control__item outputs__item--snapcast"+(commands && commands.power ? " output-control__item--has-power-button": '')} key={client.id}>
<div className="output-control__item outputs__item--snapcast" key={client.id}>
<div className="output-control__item__name">
{client.name}
</div>
<div className="output-control__item__controls">
{commands && commands.power ? <SnapcastPowerButton
className="output-control__item__power"
client={client}
onClick={e => this.props.snapcastActions.sendClientCommand(client.id, commands.power)}
/> : null}
<MuteControl
className="output-control__item__mute"
noTooltip={true}
@ -189,7 +184,7 @@ const mapStateToProps = (state, ownProps) => {
pusher_connected: state.pusher.connected,
snapcast_enabled: (state.pusher.config ? state.pusher.config.snapcast_enabled : null),
snapcast_clients: state.snapcast.clients,
snapcast_client_commands: (state.snapcast.client_commands ? state.snapcast.client_commands : {})
snapcast_commands: (state.snapcast.commands ? state.snapcast.commands : {})
}
}

View File

@ -29,10 +29,6 @@ export default class Notifications extends React.Component{
this.props.geniusActions.importAuthorization(configuration.genius.authorization, configuration.genius.me);
}
if (configuration.snapcast_client_commands){
this.props.snapcastActions.clientCommandsUpdated(configuration.snapcast_client_commands);
}
this.props.uiActions.removeNotification(notification_key, true);
this.props.uiActions.createNotification({type: 'info', content: 'Import successful'});
}
@ -72,7 +68,6 @@ export default class Notifications extends React.Component{
{notification.configuration.spotify ? <li>Spotify</li> : null}
{notification.configuration.lastfm ? <li>LastFM</li> : null}
{notification.configuration.genius ? <li>Genius</li> : null}
{notification.configuration.snapcast_client_commands ? <li>Snapcast</li> : null}
</ul>
<p>Do you want to import this?</p>
</div>

View File

@ -89,8 +89,8 @@ class Snapcast extends React.Component{
if (this.state.clients_expanded.includes(client.id)){
var commands = {};
if (this.props.client_commands[client.id]){
commands = this.props.client_commands[client.id];
if (this.props.commands[client.id]){
commands = this.props.commands[client.id];
}
return (
@ -176,7 +176,7 @@ class Snapcast extends React.Component{
</div>
<div className="input">
<TextField
onChange={value => this.props.snapcastActions.setClientCommand(client.id, {power: value})}
onChange={value => this.props.snapcastActions.setCommand(client.id, {power: value})}
value={commands.power ? commands.power : ""}
placeholder='{"url":"https://myserver.local:8080/sendCommand/power"}'
/>
@ -335,8 +335,7 @@ 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),
client_commands: (state.snapcast.client_commands ? state.snapcast.client_commands : {})
clients: (state.snapcast.clients ? state.snapcast.clients : null)
}
}

View File

@ -44,6 +44,7 @@ import KioskMode from './views/modals/KioskMode';
import ShareConfiguration from './views/modals/ShareConfiguration';
import AddToPlaylist from './views/modals/AddToPlaylist';
import ImageZoom from './views/modals/ImageZoom';
import EditCommand from './views/modals/EditCommand';
global.baseURL = '/';
@ -59,6 +60,7 @@ ReactDOM.render(
<Route path="add-to-playlist/:uris" component={AddToPlaylist} />
<Route path="image-zoom" component={ImageZoom} />
<Route path="share-configuration" component={ShareConfiguration} />
<Route path="edit-command(/:id)" component={EditCommand} />
<Route path="queue" component={Queue} />
<Route path="queue/history" component={QueueHistory} />

View File

@ -240,11 +240,11 @@ const localstorageMiddleware = (function(){
);
break;
case 'SNAPCAST_CLIENT_COMMANDS_UPDATED':
case 'SNAPCAST_COMMANDS_UPDATED':
helpers.setStorage(
'snapcast',
{
client_commands: action.client_commands
commands: action.commands
}
);
break;

View File

@ -192,97 +192,28 @@ export function addQueueMetadata(tlids = [], from_uri = null){
}
}
/**
* Snapcast actions
* TODO: Figure out how to cleanly split this out to it's own service
* but still share the pusher middleware connection
* Commands (buttons)
**/
export function getSnapcast(){
return {
type: 'PUSHER_GET_SNAPCAST',
data: {
method: 'Server.GetStatus'
}
}
export function setCommand(command){
return {
type: 'PUSHER_SET_COMMAND',
command: command
}
}
export function snapcastServerLoaded(server){
return {
type: 'PUSHER_SNAPCAST_SERVER_LOADED',
server: server
}
}
export function setSnapcastClientName(id, name){
return {
type: 'PUSHER_SET_SNAPCAST_CLIENT_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',
id: id,
latency: latency
}
}
export function setSnapcastClientGroup(id, group_id){
return {
type: 'PUSHER_SET_SNAPCAST_CLIENT_GROUP',
id: id,
group_id: group_id
}
}
export function deleteSnapcastClient(id){
return {
type: 'PUSHER_DELETE_SNAPCAST_CLIENT',
id: id
}
}
export function setSnapcastGroupStream(id, stream_id){
return {
type: 'PUSHER_SET_SNAPCAST_GROUP_STREAM',
id: id,
stream_id: stream_id
}
}
export function setSnapcastGroupMute(id, mute){
return {
type: 'PUSHER_SET_SNAPCAST_GROUP_MUTE',
id: id,
mute: mute
}
}
export function setSnapcastGroupVolume(id, percent, old_percent = 0){
return {
type: 'PUSHER_SET_SNAPCAST_GROUP_VOLUME',
id: id,
percent: percent,
old_percent: old_percent
}
export function sendCommand(id){
return {
type: 'PUSHER_SEND_COMMAND',
id: id
}
}
export function commandsUpdated(commands){
return {
type: 'PUSHER_COMMANDS_UPDATED',
commands: commands
}
}

View File

@ -387,6 +387,51 @@ const PusherMiddleware = (function(){
return next(action);
break;
/**
* Commands
**/
case 'PUSHER_SET_COMMAND':
var commands_index = Object.assign({}, store.getState().pusher.commands);
if (commands_index[action.command.id]){
var command = Object.assign({}, commands_index[action.command.id], action.command);
} else {
var command = action.command;
}
commands_index[action.command.id] = command;
store.dispatch(pusherActions.commandsUpdated(commands_index));
next(action);
break
case 'PUSHER_SEND_COMMAND':
var commands_index = Object.assign({}, pusher.commands);
// Prepare our command
// We try and make it cross-origin compatible
var command = JSON.parse(commands_index[action.id]);
command.crossDomain = true;
command.dataType = "jsonp";
command.success = function(response){
store.dispatch(uiActions.createNotification({type: 'info', content: 'Command sent', description: command.url}));
}
command.fail = function(xhr, status, error){
store.dispatch(uiActions.createNotification({type: 'bad', content: 'Command failed', description: error}));
}
// Actually send the request
$.ajax(command);
break
/**
* Radio
**/
case 'PUSHER_GET_RADIO':
request(store, 'get_radio')
.then(

View File

@ -54,30 +54,8 @@ export default function reducer(pusher = {}, action){
case 'PUSHER_CONFIG':
return Object.assign({}, pusher, { config: action.config });
case 'PUSHER_SNAPCAST':
return Object.assign({}, pusher, {
snapcast_streams: action.snapcast_streams,
snapcast_groups: action.snapcast_groups,
snapcast_clients: action.snapcast_clients
});
case 'PUSHER_SNAPCAST_GROUP_UPDATED':
var snapcast_groups = Object.assign({}, pusher.snapcast_groups);
var group = snapcast_groups[action.key];
snapcast_groups[action.key] = Object.assign({}, group, action.group);
return Object.assign({}, pusher, { snapcast_groups: snapcast_groups });
case 'PUSHER_SNAPCAST_CLIENT_UPDATED':
var snapcast_clients = Object.assign({}, pusher.snapcast_clients);
var client = snapcast_clients[action.key];
client.config = Object.assign({}, client.config, action.client.config);
snapcast_clients[action.key] = client;
return Object.assign({}, pusher, { snapcast_clients: snapcast_clients });
case 'PUSHER_SNAPCAST_CLIENT_REMOVED':
var snapcast_clients = Object.assign({}, pusher.snapcast_clients);
delete snapcast_clients[action.key];
return Object.assign({}, pusher, { snapcast_clients: snapcast_clients });
case 'PUSHER_COMMANDS_UPDATED':
return Object.assign({}, pusher, { commands: action.commands });
default:
return pusher

View File

@ -37,29 +37,6 @@ export function setClientLatency(id, latency){
}
}
export function setClientCommand(id, command){
return {
type: 'SNAPCAST_SET_CLIENT_COMMAND',
id: id,
command: command
}
}
export function sendClientCommand(id, command){
return {
type: 'SNAPCAST_SEND_CLIENT_COMMAND',
id: id,
command: command
}
}
export function clientCommandsUpdated(client_commands){
return {
type: 'SNAPCAST_CLIENT_COMMANDS_UPDATED',
client_commands: client_commands
}
}
export function setClientGroup(id, group_id){
return {
type: 'SNAPCAST_SET_CLIENT_GROUP',

View File

@ -228,37 +228,6 @@ const SnapcastMiddleware = (function(){
);
break
case 'SNAPCAST_SET_CLIENT_COMMAND':
var client_commands_index = Object.assign({}, snapcast.client_commands);
if (client_commands_index[action.id]){
var client_command = Object.assign({}, client_commands_index[action.id], action.command);
} else {
var client_command = action.command;
}
client_commands_index[action.id] = client_command;
store.dispatch(snapcastActions.clientCommandsUpdated(client_commands_index));
next(action);
break
case 'SNAPCAST_SEND_CLIENT_COMMAND':
// Prepare our command
// We try and make it cross-origin compatible
var command = JSON.parse(action.command);
command.crossDomain = true;
command.dataType = "jsonp";
command.success = function(response){
store.dispatch(uiActions.createNotification({type: 'info', content: 'Command sent', description: command.url}));
}
// Actually send the request
$.ajax(command);
break
case 'SNAPCAST_SET_CLIENT_GROUP':
var group = snapcast.groups[action.group_id];

View File

@ -6,9 +6,6 @@ export default function reducer(snapcast = {}, action){
var server = Object.assign({}, action.server);
return Object.assign({}, snapcast, { server: server });
case 'SNAPCAST_CLIENT_COMMANDS_UPDATED':
return Object.assign({}, snapcast, { client_commands: action.client_commands });
case 'SNAPCAST_CLIENTS_LOADED':
if (action.flush){
var clients = {};

View File

@ -166,6 +166,15 @@ class Settings extends React.Component {
var upgrade_button = null;
}
var commands = [];
if (this.props.pusher.commands){
for (var id in this.props.pusher.commands){
if (this.props.pusher.commands.hasOwnProperty(id)){
commands.push(this.props.pusher.commands[id]);
}
}
}
return (
<div className="view settings-view">
<Header options={options} uiActions={this.props.uiActions}>
@ -325,10 +334,28 @@ class Settings extends React.Component {
Allow reporting of anonymous usage statistics
</span>
</label>
<div className="description">Anonymous usage data is used to identify errors and potential features that make Iris better for everyone. Read the <a href="https://github.com/jaedb/Iris/wiki/Terms-of-use#privacy-policy" target="_blank">privacy policy</a>.</div>
<div className="description">This helps identify errors and potential features that make Iris better for everyone. See <a href="https://github.com/jaedb/Iris/wiki/Terms-of-use#privacy-policy" target="_blank">privacy policy</a>.</div>
</div>
</div>}
<div className="field commands-setup">
<div className="name">
Commands
</div>
<div className="input">
{
commands.map(command => {
return (
<Link className="command__item commands-setup__item" key={command.id} to={global.baseURL+'edit-command/'+command.id}>
<Icon name={command.icon} />
</Link>
);
})
}
<Link to={global.baseURL+'edit-command'} className="button">Add new</Link>
</div>
</div>
<h4 className="underline">Advanced</h4>
<div className="field">

View File

@ -0,0 +1,111 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { Link } from 'react-router'
import ReactGA from 'react-ga'
import Modal from './Modal';
import Icon from '../../components/Icon';
import * as pusherActions from '../../services/pusher/actions';
import * as uiActions from '../../services/ui/actions';
import * as helpers from '../../helpers';
class EditCommand extends React.Component{
constructor(props){
super(props)
this.state = {
id: helpers.generateGuid(),
icon: 'power_settings_new',
command: '{"url":"https://myserver.local:8080/sendCommand/power"}'
}
}
componentDidMount(){
if (this.props.command){
this.props.uiActions.setWindowTitle("Edit command");
this.setState(this.props.command);
} else {
this.props.uiActions.setWindowTitle("Create command");
}
}
handleSubmit(e){
e.preventDefault();
this.props.pusherActions.setCommand({
id: this.state.id,
icon: this.state.icon,
command: this.state.command
});
window.history.back();
return false;
}
render(){
return (
<Modal className="modal--create-command">
<h1>Create command</h1>
<form onSubmit={(e) => this.handleSubmit(e)}>
<div className="field text white">
<div className="name">
Icon
</div>
<div className="input">
<input
type="text"
name="icon"
value={this.state.icon}
onChange={ e => this.setState({ icon: e.target.value })}
/>
<div className="description">
Material icon, see <a href="https://material.io/tools/icons/?style=baseline" target="_blank" noopener="true">docs</a>
</div>
</div>
</div>
<div className="field textarea white">
<div className="name tooltip">
Command
<div className="tooltip__content">
Ajax configuration (JSON string)
</div>
</div>
<div className="input">
<textarea
name="command"
value={this.state.command}
onChange={ e => this.setState({ command: e.target.value })}>
</textarea>
</div>
</div>
<div className="actions centered-text">
<button type="submit" className="primary large">Save</button>
</div>
</form>
</Modal>
)
}
}
const mapStateToProps = (state, ownProps) => {
var id = ownProps.params.id;
return {
command: (id && state.pusher.commands && state.pusher.commands[id] !== undefined ? state.pusher.commands[id] : null)
}
}
const mapDispatchToProps = (dispatch) => {
return {
pusherActions: bindActionCreators(pusherActions, dispatch),
uiActions: bindActionCreators(uiActions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(EditCommand)

View File

@ -21,7 +21,6 @@ class ShareConfiguration extends React.Component {
spotify: false,
lastfm: false,
genius: false,
snapcast_client_commands: false,
ui: false
};
}
@ -66,9 +65,6 @@ class ShareConfiguration extends React.Component {
if (this.state.ui){
configuration.ui = this.props.ui;
}
if (this.state.snapcast_client_commands){
configuration.snapcast_client_commands = this.props.snapcast_client_commands;
}
for (var recipient of this.state.recipients){
this.props.pusherActions.deliverMessage(
@ -149,17 +145,6 @@ class ShareConfiguration extends React.Component {
</span>
</label>
{this.props.snapcast_client_commands ? <label>
<input
type="checkbox"
name="snapcast_client_commands"
checked={this.state.snapcast_client_commands}
onChange={ e => this.setState({ snapcast_client_commands: !this.state.snapcast_client_commands })} />
<span className="label">
Snapcast client commands
</span>
</label> : null}
{this.props.spotify_me && this.props.spotify_authorization ? <label>
<input
type="checkbox"
@ -212,7 +197,6 @@ const mapStateToProps = (state, ownProps) => {
genius_me: state.genius.me,
lastfm_authorization: state.lastfm.authorization,
lastfm_me: state.lastfm.me,
snapcast_client_commands: state.snapcast.client_commands,
ui: state.ui,
connection_id: state.pusher.connection_id,
connections: state.pusher.connections

View File

@ -79,8 +79,8 @@
width: 18%;
padding-right: 2%;
float: left;
opacity: 0.5;
padding-top: 8px;
color: $mid_grey;
}
.description {