Added new Snapcast webpanel plugin

This commit is contained in:
Fabio Manganiello 2019-06-10 00:55:23 +02:00
parent 33d55dcd93
commit 95a9c22618
11 changed files with 540 additions and 2 deletions

View file

@ -12,7 +12,7 @@
display: inline-block;
text-align: center;
user-select: none;
padding-top: 1rem;
padding: .5rem 0;
input[type=checkbox] {
display: none !important;
@ -39,7 +39,7 @@
border-radius: 50%;
box-shadow: $switch-shadow-1;
display: block;
margin: 0 auto;
margin: .5rem auto 0 auto;
font-size: 1.4em;
transition: all 350ms ease-in;

View file

@ -3,9 +3,13 @@ $default-bg: white !default;
$default-bg-2: #f4f5f6 !default;
$default-bg-3: #f1f3f2 !default;
$default-bg-4: #edf0ee !default;
$default-bg-5: #f8f8f8 !default;
$default-fg: black !default;
$default-fg-2: #333333 !default;
$default-fg-3: #888888 !default;
$default-font-size: 1.5rem !default;
$default-shadow: 2px 2px 2px #ccc !default;
$default-hover-fg: #35b870 !default;
$default-font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif !default;
$default-border: 1px solid #e1e4e8 !default;

View file

@ -0,0 +1,106 @@
@import 'common/vars';
@import 'common/mixins';
@import 'common/layout';
$host-border: $default-border-2;
$host-shadow: $default-shadow;
.music-snapcast-container {
.host {
width: 95%;
margin: 2rem auto;
border: $host-border;
border-radius: 1rem;
box-shadow: $host-shadow;
.head {
padding: 1rem .5rem;
background: $default-bg-4;
border-bottom: $host-border;
border-radius: 1rem 1rem 0 0;
display: flex;
align-items: center;
.name {
padding-left: .5rem;
text-transform: uppercase;
&:hover {
color: $default-hover-fg;
cursor: pointer;
}
}
button {
padding: 0;
border: 0;
&:hover { color: $default-hover-fg; }
}
}
.group {
.head {
background: $default-bg-5;
border-radius: 0;
}
.head,
.client {
padding: 0 1rem;
}
.client {
display: flex;
align-items: center;
&.offline { color: $default-fg-3; }
&:hover { background: $hover-bg; }
.name {
&:hover {
color: $default-hover-fg;
cursor: pointer;
}
}
}
}
.icon { margin-right: 1rem; }
}
.modal {
.info {
.row {
&:hover { background: $hover-bg; }
.value {
text-align: right;
}
}
}
}
}
@media #{map-get($widths, 's')} {
.music-snapcast-container {
.modal {
width: 80vw;
}
}
}
@media #{map-get($widths, 'm')} {
.music-snapcast-container {
.modal {
width: 70vw;
}
}
}
@media #{map-get($widths, 'l')} {
.music-snapcast-container {
.modal {
width: 65vw;
}
}
}

View file

@ -0,0 +1,37 @@
Vue.component('music-snapcast-client', {
template: '#tmpl-music-snapcast-client',
props: {
config: { type: Object },
connected: { type: Boolean },
host: { type: Object },
id: { type: String },
groupId: { type: String },
lastSeen: { type: Object },
snapclient: { type: Object },
server: { type: Object },
bus: { type: Object },
},
methods: {
muteToggled: function(event) {
this.bus.$emit('client-mute-changed', {
id: this.id,
server: this.server,
value: !event.value,
});
},
volumeChanged: function(event) {
this.bus.$emit('client-volume-changed', {
id: this.id,
server: this.server,
value: parseInt(event.target.value),
});
},
},
});
Vue.component('music-snapcast-client-info', {
props: ['info'],
});

View file

@ -0,0 +1,27 @@
Vue.component('music-snapcast-group', {
template: '#tmpl-music-snapcast-group',
props: {
id: { type: String },
clients: { type: Object },
muted: { type: Boolean },
name: { type: String },
stream: { type: Object },
server: { type: Object },
bus: { type: Object },
},
methods: {
muteToggled: function(event) {
this.bus.$emit('group-mute-changed', {
id: this.id,
server: this.server,
value: !event.value,
});
},
},
});
Vue.component('music-snapcast-group-info', {
props: ['info'],
});

View file

@ -0,0 +1,20 @@
Vue.component('music-snapcast-host', {
template: '#tmpl-music-snapcast-host',
props: {
groups: { type: Object },
server: { type: Object },
streams: { type: Object },
bus: { type: Object },
},
data: function() {
return {
collapsed: false,
};
},
});
Vue.component('music-snapcast-host-info', {
props: ['info'],
});

View file

@ -0,0 +1,178 @@
Vue.component('music-snapcast', {
template: '#tmpl-music-snapcast',
props: ['config'],
data: function() {
return {
hosts: {},
modal: {
host: {
visible: false,
info: {},
},
group: {
visible: false,
info: {},
},
client: {
visible: false,
info: {},
},
},
bus: new Vue({}),
};
},
methods: {
refresh: async function() {
let hosts = await request('music.snapcast.get_backend_hosts');
let promises = Object.keys(hosts).map(
(host) => request('music.snapcast.status', {host: host, port: hosts[host]})
);
let statuses = await Promise.all(promises);
this.hosts = {};
for (const status of statuses) {
status.server.host.port = hosts[status.server.host.name];
var groups = {};
for (const group of status.groups) {
var clients = {};
for (const client of group.clients) {
clients[client.id] = client;
}
group.clients = clients;
groups[group.id] = group;
}
status.groups = groups;
var streams = {};
for (const stream of status.streams) {
streams[stream.id] = stream;
}
status.streams = streams;
Vue.set(this.hosts, status.server.host.name, status);
}
},
onClientUpdate: function(event) {
for (const groupId of Object.keys(this.hosts[event.host].groups)) {
if (event.client.id in this.hosts[event.host].groups[groupId].clients) {
this.hosts[event.host].groups[groupId].clients[event.client.id] = event.client;
}
}
},
onGroupStreamChange: function(event) {
this.hosts[event.host].groups[event.group].stream_id = event.stream;
},
onServerUpdate: function(event) {
this.refresh();
},
onStreamUpdate: function(event) {
this.streams[event.stream_id] = event.stream;
},
onClientVolumeChange: function(event) {
for (const groupId of Object.keys(this.hosts[event.host].groups)) {
if (event.client in this.hosts[event.host].groups[groupId].clients) {
if (event.volume != null) {
this.hosts[event.host].groups[groupId].clients[event.client].config.volume.percent = event.volume;
}
if (event.muted != null) {
this.hosts[event.host].groups[groupId].clients[event.client].config.volume.muted = event.muted;
}
}
}
},
onGroupMuteChange: function(event) {
this.hosts[event.host].groups[event.group].muted = event.muted;
},
modalShow: function(event) {
switch(event.type) {
case 'host':
this.modal[event.type].info = this.hosts[event.host];
break;
case 'group':
this.modal[event.type].info = this.hosts[event.host].groups[event.group];
break;
case 'client':
this.modal[event.type].info = this.hosts[event.host].groups[event.group].clients[event.client];
break;
}
this.modal[event.type].visible = true;
},
groupMute: async function(event) {
await request('music.snapcast.mute', {
group: event.id,
host: event.server.ip || event.server.name,
port: event.server.port,
mute: event.value,
});
this.hosts[event.server.name].groups[event.id].muted = event.value;
},
clientMute: async function(event) {
await request('music.snapcast.mute', {
client: event.id,
host: event.server.ip || event.server.name,
port: event.server.port,
mute: event.value,
});
for (const groupId of Object.keys(this.hosts[event.server.name].groups)) {
if (event.id in this.hosts[event.server.name].groups[groupId].clients) {
this.hosts[event.server.name].groups[groupId].clients[event.id].config.volume.muted = event.value;
}
}
},
clientSetVolume: async function(event) {
await request('music.snapcast.volume', {
client: event.id,
host: event.server.ip || event.server.name,
port: event.server.port,
volume: event.value,
});
for (const groupId of Object.keys(this.hosts[event.server.name].groups)) {
if (event.id in this.hosts[event.server.name].groups[groupId].clients) {
this.hosts[event.server.name].groups[groupId].clients[event.id].config.volume.percent = event.value;
}
}
},
},
created: function() {
this.refresh();
registerEventHandler(this.onClientUpdate,
'platypush.message.event.music.snapcast.ClientConnectedEvent',
'platypush.message.event.music.snapcast.ClientDisconnectedEvent',
'platypush.message.event.music.snapcast.ClientNameChangeEvent');
registerEventHandler(this.onGroupStreamChange, 'platypush.message.event.music.snapcast.GroupStreamChangeEvent');
registerEventHandler(this.onServerUpdate, 'platypush.message.event.music.snapcast.ServerUpdateEvent');
registerEventHandler(this.onStreamUpdate, 'platypush.message.event.music.snapcast.StreamUpdateEvent');
registerEventHandler(this.onClientVolumeChange, 'platypush.message.event.music.snapcast.ClientVolumeChangeEvent');
registerEventHandler(this.onGroupMuteChange, 'platypush.message.event.music.snapcast.GroupMuteChangeEvent');
this.bus.$on('group-mute-changed', this.groupMute);
this.bus.$on('client-mute-changed', this.clientMute);
this.bus.$on('client-volume-changed', this.clientSetVolume);
this.bus.$on('modal-show', this.modalShow);
},
});

View file

@ -0,0 +1,16 @@
<script type="text/x-template" id="tmpl-music-snapcast-client">
<div class="client" :class="{offline: !connected}">
<div class="col-s-3 col-m-2 col-l-2 name" v-text="host.name"
@click="bus.$emit('modal-show', {type:'client', client:id, group:groupId, host:server.name})">
</div>
<div class="slider-container col-s-7 col-m-8 col-l-9">
<input class="slider" type="range" min="0" max="100" :value="config.volume.percent" @change="volumeChanged">
</div>
<div class="col-s-2 col-m-2 col-l-1 switch pull-right">
<toggle-switch :value="!config.volume.muted" @toggled="muteToggled"></toggle-switch>
</div>
</div>
</script>
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/music.snapcast/client.js') }}"></script>

View file

@ -0,0 +1,33 @@
{% include 'plugins/music.snapcast/client.html' %}
<script type="text/x-template" id="tmpl-music-snapcast-group">
<div class="group">
<div class="head">
<div class="col-10 name"
@click="bus.$emit('modal-show', {type:'group', group:id, host:server.name})">
<i class="icon fa fa-network-wired"></i>
{% raw %}{{ name || stream.id || id }}{% endraw %}
</div>
<div class="col-2 switch pull-right">
<toggle-switch :glow="true" :value="!muted" @toggled="muteToggled"></toggle-switch>
</div>
</div>
<music-snapcast-client
v-for="client in clients"
:key="client.id"
:config="client.config"
:connected="client.connected"
:server="server"
:host="client.host"
:groupId="id"
:id="client.id"
:lastSeen="client.lastSeen"
:bus="bus"
:snapclient="client.snapclient">
</music-snapcast-client>
</div>
</script>
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/music.snapcast/group.js') }}"></script>

View file

@ -0,0 +1,34 @@
{% include 'plugins/music.snapcast/group.html' %}
<script type="text/x-template" id="tmpl-music-snapcast-host">
<div class="host">
<div class="head">
<div class="col-11 name"
@click="bus.$emit('modal-show', {type:'host', host:server.host.name})">
<i class="icon fa fa-server"></i>
{% raw %}{{ server.host.name }}{% endraw %}
</div>
<div class="col-1 buttons pull-right">
<button type="button" @click="collapsed = !collapsed">
<i class="icon fa" :class="{'fa-chevron-up': !collapsed, 'fa-chevron-down': collapsed}"></i>
</button>
</div>
</div>
<music-snapcast-group
v-for="group in groups"
v-if="!collapsed"
:key="group.id"
:id="group.id"
:name="group.name"
:bus="bus"
:server="server.host"
:muted="group.muted"
:clients="group.clients"
:stream="streams[group.stream_id]">
</music-snapcast-group>
</div>
</script>
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/music.snapcast/host.js') }}"></script>

View file

@ -0,0 +1,83 @@
{% include 'plugins/music.snapcast/host.html' %}
<script type="text/x-template" id="tmpl-music-snapcast">
<div class="row music-snapcast-container">
<modal id="music-snapcast-host-info" title="Server info" v-model="modal.host.visible" ref="modalHost">
<music-snapcast-host-info v-if="modal.host.visible" :info="modal.host.info" inline-template>
<div class="info">
<div class="row" v-if="info.server.host.ip && modal.host.info.server.host.ip.length">
<div class="label col-3">IP Address</div>
<div class="value col-9" v-text="modal.host.info.server.host.ip"></div>
</div>
<div class="row" v-if="info.server.host.mac && info.server.host.mac.length">
<div class="label col-3">MAC Address</div>
<div class="value col-9" v-text="info.server.host.mac"></div>
</div>
<div class="row" v-if="info.server.host.name && info.server.host.name.length">
<div class="label col-3">Name</div>
<div class="value col-9" v-text="info.server.host.name"></div>
</div>
<div class="row" v-if="info.server.host.port">
<div class="label col-3">Port</div>
<div class="value col-9" v-text="info.server.host.port"></div>
</div>
<div class="row" v-if="info.server.host.os && info.server.host.os.length">
<div class="label col-3">OS</div>
<div class="value col-9" v-text="info.server.host.os"></div>
</div>
<div class="row" v-if="info.server.host.arch && info.server.host.arch.length">
<div class="label col-3">Architecture</div>
<div class="value col-9" v-text="info.server.host.arch"></div>
</div>
<div class="row">
<div class="label col-3">Server name</div>
<div class="value col-9" v-text="info.server.snapserver.name"></div>
</div>
<div class="row">
<div class="label col-3">Server version</div>
<div class="value col-9" v-text="info.server.snapserver.version"></div>
</div>
<div class="row">
<div class="label col-3">Protocol version</div>
<div class="value col-9" v-text="info.server.snapserver.protocolVersion"></div>
</div>
<div class="row">
<div class="label col-3">Control protocol version</div>
<div class="value col-9" v-text="info.server.snapserver.controlProtocolVersion"></div>
</div>
</div>
</music-snapcast-host-info>
</modal>
<modal id="music-snapcast-group-info" title="Group info" v-if="modal.group.visible" v-model="modal.group.visible" ref="modalGroup">
<music-snapcast-group-info :info="modal.group.info" inline-template>
<p>IT WORKED!</p>
</music-snapcast-group-info>
</modal>
<modal id="music-snapcast-client-info" title="Client info" v-if="modal.client.visible" v-model="modal.client.visible" ref="modalClient">
<music-snapcast-client-info :info="modal.client.info" inline-template>
<p>IT WORKED!</p>
</music-snapcast-client-info>
</modal>
<music-snapcast-host
v-for="host in hosts"
:key="host.server.host.name"
:server="host.server"
:streams="host.streams"
:groups="host.groups"
:bus="bus">
</music-snapcast-host>
</div>
</script>