Snapcast using indexes; Broadlink serversidde API
This commit is contained in:
@ -40,6 +40,9 @@ class Extension( ext.Extension ):
|
||||
schema['snapcast_enabled'] = config.Boolean()
|
||||
schema['snapcast_host'] = config.String()
|
||||
schema['snapcast_port'] = config.Integer()
|
||||
schema['broadlink_enabled'] = config.Boolean()
|
||||
schema['broadlink_host'] = config.String()
|
||||
schema['broadlink_port'] = config.Integer()
|
||||
return schema
|
||||
|
||||
def setup(self, registry):
|
||||
|
||||
@ -181,6 +181,41 @@ class IrisCore(object):
|
||||
return
|
||||
|
||||
|
||||
##
|
||||
# Send a command to the broadlink API
|
||||
# Credentials for this API are defined in Mopidy config file.
|
||||
#
|
||||
# @return String
|
||||
##
|
||||
def send_broadlink_command(self, *args, **kwargs):
|
||||
callback = kwargs.get('callback', None)
|
||||
data = kwargs.get('data', {})
|
||||
|
||||
# Use client_id and client_secret from config
|
||||
# This was introduced in Mopidy-Spotify 3.1.0
|
||||
url = 'http://'+self.config['iris']['broadlink_host']+':'+str(self.config['iris']['broadlink_port'])+"/"
|
||||
url+= data['command']
|
||||
|
||||
try:
|
||||
http_client = tornado.httpclient.HTTPClient()
|
||||
request = tornado.httpclient.HTTPRequest(url)
|
||||
response = http_client.fetch(request)
|
||||
response = json.loads(response.body)
|
||||
|
||||
if (callback):
|
||||
callback(response)
|
||||
else:
|
||||
return response
|
||||
|
||||
except urllib2.HTTPError as e:
|
||||
error = json.loads(e.read())
|
||||
error = {'message': 'Could not send Broadlink command: '+error['error_description']}
|
||||
|
||||
if (callback):
|
||||
callback(False, error)
|
||||
else:
|
||||
return error
|
||||
|
||||
|
||||
|
||||
##
|
||||
|
||||
@ -8,3 +8,6 @@ genius_authorization_url = https://jamesbarnsley.co.nz/iris/auth_genius.php
|
||||
snapcast_enabled = false
|
||||
snapcast_host = localhost
|
||||
snapcast_port = 1705
|
||||
broadlink_enabled = false
|
||||
broadlink_host = localhost
|
||||
broadlink_port = 8080
|
||||
|
||||
@ -4732,7 +4732,7 @@ $icon-websocket: \e920;
|
||||
.output-control.disabled > .control {
|
||||
opacity: 0.2;
|
||||
cursor: not-allowed; }
|
||||
.output-control .outputs {
|
||||
.output-control__items {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
right: -150px;
|
||||
@ -4745,7 +4745,7 @@ $icon-websocket: \e920;
|
||||
border-radius: 3px;
|
||||
box-shadow: 2px 3px 6px rgba(0, 0, 0, 0.2);
|
||||
z-index: 9; }
|
||||
.output-control .outputs:after {
|
||||
.output-control__items:after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 6px;
|
||||
@ -4757,39 +4757,116 @@ $icon-websocket: \e920;
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
right: 159px; }
|
||||
.output-control .outputs .no-results {
|
||||
.output-control__items .no-results {
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
font-weight: normal; }
|
||||
.output-control .outputs .output {
|
||||
padding: 16px 20px 12px;
|
||||
position: relative; }
|
||||
.output-control .outputs .output:not(:first-child) {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05); }
|
||||
.output-control .outputs .output .name {
|
||||
padding-bottom: 5px; }
|
||||
.output-control .outputs .output .actions {
|
||||
float: right; }
|
||||
.output-control .outputs .output .actions .action {
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
-moz-transition: all 0.2s ease-in-out;
|
||||
-o-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
color: #a3a19f; }
|
||||
.output-control .outputs .output .actions .action:hover {
|
||||
color: #181818; }
|
||||
.output-control .outputs .output .volume-control {
|
||||
.output-control__item {
|
||||
padding: 16px 20px 12px;
|
||||
position: relative; }
|
||||
.output-control__item:not(:first-child) {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05); }
|
||||
.output-control__item__name {
|
||||
padding-bottom: 5px; }
|
||||
.output-control__item__actions {
|
||||
float: right; }
|
||||
.output-control__item__actions .action {
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
-moz-transition: all 0.2s ease-in-out;
|
||||
-o-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
color: #a3a19f; }
|
||||
.output-control__item__actions .action:hover {
|
||||
color: #181818; }
|
||||
.output-control__item__details {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 50px; }
|
||||
.output-control__item__power {
|
||||
position: relative;
|
||||
position: absolute !important;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
width: 18px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
padding: 2px 0 6px 0;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
color: #FFFFFF;
|
||||
background: #f4f2f0;
|
||||
cursor: pointer; }
|
||||
|
||||
@keyframes click_feedback {
|
||||
0% {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
opacity: 1; }
|
||||
100% {
|
||||
top: 25%;
|
||||
left: -25%;
|
||||
width: 150%;
|
||||
padding-bottom: 150%;
|
||||
opacity: 0; } }
|
||||
.output-control__item__power:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 22px; }
|
||||
.output-control .outputs .output .volume-control .mute-control {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
padding: 2px;
|
||||
color: #000000; }
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
border-radius: 50%;
|
||||
margin-top: -50%;
|
||||
pointer-events: none; }
|
||||
.output-control__item__power:focus:after, .output-control__item__power:active:after {
|
||||
animation-name: click_feedback;
|
||||
animation-duration: 0.2s;
|
||||
animation-timing-function: ease-out;
|
||||
animation-iteration-count: 1; }
|
||||
.output-control__item__power--on {
|
||||
color: #cf2d2d; }
|
||||
.output-control__item__power--off {
|
||||
color: #47af2a; }
|
||||
.output-control__item__volume .mute-control {
|
||||
position: relative;
|
||||
position: absolute !important;
|
||||
top: -2px;
|
||||
left: 24px;
|
||||
padding: 2px;
|
||||
color: #000000; }
|
||||
|
||||
@keyframes click_feedback {
|
||||
0% {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
opacity: 1; }
|
||||
100% {
|
||||
top: 25%;
|
||||
left: -25%;
|
||||
width: 150%;
|
||||
padding-bottom: 150%;
|
||||
opacity: 0; } }
|
||||
.output-control__item__volume .mute-control:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
border-radius: 50%;
|
||||
margin-top: -50%;
|
||||
pointer-events: none; }
|
||||
.output-control__item__volume .mute-control:focus:after, .output-control__item__volume .mute-control:active:after {
|
||||
animation-name: click_feedback;
|
||||
animation-duration: 0.2s;
|
||||
animation-timing-function: ease-out;
|
||||
animation-iteration-count: 1; }
|
||||
@media (max-width: 800px) {
|
||||
.output-control .outputs {
|
||||
width: 86vw;
|
||||
@ -5973,12 +6050,10 @@ main header {
|
||||
content: '';
|
||||
clear: both;
|
||||
display: block; }
|
||||
.snapcast__client--disconnected .snapcast__client__header {
|
||||
opacity: 0.5; }
|
||||
.snapcast__client--expanded {
|
||||
background: rgba(255, 255, 255, 0.1); }
|
||||
background: rgba(255, 255, 255, 0.05); }
|
||||
.light-theme .snapcast__client--expanded {
|
||||
background: rgba(0, 0, 0, 0.1); }
|
||||
background: rgba(0, 0, 0, 0.05); }
|
||||
.snapcast__client--collapsed .snapcast__client__header:hover {
|
||||
background: rgba(255, 255, 255, 0.03); }
|
||||
.light-theme .snapcast__client--collapsed .snapcast__client__header:hover {
|
||||
@ -5997,6 +6072,8 @@ main header {
|
||||
padding: 14px 10px; }
|
||||
.snapcast__client__header__icons .icon {
|
||||
padding-left: 6px; }
|
||||
.snapcast__client__header__icons .icon.disconnected {
|
||||
color: #cf2d2d; }
|
||||
.snapcast__client__details {
|
||||
position: relative;
|
||||
padding: 0 14px 14px 14px; }
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -98,7 +98,7 @@
|
||||
|
||||
// Release details
|
||||
// These are automatically injected to built HTML
|
||||
var build = "1537819388";
|
||||
var build = "1537907593";
|
||||
var version = "3.26.2";
|
||||
|
||||
// Construct the script tag
|
||||
|
||||
5
src/js/bootstrap.js
vendored
5
src/js/bootstrap.js
vendored
@ -85,7 +85,10 @@ var initialState = {
|
||||
authorization_url: 'https://jamesbarnsley.co.nz/iris/auth_spotify.php'
|
||||
},
|
||||
snapcast: {
|
||||
|
||||
streams: {},
|
||||
groups: {},
|
||||
clients: {},
|
||||
server: null
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -4,11 +4,13 @@ import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import VolumeControl from './VolumeControl'
|
||||
import SnapcastPowerButton from './SnapcastPowerButton'
|
||||
import Icon from '../Icon'
|
||||
|
||||
import * as helpers from '../../helpers'
|
||||
import * as coreActions from '../../services/core/actions'
|
||||
import * as pusherActions from '../../services/pusher/actions'
|
||||
import * as helpers from '../../helpers';
|
||||
import * as coreActions from '../../services/core/actions';
|
||||
import * as pusherActions from '../../services/pusher/actions';
|
||||
import * as snapcastActions from '../../services/snapcast/actions';
|
||||
|
||||
class OutputControl extends React.Component{
|
||||
|
||||
@ -51,9 +53,9 @@ class OutputControl extends React.Component{
|
||||
for (var key in this.props.snapcast_clients){
|
||||
if (this.props.snapcast_clients.hasOwnProperty(key)){
|
||||
var client = this.props.snapcast_clients[key];
|
||||
if (client.connected){
|
||||
//if (client.connected){
|
||||
clients.push(client);
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,20 +65,24 @@ class OutputControl extends React.Component{
|
||||
<div>
|
||||
{
|
||||
clients.map(client => {
|
||||
var name = client.config.name ? client.config.name : client.host.name;
|
||||
|
||||
return (
|
||||
<div className="output snapcast-output" key={client.id}>
|
||||
<div className="name">
|
||||
{name}
|
||||
<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__details">
|
||||
{client.commands && client.commands.power_on ? <SnapcastPowerButton
|
||||
className="output-control__item__power output-control__item__power--on"
|
||||
client={client}
|
||||
/> : null}
|
||||
<VolumeControl
|
||||
className="output-control__item__volume"
|
||||
volume={client.volume}
|
||||
mute={client.mute}
|
||||
onVolumeChange={percent => this.props.snapcastActions.setClientVolume(client.id, percent)}
|
||||
onMuteChange={mute => this.props.snapcastActions.setClientMute(client.id, mute)}
|
||||
/>
|
||||
</div>
|
||||
<VolumeControl
|
||||
className="client-volume-control"
|
||||
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)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
@ -88,35 +94,37 @@ class OutputControl extends React.Component{
|
||||
var local_streaming = null;
|
||||
if (this.props.http_streaming_enabled){
|
||||
local_streaming = (
|
||||
<div className="output icecast-output">
|
||||
<div className="actions">
|
||||
<span className="action" onClick={e => this.props.coreActions.cachebustHttpStream()}>
|
||||
<div className="output-control__item outputs__item--icecast">
|
||||
<div className="output-control__item__actions">
|
||||
<span className="output-control__item__action" onClick={e => this.props.coreActions.cachebustHttpStream()}>
|
||||
<Icon name="refresh" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="name">
|
||||
<div className="output-control__item__name">
|
||||
Local browser
|
||||
</div>
|
||||
<VolumeControl
|
||||
className="client-volume-control"
|
||||
volume={this.props.http_streaming_volume}
|
||||
mute={this.props.http_streaming_mute}
|
||||
onVolumeChange={percent => this.props.coreActions.set({http_streaming_volume: percent})}
|
||||
onMuteChange={mute => this.props.coreActions.set({http_streaming_mute: mute})}
|
||||
/>
|
||||
<div className="output-control__item__details">
|
||||
<VolumeControl
|
||||
className="output-control__item__volume"
|
||||
volume={this.props.http_streaming_volume}
|
||||
mute={this.props.http_streaming_mute}
|
||||
onVolumeChange={percent => this.props.coreActions.set({http_streaming_volume: percent})}
|
||||
onMuteChange={mute => this.props.coreActions.set({http_streaming_mute: mute})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!local_streaming && !snapcast_clients){
|
||||
return (
|
||||
<div className="outputs">
|
||||
<div className="output-control__items">
|
||||
<p className="no-results">No outputs</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="outputs">
|
||||
<div className="output-control__items">
|
||||
{local_streaming}
|
||||
{snapcast_clients}
|
||||
</div>
|
||||
@ -159,13 +167,14 @@ const mapStateToProps = (state, ownProps) => {
|
||||
http_streaming_mute: state.core.http_streaming_mute,
|
||||
snapcast_enabled: (state.pusher.config ? state.pusher.config.snapcast_enabled : null),
|
||||
pusher_connected: state.pusher.connected,
|
||||
snapcast_clients: state.pusher.snapcast_clients
|
||||
snapcast_clients: state.snapcast.clients
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
coreActions: bindActionCreators(coreActions, dispatch),
|
||||
snapcastActions: bindActionCreators(snapcastActions, dispatch),
|
||||
pusherActions: bindActionCreators(pusherActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
44
src/js/components/Fields/SnapcastPowerButton.js
Executable file
44
src/js/components/Fields/SnapcastPowerButton.js
Executable file
@ -0,0 +1,44 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import Icon from '../Icon'
|
||||
|
||||
import * as helpers from '../../helpers';
|
||||
|
||||
class SnapcastPowerButton extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick(e){
|
||||
var command = JSON.parse(this.props.client.commands.power_on);
|
||||
$.ajax(command);
|
||||
}
|
||||
|
||||
render(){
|
||||
var classname = "snapcast-power-button";
|
||||
if (this.props.className){
|
||||
classname += " "+this.props.className;
|
||||
}
|
||||
return (
|
||||
<span className={classname} onClick={e => this.handleClick(e)}>
|
||||
<Icon name="power_settings_new" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SnapcastPowerButton)
|
||||
@ -61,7 +61,13 @@ class Snapcast extends React.Component{
|
||||
|
||||
renderClientsList(group, groups){
|
||||
|
||||
if (!group.clients || group.clients.length <= 0){
|
||||
if (!this.props.show_disconnected_clients && group.clients){
|
||||
var clients = helpers.applyFilter('connected', true, group.clients);
|
||||
} else {
|
||||
var clients = group.clients;
|
||||
}
|
||||
|
||||
if (!clients || clients.length <= 0){
|
||||
return (
|
||||
<div className="text grey-text">
|
||||
No clients
|
||||
@ -72,9 +78,7 @@ class Snapcast extends React.Component{
|
||||
return (
|
||||
<div className="list snapcast__clients">
|
||||
{
|
||||
group.clients.map(client => {
|
||||
var name = client.config.name ? client.config.name : client.host.name;
|
||||
|
||||
clients.map(client => {
|
||||
var class_name = "list__item snapcast__client";
|
||||
if (client.connected){
|
||||
class_name += " snapcast__client--connected";
|
||||
@ -86,7 +90,7 @@ class Snapcast extends React.Component{
|
||||
return (
|
||||
<div className={class_name+" snapcast__client--expanded"} key={client.id}>
|
||||
<div className="snapcast__client__header" onClick={e => this.toggleClientExpanded(client.id)}>
|
||||
{name}
|
||||
{client.name}
|
||||
<div className="snapcast__client__header__icons">
|
||||
{!client.connected ? <Icon name="power_off" className="disconnected" /> : null}
|
||||
<Icon name="expand_less" />
|
||||
@ -100,7 +104,7 @@ class Snapcast extends React.Component{
|
||||
<div className="input">
|
||||
<TextField
|
||||
onChange={value => this.props.snapcastActions.setClientName(client.id, value)}
|
||||
value={name}
|
||||
value={client.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -132,8 +136,8 @@ class Snapcast extends React.Component{
|
||||
<div className="input">
|
||||
<VolumeControl
|
||||
className="snapcast__client__volume-control"
|
||||
volume={client.config.volume.percent}
|
||||
mute={client.config.volume.muted}
|
||||
volume={client.volume}
|
||||
mute={client.mute}
|
||||
onVolumeChange={percent => this.props.snapcastActions.setClientVolume(client.id, percent)}
|
||||
onMuteChange={mute => this.props.snapcastActions.setClientMute(client.id, mute)}
|
||||
/>
|
||||
@ -146,42 +150,44 @@ class Snapcast extends React.Component{
|
||||
<div className="input">
|
||||
<LatencyControl
|
||||
max="100"
|
||||
value={client.config.latency}
|
||||
value={client.latency}
|
||||
onChange={value => this.props.snapcastActions.setClientLatency(client.id, parseInt(value))}
|
||||
/>
|
||||
<TextField
|
||||
className="tiny"
|
||||
type="number"
|
||||
onChange={value => this.props.snapcastActions.setClientLatency(client.id, parseInt(value))}
|
||||
value={String(client.config.latency)}
|
||||
value={String(client.latency)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="name tooltip">
|
||||
Power on command
|
||||
Power on request
|
||||
<span className="tooltip__content">
|
||||
Fire this HTTP request to turn power on
|
||||
</span>
|
||||
</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
onChange={value => console.log(value)}
|
||||
value="http://localhost:8080/sendCommand/power_on"
|
||||
onChange={value => this.props.snapcastActions.setClientCommand(client.id, "power_on", value)}
|
||||
value={client.power_on_command ? client.power_on_command : ""}
|
||||
placeholder="Leave empty to disable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="name tooltip">
|
||||
Power off command
|
||||
Power off request
|
||||
<span className="tooltip__content">
|
||||
Fire this HTTP request to turn power off
|
||||
</span>
|
||||
</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
onChange={value => console.log(value)}
|
||||
value="http://localhost:8080/sendCommand/power_off"
|
||||
onChange={value => this.props.snapcastActions.setClientCommand(client.id, "power_off", value)}
|
||||
value={client.power_off_command ? client.power_off_command : ""}
|
||||
placeholder="Leave empty to disable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -192,7 +198,7 @@ class Snapcast extends React.Component{
|
||||
return (
|
||||
<div className={class_name+" snapcast__client--collapsed"} key={client.id}>
|
||||
<div className="snapcast__client__header" onClick={e => this.toggleClientExpanded(client.id)}>
|
||||
{name}
|
||||
{client.name}
|
||||
<div className="snapcast__client__header__icons">
|
||||
{!client.connected ? <Icon name="power_off" className="disconnected" /> : null}
|
||||
<Icon name="expand_more" />
|
||||
@ -214,45 +220,17 @@ class Snapcast extends React.Component{
|
||||
)
|
||||
}
|
||||
|
||||
if (!this.props.clients || !this.props.groups){
|
||||
return (
|
||||
<div className="lazy-loader body-loader loading">
|
||||
<div className="loader"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
var streams = [];
|
||||
for (var key in this.props.streams){
|
||||
if (this.props.streams.hasOwnProperty(key)){
|
||||
streams.push(this.props.streams[key]);
|
||||
for (var id in this.props.streams){
|
||||
if (this.props.streams.hasOwnProperty(id)){
|
||||
streams.push(this.props.streams[id]);
|
||||
}
|
||||
}
|
||||
|
||||
// Construct a simple array of our groups index
|
||||
var groups = [];
|
||||
for (var group_id in this.props.groups){
|
||||
if (this.props.groups.hasOwnProperty(group_id)){
|
||||
var group = this.props.groups[group_id];
|
||||
|
||||
// Merge the group's clients into this group (also as a simple array)
|
||||
var clients = [];
|
||||
for (var i = 0; i < group.clients_ids.length; i++){
|
||||
if (this.props.clients.hasOwnProperty(group.clients_ids[i])){
|
||||
var client = this.props.clients[group.clients_ids[i]];
|
||||
if (client.connected || this.props.show_disconnected_clients){
|
||||
clients.push(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups.push(Object.assign(
|
||||
{},
|
||||
group,
|
||||
{
|
||||
clients: clients
|
||||
}
|
||||
));
|
||||
for (var id in this.props.groups){
|
||||
if (this.props.groups.hasOwnProperty(id)){
|
||||
groups.push(this.props.groups[id]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -286,11 +264,13 @@ class Snapcast extends React.Component{
|
||||
{
|
||||
groups.map(group => {
|
||||
|
||||
group = helpers.collate(group, {clients: this.props.clients});
|
||||
|
||||
// Average our clients' volume for an overall group volume
|
||||
var group_volume = 0;
|
||||
for (var i = 0; i < group.clients.length; i++){
|
||||
var client = group.clients[i];
|
||||
group_volume += client.config.volume.percent;
|
||||
group_volume += client.volume;
|
||||
}
|
||||
group_volume = group_volume / group.clients.length;
|
||||
|
||||
@ -302,7 +282,7 @@ class Snapcast extends React.Component{
|
||||
</div>
|
||||
<div className="input">
|
||||
<div className="text">
|
||||
{group.name ? group.name : 'Group '+group.id.substring(0,3)}
|
||||
{group.name}
|
||||
<span className="grey-text">({group.id})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -701,6 +701,91 @@ export let formatTrack = function(data){
|
||||
return track;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a snapcast client object into a universal format
|
||||
*
|
||||
* @param data obj
|
||||
* @return obj
|
||||
**/
|
||||
export let formatClient = function(data){
|
||||
var client = {}
|
||||
var fields = [
|
||||
'id',
|
||||
'connected',
|
||||
'name',
|
||||
'host_name',
|
||||
'volume',
|
||||
'mute',
|
||||
'latency',
|
||||
'power_on_command',
|
||||
'power_off_command'
|
||||
];
|
||||
|
||||
for (var field of fields){
|
||||
if (data.hasOwnProperty(field)){
|
||||
client[field] = data[field];
|
||||
}
|
||||
}
|
||||
|
||||
if (data.config){
|
||||
if (client.latency === undefined && data.config.latency !== undefined){
|
||||
client.latency = data.config.latency;
|
||||
}
|
||||
|
||||
if (!client.name && data.config.name){
|
||||
client.name = data.config.name;
|
||||
}
|
||||
|
||||
if (data.config.volume){
|
||||
if (data.config.volume.percent){
|
||||
client.volume = data.config.volume.percent;
|
||||
}
|
||||
if (data.config.volume.muted){
|
||||
client.mute = data.config.volume.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (client.name === undefined && data.host && data.host.name){
|
||||
client.name = data.host.name;
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a snapcast client object into a universal format
|
||||
*
|
||||
* @param data obj
|
||||
* @return obj
|
||||
**/
|
||||
export let formatGroup = function(data){
|
||||
var group = {}
|
||||
var fields = [
|
||||
'id',
|
||||
'name',
|
||||
'mute',
|
||||
'stream_id',
|
||||
'clients_ids'
|
||||
];
|
||||
|
||||
for (var field of fields){
|
||||
if (data.hasOwnProperty(field)){
|
||||
group[field] = data[field];
|
||||
}
|
||||
}
|
||||
|
||||
if (!group.name && data.name){
|
||||
group.name = 'Group '+data.id.substring(0,3);
|
||||
}
|
||||
|
||||
if (group.mute === undefined && data.mute !== undefined){
|
||||
group.mute = data.mute;
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Collate an object with external references into a fully self-contained object
|
||||
@ -726,6 +811,7 @@ export let collate = function(obj, indexes = {}){
|
||||
if (obj.users_uris !== undefined) obj.users = [];
|
||||
if (obj.playlists_uris !== undefined) obj.playlists = [];
|
||||
if (obj.related_artists_uris !== undefined) obj.related_artists = [];
|
||||
if (obj.clients_ids !== undefined) obj.clients = [];
|
||||
|
||||
if (indexes.artists){
|
||||
if (obj.artists_uris){
|
||||
@ -809,6 +895,16 @@ export let collate = function(obj, indexes = {}){
|
||||
}
|
||||
}
|
||||
|
||||
if (indexes.clients){
|
||||
if (obj.clients_ids){
|
||||
for (var id of obj.clients_ids){
|
||||
if (indexes.clients[id]){
|
||||
obj.clients.push(indexes.clients[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
@ -1,49 +1,4 @@
|
||||
|
||||
export function connect(){
|
||||
return {
|
||||
type: 'SNAPCAST_CONNECT'
|
||||
}
|
||||
}
|
||||
|
||||
export function disconnect(){
|
||||
return {
|
||||
type: 'SNAPCAST_DISCONNECT'
|
||||
}
|
||||
}
|
||||
|
||||
export function reload(){
|
||||
return {
|
||||
type: 'SNAPCAST_RELOAD'
|
||||
}
|
||||
}
|
||||
|
||||
export function getConnections(){
|
||||
return {
|
||||
type: 'SNAPCAST_GET_CONNECTIONS'
|
||||
}
|
||||
}
|
||||
|
||||
export function connectionAdded(connection){
|
||||
return {
|
||||
type: 'SNAPCAST_CONNECTION_ADDED',
|
||||
connection: connection
|
||||
}
|
||||
}
|
||||
|
||||
export function connectionChanged(connection){
|
||||
return {
|
||||
type: 'SNAPCAST_CONNECTION_CHANGED',
|
||||
connection: connection
|
||||
}
|
||||
}
|
||||
|
||||
export function connectionRemoved(connection){
|
||||
return {
|
||||
type: 'SNAPCAST_CONNECTION_REMOVED',
|
||||
connection: connection
|
||||
}
|
||||
}
|
||||
|
||||
export function getServer(){
|
||||
return {
|
||||
type: 'SNAPCAST_GET_SERVER'
|
||||
@ -66,11 +21,11 @@ export function setClientMute(id, mute){
|
||||
}
|
||||
}
|
||||
|
||||
export function setClientVolume(id, percent){
|
||||
export function setClientVolume(id, volume){
|
||||
return {
|
||||
type: 'SNAPCAST_SET_CLIENT_VOLUME',
|
||||
id: id,
|
||||
percent: percent
|
||||
volume: volume
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +37,15 @@ export function setClientLatency(id, latency){
|
||||
}
|
||||
}
|
||||
|
||||
export function setClientCommand(id, name, command){
|
||||
return {
|
||||
type: 'SNAPCAST_SET_CLIENT_COMMAND',
|
||||
id: id,
|
||||
name: name,
|
||||
command: command
|
||||
}
|
||||
}
|
||||
|
||||
export function setClientGroup(id, group_id){
|
||||
return {
|
||||
type: 'SNAPCAST_SET_CLIENT_GROUP',
|
||||
@ -132,12 +96,9 @@ export function setGroupVolume(id, percent, old_percent = 0){
|
||||
**/
|
||||
|
||||
export function serverLoaded(server){
|
||||
return serversLoaded([server]);
|
||||
}
|
||||
export function serversLoaded(servers){
|
||||
return {
|
||||
type: 'SNAPCAST_SERVERS_LOADED',
|
||||
servers: servers
|
||||
type: 'SNAPCAST_SERVER_LOADED',
|
||||
server: server
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,8 +117,8 @@ export function groupLoaded(group){
|
||||
}
|
||||
export function groupsLoaded(groups){
|
||||
return {
|
||||
type: 'SNAPCAST_SERVERS_LOADED',
|
||||
servers: group
|
||||
type: 'SNAPCAST_GROUPS_LOADED',
|
||||
groups: groups
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,8 @@ const SnapcastMiddleware = (function(){
|
||||
}
|
||||
|
||||
return store => next => action => {
|
||||
var snapcast = store.getState().snapcast;
|
||||
|
||||
switch(action.type){
|
||||
|
||||
case 'SNAPCAST_GET_SERVER':
|
||||
@ -32,7 +34,8 @@ const SnapcastMiddleware = (function(){
|
||||
method: 'Server.GetStatus'
|
||||
},
|
||||
response => {
|
||||
store.dispatch(snapcastActions.serverLoaded(response.server));
|
||||
console.log(response);
|
||||
store.dispatch(snapcastActions.serverLoaded(response.server));
|
||||
},
|
||||
error => {
|
||||
store.dispatch(coreActions.handleException(
|
||||
@ -44,45 +47,66 @@ const SnapcastMiddleware = (function(){
|
||||
break
|
||||
|
||||
case 'SNAPCAST_SERVER_LOADED':
|
||||
var groups = {};
|
||||
var clients = {};
|
||||
var streams = {};
|
||||
store.dispatch(snapcastActions.groupsLoaded(action.server.groups));
|
||||
store.dispatch(snapcastActions.streamsLoaded(action.server.streams));
|
||||
|
||||
// Loop all the groups
|
||||
for (var i = 0; i < action.server.groups.length; i++){
|
||||
var group = action.server.groups[i];
|
||||
var clients_ids = [];
|
||||
// Snapcast double-nests the server object
|
||||
action.server = action.server.server;
|
||||
|
||||
// And now this groups' clients
|
||||
for (var j = 0; j < group.clients.length; j++){
|
||||
var client = group.clients[j];
|
||||
clients[client.id] = client;
|
||||
clients_ids.push(client.id);
|
||||
}
|
||||
|
||||
groups[group.id] = {
|
||||
id: group.id,
|
||||
muted: group.muted,
|
||||
name: group.name,
|
||||
stream_id: group.stream_id,
|
||||
clients_ids: clients_ids
|
||||
}
|
||||
}
|
||||
|
||||
// Loop all the streams
|
||||
for (var i = 0; i < action.server.streams.length; i++){
|
||||
var stream = action.server.streams[i];
|
||||
streams[stream.id] = stream;
|
||||
}
|
||||
|
||||
action.clients = clients;
|
||||
action.groups = groups;
|
||||
action.streams = streams;
|
||||
next(action);
|
||||
break
|
||||
|
||||
|
||||
case 'SNAPCAST_GROUPS_LOADED':
|
||||
var groups_index = Object.assign({}, snapcast.groups);
|
||||
var groups_loaded = [];
|
||||
var clients_loaded = [];
|
||||
|
||||
for (var raw_group of action.groups){
|
||||
var group = helpers.formatGroup(raw_group);
|
||||
|
||||
if (groups_index[group.id]){
|
||||
group = Object.assign({}, groups_index[group.id], group);
|
||||
}
|
||||
|
||||
if (raw_group.clients){
|
||||
group.clients_ids = helpers.arrayOf('id', raw_group.clients);
|
||||
clients_loaded = [...clients_loaded, ...raw_group.clients];
|
||||
}
|
||||
|
||||
groups_loaded.push(group);
|
||||
};
|
||||
|
||||
action.groups = groups_loaded;
|
||||
|
||||
if (clients_loaded.length > 0){
|
||||
store.dispatch(snapcastActions.clientsLoaded(clients_loaded));
|
||||
}
|
||||
|
||||
next(action);
|
||||
break
|
||||
|
||||
case 'SNAPCAST_CLIENTS_LOADED':
|
||||
var clients_index = Object.assign({}, snapcast.clients);
|
||||
var clients_loaded = [];
|
||||
|
||||
for (var raw_client of action.clients){
|
||||
var client = helpers.formatClient(raw_client);
|
||||
|
||||
if (clients_index[client.id]){
|
||||
client = Object.assign({}, clients_index[client.id], client);
|
||||
}
|
||||
|
||||
clients_loaded.push(client);
|
||||
}
|
||||
|
||||
action.clients = clients_loaded;
|
||||
|
||||
next(action);
|
||||
break;
|
||||
|
||||
case 'SNAPCAST_SET_CLIENT_NAME':
|
||||
var client = store.getState().snapcast.clients[action.id];
|
||||
var client = snapcast.clients[action.id];
|
||||
var data = {
|
||||
method: 'Client.SetName',
|
||||
params: {
|
||||
@ -95,15 +119,12 @@ const SnapcastMiddleware = (function(){
|
||||
store,
|
||||
data,
|
||||
response => {
|
||||
store.dispatch({
|
||||
type: 'SNAPCAST_CLIENT_UPDATED',
|
||||
key: action.id,
|
||||
client: {
|
||||
config: {
|
||||
name: response.name
|
||||
}
|
||||
store.dispatch(snapcastActions.clientLoaded(
|
||||
{
|
||||
id: action.id,
|
||||
name: response.name
|
||||
}
|
||||
});
|
||||
));
|
||||
}
|
||||
);
|
||||
break
|
||||
@ -116,7 +137,7 @@ const SnapcastMiddleware = (function(){
|
||||
id: action.id,
|
||||
volume: {
|
||||
muted: action.mute,
|
||||
percent: client.config.volume.percent,
|
||||
percent: client.volume,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -125,15 +146,13 @@ const SnapcastMiddleware = (function(){
|
||||
store,
|
||||
data,
|
||||
response => {
|
||||
store.dispatch({
|
||||
type: 'SNAPCAST_CLIENT_UPDATED',
|
||||
key: action.id,
|
||||
client: {
|
||||
config: {
|
||||
volume: response.volume
|
||||
}
|
||||
store.dispatch(snapcastActions.clientLoaded(
|
||||
{
|
||||
id: action.id,
|
||||
volume: response.volume.percent,
|
||||
mute: response.volume.muted
|
||||
}
|
||||
})
|
||||
));
|
||||
},
|
||||
error => {
|
||||
store.dispatch(coreActions.handleException(
|
||||
@ -146,14 +165,15 @@ const SnapcastMiddleware = (function(){
|
||||
break
|
||||
|
||||
case 'SNAPCAST_SET_CLIENT_VOLUME':
|
||||
var client = store.getState().snapcast.clients[action.id];
|
||||
var client = snapcast.clients[action.id];
|
||||
|
||||
var data = {
|
||||
method: 'Client.SetVolume',
|
||||
params: {
|
||||
id: action.id,
|
||||
volume: {
|
||||
muted: client.config.volume.muted,
|
||||
percent: action.percent
|
||||
muted: client.mute,
|
||||
percent: action.volume
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -162,15 +182,12 @@ const SnapcastMiddleware = (function(){
|
||||
store,
|
||||
data,
|
||||
response => {
|
||||
store.dispatch({
|
||||
type: 'SNAPCAST_CLIENT_UPDATED',
|
||||
key: action.id,
|
||||
client: {
|
||||
config: {
|
||||
volume: response.volume
|
||||
}
|
||||
store.dispatch(snapcastActions.clientLoaded(
|
||||
{
|
||||
id: action.id,
|
||||
volume: response.volume.percent
|
||||
}
|
||||
})
|
||||
));
|
||||
},
|
||||
error => {
|
||||
store.dispatch(coreActions.handleException(
|
||||
@ -193,18 +210,15 @@ const SnapcastMiddleware = (function(){
|
||||
}
|
||||
|
||||
request(
|
||||
store,
|
||||
data,
|
||||
store,
|
||||
data,
|
||||
response => {
|
||||
store.dispatch({
|
||||
type: 'SNAPCAST_CLIENT_UPDATED',
|
||||
key: action.id,
|
||||
client: {
|
||||
config: {
|
||||
latency: response.latency
|
||||
}
|
||||
store.dispatch(snapcastActions.clientLoaded(
|
||||
{
|
||||
id: action.id,
|
||||
latency: response.latency
|
||||
}
|
||||
})
|
||||
));
|
||||
},
|
||||
error => {
|
||||
store.dispatch(coreActions.handleException(
|
||||
@ -216,9 +230,18 @@ const SnapcastMiddleware = (function(){
|
||||
);
|
||||
break
|
||||
|
||||
case 'SNAPCAST_SET_CLIENT_COMMAND':
|
||||
var client = snapcast.clients[action.id];
|
||||
if (!client.commands){
|
||||
client.commands = {};
|
||||
}
|
||||
client.commands[action.name] = action.command;
|
||||
store.dispatch(snapcastActions.clientLoaded(client));
|
||||
break
|
||||
|
||||
case 'SNAPCAST_SET_CLIENT_GROUP':
|
||||
|
||||
var group = store.getState().snapcast.groups[action.group_id];
|
||||
var group = snapcast.groups[action.group_id];
|
||||
var clients_ids = group.clients_ids;
|
||||
var clients_ids_index = clients_ids.indexOf(action.id);
|
||||
|
||||
@ -296,13 +319,12 @@ const SnapcastMiddleware = (function(){
|
||||
store,
|
||||
data,
|
||||
response => {
|
||||
store.dispatch({
|
||||
type: 'SNAPCAST_GROUP_UPDATED',
|
||||
key: action.id,
|
||||
group: {
|
||||
store.dispatch(snapcastActions.groupLoaded(
|
||||
{
|
||||
id: action.id,
|
||||
stream_id: action.stream_id
|
||||
}
|
||||
})
|
||||
));
|
||||
},
|
||||
error => {
|
||||
store.dispatch(coreActions.handleException(
|
||||
@ -328,13 +350,12 @@ const SnapcastMiddleware = (function(){
|
||||
store,
|
||||
data,
|
||||
response => {
|
||||
store.dispatch({
|
||||
type: 'SNAPCAST_GROUP_UPDATED',
|
||||
key: action.id,
|
||||
group: {
|
||||
store.dispatch(snapcastActions.groupLoaded(
|
||||
{
|
||||
id: action.id,
|
||||
muted: response.mute
|
||||
}
|
||||
})
|
||||
));
|
||||
},
|
||||
error => {
|
||||
store.dispatch(coreActions.handleException(
|
||||
@ -348,38 +369,37 @@ const SnapcastMiddleware = (function(){
|
||||
|
||||
case 'SNAPCAST_SET_GROUP_VOLUME':
|
||||
var clients_to_update = [];
|
||||
var group = store.getState().snapcast.groups[action.id];
|
||||
var group = snapcast.groups[action.id];
|
||||
var change = action.percent - action.old_percent;
|
||||
|
||||
for (var i = 0; i < group.clients_ids.length; i++){
|
||||
for (var client_id of group.clients_ids){
|
||||
|
||||
// Apply the change proportionately to each client
|
||||
var client = store.getState().snapcast.clients[group.clients_ids[i]];
|
||||
var current_percent = client.config.volume.percent;
|
||||
var new_percent = current_percent + change;
|
||||
var client = snapcast.clients[client_id];
|
||||
var current_volume = client.volume;
|
||||
var new_volume = current_volume + change;
|
||||
|
||||
// Only change if the client is within min/max limits
|
||||
if ((change > 0 && current_percent < 100) || (change < 0 && current_percent > 0)){
|
||||
if ((change > 0 && current_volume < 100) || (change < 0 && current_volume > 0)){
|
||||
clients_to_update.push({
|
||||
id: client.id,
|
||||
percent: new_percent
|
||||
volume: new_volume
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Loop our required changes, and post each to Snapcast
|
||||
for (var i = 0; i < clients_to_update.length; i++){
|
||||
var update = clients_to_update[i];
|
||||
var percent = update.percent + ((group.clients_ids.length - clients_to_update.length) * change);
|
||||
for (var client_to_update of clients_to_update){
|
||||
var volume = client_to_update.volume + ((group.clients_ids.length - clients_to_update.length) * change);
|
||||
|
||||
// Make sure we're not creating an impossible percent
|
||||
if (percent < 0){
|
||||
percent = 0;
|
||||
} else if (percent > 100){
|
||||
percent = 100;
|
||||
if (volume < 0){
|
||||
volume = 0;
|
||||
} else if (volume > 100){
|
||||
volume = 100;
|
||||
}
|
||||
|
||||
store.dispatch(snapcastActions.setClientVolume(update.id, percent));
|
||||
store.dispatch(snapcastActions.setClientVolume(client_to_update.id, volume));
|
||||
}
|
||||
|
||||
// This action is irrelevant to us, pass it on to the next middleware
|
||||
|
||||
@ -2,27 +2,31 @@
|
||||
export default function reducer(snapcast = {}, action){
|
||||
switch (action.type){
|
||||
|
||||
case 'SNAPCAST_SERVER_LOADED':
|
||||
var server = Object.assign({}, action.server);
|
||||
return Object.assign({}, snapcast, { server: server });
|
||||
|
||||
case 'SNAPCAST_CLIENTS_LOADED':
|
||||
var clients = Object.assign({}, snapcast.clients);
|
||||
for (var client of action.clients){
|
||||
clients[client.uri] = client;
|
||||
clients[client.id] = client;
|
||||
}
|
||||
return Object.assign({}, snapcast, { clients: clients });
|
||||
|
||||
case 'SNAPCAST_SOURCES_LOADED':
|
||||
var sources = Object.assign({}, snapcast.sources);
|
||||
for (var source of action.sources){
|
||||
sources[source.uri] = source;
|
||||
}
|
||||
return Object.assign({}, snapcast, { sources: sources });
|
||||
|
||||
case 'SNAPCAST_GROUPS_LOADED':
|
||||
var groups = Object.assign({}, snapcast.groups);
|
||||
for (var group of action.groups){
|
||||
groups[group.uri] = group;
|
||||
groups[group.id] = group;
|
||||
}
|
||||
return Object.assign({}, snapcast, { groups: groups });
|
||||
|
||||
case 'SNAPCAST_STREAMS_LOADED':
|
||||
var streams = Object.assign({}, snapcast.streams);
|
||||
for (var stream of action.streams){
|
||||
streams[stream.id] = stream;
|
||||
}
|
||||
return Object.assign({}, snapcast, { streams: streams });
|
||||
|
||||
default:
|
||||
return snapcast
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.outputs {
|
||||
&__items {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
right: -150px;
|
||||
@ -43,45 +43,72 @@
|
||||
opacity: 0.5;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.output {
|
||||
padding: 16px 20px 12px;
|
||||
&__item {
|
||||
padding: 16px 20px 12px;
|
||||
position: relative;
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
&__name {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
float: right;
|
||||
|
||||
.action {
|
||||
@include animate();
|
||||
cursor: pointer;
|
||||
color: $mid_grey;
|
||||
|
||||
&:hover {
|
||||
color: $dark_grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__details {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid rgba(0,0,0,0.05);
|
||||
&__power {
|
||||
@include click_feedback(rgba(0,0,0,0.5));
|
||||
position: absolute !important;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
width: 18px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
padding: 2px 0 6px 0;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
background: $faint_grey;
|
||||
cursor: pointer;
|
||||
|
||||
&--on {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.name {
|
||||
padding-bottom: 5px;
|
||||
&--off {
|
||||
color: $green;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
float: right;
|
||||
|
||||
.action {
|
||||
@include animate();
|
||||
cursor: pointer;
|
||||
color: $mid_grey;
|
||||
|
||||
&:hover {
|
||||
color: $dark_grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.volume-control {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 22px;
|
||||
|
||||
.mute-control {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
padding: 2px;
|
||||
color: $black;
|
||||
}
|
||||
&__volume {
|
||||
.mute-control {
|
||||
@include click_feedback(rgba(0,0,0,0.5));
|
||||
position: absolute !important;
|
||||
top: -2px;
|
||||
left: 24px;
|
||||
padding: 2px;
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,17 +46,11 @@
|
||||
@include clearfix();
|
||||
padding: 0;
|
||||
|
||||
&--disconnected {
|
||||
.snapcast__client__header {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&--expanded {
|
||||
background: rgba(255,255,255,0.1);
|
||||
background: rgba(255,255,255,0.05);
|
||||
|
||||
.light-theme & {
|
||||
background: rgba(0,0,0,0.1);
|
||||
background: rgba(0,0,0,0.05);
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,6 +80,10 @@
|
||||
|
||||
.icon {
|
||||
padding-left: 6px;
|
||||
|
||||
&.disconnected {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,7 +140,7 @@ $bp_shallow: 650px;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
@mixin click_feedback($duration: 0.2s, $easing: ease-out){
|
||||
@mixin click_feedback($colour: rgba(255,255,255,0.5), $duration: 0.2s, $easing: ease-out){
|
||||
position: relative;
|
||||
|
||||
@keyframes click_feedback {
|
||||
@ -164,7 +164,7 @@ $bp_shallow: 650px;
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: rgba(255,255,255,0.5);
|
||||
background: $colour;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
border-radius: 50%;
|
||||
|
||||
Reference in New Issue
Block a user