Added new Snapcast webpanel plugin
This commit is contained in:
parent
33d55dcd93
commit
95a9c22618
11 changed files with 540 additions and 2 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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'],
|
||||
});
|
||||
|
|
@ -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'],
|
||||
});
|
||||
|
|
@ -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'],
|
||||
});
|
||||
|
178
platypush/backend/http/static/js/plugins/music.snapcast/index.js
Normal file
178
platypush/backend/http/static/js/plugins/music.snapcast/index.js
Normal 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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
Loading…
Reference in a new issue