platypush/platypush/backend/http/webapp/src/components/panels/MusicSnapcast/Index.vue

500 lines
13 KiB
Vue

<template>
<div class="music-snapcast-container">
<Loading v-if="loading" />
<div class="info">
<Modal title="Server info" ref="modalHost">
<ModalHost :info="hosts[selectedHost]" v-if="selectedHost" />
</Modal>
</div>
<div class="info">
<Modal title="Group info" ref="modalGroup">
<ModalGroup :group="hosts[selectedHost].groups[selectedGroup]" :streams="hosts[selectedHost].streams"
:clients="clientsByHost[selectedHost]" :loading="loading" @add-client="addClientToGroup"
@remove-client="removeClientFromGroup" @stream-change="streamChange"
@rename-group="renameGroup($event)" v-if="selectedGroup" />
</Modal>
</div>
<div class="info">
<Modal title="Client info" ref="modalClient">
<ModalClient :client="hosts[selectedHost].groups[selectedGroup].clients[selectedClient]" :loading="loading"
@remove-client="removeClient" @rename-client="renameClient($event)" v-if="selectedClient" />
</Modal>
</div>
<Host v-for="(host, id) in hosts" :key="id"
:server="host.server"
:streams="host.streams"
:groups="host.groups"
@group-mute-toggle="groupMute($event)"
@client-mute-toggle="clientMute($event)"
@client-volume-change="clientSetVolume($event)"
@modal-show="onModalShow($event)" />
</div>
</template>
<script>
import Modal from "@/components/Modal";
import Utils from "@/Utils";
import Host from "@/components/panels/MusicSnapcast/Host";
import ModalHost from "@/components/panels/MusicSnapcast/modals/Host";
import ModalGroup from "@/components/panels/MusicSnapcast/modals/Group";
import ModalClient from "@/components/panels/MusicSnapcast/modals/Client";
import Loading from "@/components/Loading";
export default {
name: "MusicSnapcast",
mixins: [Utils],
components: {Loading, Modal, Host, ModalHost, ModalGroup, ModalClient},
data: function() {
return {
loading: false,
hosts: {},
ports: {},
selectedHost: null,
selectedGroup: null,
selectedClient: null,
}
},
computed: {
clientsByHost() {
return Object.entries(this.hosts).reduce((hosts, [name, info]) => {
hosts[name] = {}
Object.values(info.groups).forEach((group) => {
Object.entries(group.clients).forEach(([clientId, client]) => {
hosts[name][clientId] = client
})
})
return hosts
}, {})
},
},
methods: {
parseServerStatus(status) {
status.server.host.port = this.ports[status.server.host.name]
this.hosts[status.server.host.name] = {
...status,
groups: status.groups.map((group) => {
return {
...group,
clients: group.clients.reduce((clients, client) => {
clients[client.id] = client
return clients
}, {}),
}
}).reduce((groups, group) => {
groups[group.id] = group
return groups
}, {}),
streams: status.streams.reduce((streams, stream) => {
streams[stream.id] = stream
return streams
}, {}),
}
},
async refresh() {
this.loading = true
try {
const hosts = await this.request('music.snapcast.get_backend_hosts')
const statuses = await Promise.all(Object.keys(hosts).map(
async (host) => this.request('music.snapcast.status', {host: host, port: hosts[host]})
))
this.hosts = {}
statuses.forEach((status) => {
this.ports[status.server.host.name] = hosts[status.server.host.name]
this.parseServerStatus(status)
})
} finally {
this.loading = false
}
},
async refreshHost(host) {
if (!(host in this.hosts))
return
this.parseServerStatus(await this.request('music.snapcast.status', {
host: host,
port: this.ports[host]
}))
},
async addClientToGroup(clientId) {
this.loading = true
try {
if (!this.selectedHost || !this.selectedGroup || !(clientId in this.clientsByHost[this.selectedHost]))
return
const clients = [...new Set([clientId,
...Object.keys(this.hosts[this.selectedHost].groups[this.selectedGroup].clients)])]
await this.request('music.snapcast.group_set_clients', {
host: this.selectedHost,
port: this.ports[this.selectedHost],
group: this.selectedGroup,
clients: clients,
})
await this.refreshHost(this.selectedHost)
} finally {
this.loading = false
}
},
async removeClientFromGroup(clientId) {
this.loading = true
try {
if (!this.selectedHost || !this.selectedGroup || !(clientId in this.clientsByHost[this.selectedHost]))
return
const clients = new Set([...Object.keys(this.hosts[this.selectedHost].groups[this.selectedGroup].clients)])
if (!clients.has(clientId))
return
clients.delete(clientId)
await this.request('music.snapcast.group_set_clients', {
host: this.selectedHost,
port: this.ports[this.selectedHost],
group: this.selectedGroup,
clients: [...clients],
})
await this.refreshHost(this.selectedHost)
} finally {
this.loading = false
}
},
async renameGroup(name) {
this.loading = true
try {
if (!this.selectedHost || !this.selectedGroup)
return
await this.request('music.snapcast.set_group_name', {
host: this.selectedHost,
port: this.ports[this.selectedHost],
group: this.selectedGroup,
name: name,
})
await this.refreshHost(this.selectedHost)
} finally {
this.loading = false
}
},
async renameClient(name) {
this.loading = true
try {
if (!this.selectedHost || !this.selectedClient)
return
await this.request('music.snapcast.set_client_name', {
host: this.selectedHost,
port: this.ports[this.selectedHost],
client: this.selectedClient,
name: name,
})
await this.refreshHost(this.selectedHost)
} finally {
this.loading = false
}
},
async removeClient() {
this.loading = true
try {
if (!(this.selectedHost && this.selectedClient))
return
await this.request('music.snapcast.delete_client', {
host: this.selectedHost,
port: this.ports[this.selectedHost],
client: this.selectedClient,
})
this.$refs.modalClient.close()
await this.refreshHost(this.selectedHost)
} finally {
this.loading = false
}
},
async streamChange(streamId) {
this.loading = true
try {
await this.request('music.snapcast.group_set_stream', {
host: this.selectedHost,
port: this.ports[this.selectedHost],
group: this.selectedGroup,
stream_id: streamId,
})
await this.refreshHost(this.selectedHost)
} finally {
this.loading = false
}
},
onClientUpdate(event) {
Object.keys(this.hosts[event.host].groups).forEach((groupId) => {
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(event) {
this.hosts[event.host].groups[event.group].stream_id = event.stream
},
onServerUpdate(event) {
this.parseServerStatus(event.server)
},
onStreamUpdate(event) {
this.hosts[event.host].streams[event.stream.id] = event.stream
},
onClientVolumeChange(event) {
Object.keys(this.hosts[event.host].groups).forEach((groupId) => {
if (!(event.client in this.hosts[event.host].groups[groupId].clients))
return
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(event) {
this.hosts[event.host].groups[event.group].muted = event.muted
},
modalShow(event) {
switch(event.type) {
case 'host':
this.modal[event.type].info = this.hosts[event.host]
break
case 'group':
this.modal[event.type].info.server = this.hosts[event.host].server
this.modal[event.type].info.group = this.hosts[event.host].groups[event.group]
this.modal[event.type].info.streams = this.hosts[event.host].streams
this.modal[event.type].info.clients = {}
for (const group of Object.values(this.hosts[event.host].groups)) {
for (const client of Object.values(group.clients)) {
this.modal[event.type].info.clients[client.id] = client
}
}
break
case 'client':
this.modal[event.type].info = this.hosts[event.host].groups[event.group].clients[event.client]
this.modal[event.type].info.server = this.hosts[event.host].server
break
}
this.modal[event.type].visible = true
},
async groupMute(event) {
await this.request('music.snapcast.mute', {
group: event.group,
host: event.host,
port: this.ports[event.host],
mute: event.muted,
})
await this.refreshHost(event.host)
},
async clientMute(event) {
await this.request('music.snapcast.mute', {
client: event.client,
host: event.host,
port: this.ports[event.host],
mute: event.muted,
})
await this.refreshHost(event.host)
},
async clientSetVolume(event) {
await this.request('music.snapcast.volume', {
client: event.client,
host: event.host,
port: this.ports[event.host],
volume: event.volume,
})
await this.refreshHost(event.host)
},
onModalShow(event) {
switch (event.type) {
case 'host':
this.selectedHost = event.host
this.$refs.modalHost.show()
break
case 'group':
this.selectedHost = event.host
this.selectedGroup = event.group
this.$refs.modalGroup.show()
break
case 'client':
this.selectedHost = event.host
this.selectedGroup = event.group
this.selectedClient = event.client
this.$refs.modalClient.show()
break
}
}
},
mounted() {
this.refresh()
this.subscribe(this.onClientUpdate, null,
'platypush.message.event.music.snapcast.ClientConnectedEvent',
'platypush.message.event.music.snapcast.ClientDisconnectedEvent',
'platypush.message.event.music.snapcast.ClientNameChangeEvent')
this.subscribe(this.onGroupStreamChange, null, 'platypush.message.event.music.snapcast.GroupStreamChangeEvent')
this.subscribe(this.onServerUpdate, null, 'platypush.message.event.music.snapcast.ServerUpdateEvent')
this.subscribe(this.onStreamUpdate, null, 'platypush.message.event.music.snapcast.StreamUpdateEvent')
this.subscribe(this.onClientVolumeChange, null, 'platypush.message.event.music.snapcast.ClientVolumeChangeEvent')
this.subscribe(this.onGroupMuteChange, null, 'platypush.message.event.music.snapcast.GroupMuteChangeEvent')
},
}
</script>
<style lang="scss" scoped>
.music-snapcast-container {
width: 100%;
overflow: auto;
background: $background-color;
}
::v-deep(.info) {
.modal {
.content {
width: 90%;
max-width: 800px;
}
.body {
padding: 0;
}
}
.row {
display: flex;
align-items: center;
border-radius: .75em;
padding: 1em;
@include until($tablet) {
flex-direction: column;
border-bottom: $default-border;
}
@include from($desktop) {
padding: 1em 2em;
}
.label {
margin-bottom: 0;
}
.value {
display: flex;
@include from($tablet) {
justify-content: right;
}
@include until($tablet) {
width: 100%;
margin-left: 0;
}
}
@include until($tablet) {
.label {
width: 100%;
display: flex;
}
}
&:nth-child(odd) {
background: $background-color;
}
&:nth-child(even) {
background: $default-bg-3;
}
&:hover {
background: $hover-bg;
}
}
.buttons {
background: initial;
margin-top: 1.5em;
padding-top: 1.5em;
border-top: $default-border-2;
display: flex;
justify-content: center;
}
}
@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: 45vw;
}
}
}
</style>