Migrated music.snapcast UI

This commit is contained in:
Fabio Manganiello 2021-01-22 01:00:49 +01:00
parent 370a7d4c15
commit 7a7e00bea2
64 changed files with 1430 additions and 148 deletions

View file

@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>platypush</title><link href="/static/css/chunk-076c5199.fc1132f4.css" rel="prefetch"><link href="/static/css/chunk-1293e286.fc1132f4.css" rel="prefetch"><link href="/static/css/chunk-14f3b6ed.fc1132f4.css" rel="prefetch"><link href="/static/css/chunk-24ff873d.25b446f2.css" rel="prefetch"><link href="/static/css/chunk-35b45d59.3285a5c5.css" rel="prefetch"><link href="/static/css/chunk-3d60f62e.4fa298b8.css" rel="prefetch"><link href="/static/css/chunk-4201fea8.ab45ee69.css" rel="prefetch"><link href="/static/css/chunk-45939517.ce9c914b.css" rel="prefetch"><link href="/static/css/chunk-4bbbb9a3.8b96bf08.css" rel="prefetch"><link href="/static/css/chunk-4bc2706b.1e09b17a.css" rel="prefetch"><link href="/static/css/chunk-545459d0.4806f03d.css" rel="prefetch"><link href="/static/css/chunk-59396623.5084b7e8.css" rel="prefetch"><link href="/static/css/chunk-62a3d08e.d329d923.css" rel="prefetch"><link href="/static/css/chunk-9684cd10.6046f7ac.css" rel="prefetch"><link href="/static/css/chunk-abbc1cdc.fc1132f4.css" rel="prefetch"><link href="/static/css/chunk-cb418146.1cc7af9d.css" rel="prefetch"><link href="/static/css/chunk-cd9a889e.b5ec8fac.css" rel="prefetch"><link href="/static/css/chunk-d8561e02.0d73779a.css" rel="prefetch"><link href="/static/css/chunk-e8078048.04620e86.css" rel="prefetch"><link href="/static/css/chunk-fa20b8a0.d7316f99.css" rel="prefetch"><link href="/static/js/chunk-076c5199.377e9834.js" rel="prefetch"><link href="/static/js/chunk-1293e286.eb2fa695.js" rel="prefetch"><link href="/static/js/chunk-14f3b6ed.d6fcafdc.js" rel="prefetch"><link href="/static/js/chunk-24ff873d.691c883d.js" rel="prefetch"><link href="/static/js/chunk-2d0cc2be.b3a583d9.js" rel="prefetch"><link href="/static/js/chunk-2d2091df.1e51ae4c.js" rel="prefetch"><link href="/static/js/chunk-2d21da1a.adf909a2.js" rel="prefetch"><link href="/static/js/chunk-2d237d41.3427f74b.js" rel="prefetch"><link href="/static/js/chunk-35b45d59.9a57c504.js" rel="prefetch"><link href="/static/js/chunk-3d60f62e.907e4050.js" rel="prefetch"><link href="/static/js/chunk-4201fea8.29361f0f.js" rel="prefetch"><link href="/static/js/chunk-45939517.c0034c6b.js" rel="prefetch"><link href="/static/js/chunk-4bbbb9a3.251fff37.js" rel="prefetch"><link href="/static/js/chunk-4bc2706b.38882fe9.js" rel="prefetch"><link href="/static/js/chunk-545459d0.da1ea7e5.js" rel="prefetch"><link href="/static/js/chunk-59396623.19b5fca7.js" rel="prefetch"><link href="/static/js/chunk-62a3d08e.17d3c86d.js" rel="prefetch"><link href="/static/js/chunk-9684cd10.7051bb65.js" rel="prefetch"><link href="/static/js/chunk-abbc1cdc.47491a05.js" rel="prefetch"><link href="/static/js/chunk-cb418146.7a824439.js" rel="prefetch"><link href="/static/js/chunk-cd9a889e.ec43fdb3.js" rel="prefetch"><link href="/static/js/chunk-d8561e02.1e366cb3.js" rel="prefetch"><link href="/static/js/chunk-e8078048.ce29b8d4.js" rel="prefetch"><link href="/static/js/chunk-fa20b8a0.78555b70.js" rel="prefetch"><link href="/static/css/app.9ee642c5.css" rel="preload" as="style"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="preload" as="style"><link href="/static/js/app.1abebcd8.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.30e3a6cb.js" rel="preload" as="script"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="stylesheet"><link href="/static/css/app.9ee642c5.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.30e3a6cb.js"></script><script src="/static/js/app.1abebcd8.js"></script></body></html>
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>platypush</title><link href="/static/css/chunk-076c5199.f186cc51.css" rel="prefetch"><link href="/static/css/chunk-0918db6a.44bbe779.css" rel="prefetch"><link href="/static/css/chunk-1293e286.f186cc51.css" rel="prefetch"><link href="/static/css/chunk-14f3b6ed.f186cc51.css" rel="prefetch"><link href="/static/css/chunk-21660230.75b51be7.css" rel="prefetch"><link href="/static/css/chunk-24ff873d.197de139.css" rel="prefetch"><link href="/static/css/chunk-35b45d59.b7730bd4.css" rel="prefetch"><link href="/static/css/chunk-4201fea8.42d666a4.css" rel="prefetch"><link href="/static/css/chunk-45557166.ab71816e.css" rel="prefetch"><link href="/static/css/chunk-45939517.4d7c2357.css" rel="prefetch"><link href="/static/css/chunk-4bbbb9a3.7294303f.css" rel="prefetch"><link href="/static/css/chunk-5841cc7d.d0bad316.css" rel="prefetch"><link href="/static/css/chunk-62a3d08e.c4e19f9e.css" rel="prefetch"><link href="/static/css/chunk-6c27e200.680ba1de.css" rel="prefetch"><link href="/static/css/chunk-9684cd10.43a25f0f.css" rel="prefetch"><link href="/static/css/chunk-9f884670.a8a2d99a.css" rel="prefetch"><link href="/static/css/chunk-abbc1cdc.f186cc51.css" rel="prefetch"><link href="/static/css/chunk-cb418146.678c9c97.css" rel="prefetch"><link href="/static/css/chunk-d0b841c2.5506a233.css" rel="prefetch"><link href="/static/css/chunk-d8561e02.7c71cffb.css" rel="prefetch"><link href="/static/css/chunk-e8078048.eda53677.css" rel="prefetch"><link href="/static/css/chunk-fa20b8a0.c233115f.css" rel="prefetch"><link href="/static/js/chunk-076c5199.377e9834.js" rel="prefetch"><link href="/static/js/chunk-0918db6a.cd3d5e70.js" rel="prefetch"><link href="/static/js/chunk-1293e286.eb2fa695.js" rel="prefetch"><link href="/static/js/chunk-14f3b6ed.d6fcafdc.js" rel="prefetch"><link href="/static/js/chunk-21660230.f7af277b.js" rel="prefetch"><link href="/static/js/chunk-24ff873d.691c883d.js" rel="prefetch"><link href="/static/js/chunk-2d0cc2be.b3a583d9.js" rel="prefetch"><link href="/static/js/chunk-2d2091df.1e51ae4c.js" rel="prefetch"><link href="/static/js/chunk-2d21da1a.adf909a2.js" rel="prefetch"><link href="/static/js/chunk-2d237d41.3427f74b.js" rel="prefetch"><link href="/static/js/chunk-35b45d59.9a57c504.js" rel="prefetch"><link href="/static/js/chunk-4201fea8.29361f0f.js" rel="prefetch"><link href="/static/js/chunk-45557166.4035bc76.js" rel="prefetch"><link href="/static/js/chunk-45939517.c0034c6b.js" rel="prefetch"><link href="/static/js/chunk-4bbbb9a3.251fff37.js" rel="prefetch"><link href="/static/js/chunk-5841cc7d.2979e631.js" rel="prefetch"><link href="/static/js/chunk-62a3d08e.17d3c86d.js" rel="prefetch"><link href="/static/js/chunk-6c27e200.bc387aa4.js" rel="prefetch"><link href="/static/js/chunk-9684cd10.7051bb65.js" rel="prefetch"><link href="/static/js/chunk-9f884670.aa7caaf0.js" rel="prefetch"><link href="/static/js/chunk-abbc1cdc.47491a05.js" rel="prefetch"><link href="/static/js/chunk-cb418146.7a824439.js" rel="prefetch"><link href="/static/js/chunk-d0b841c2.c0e1a82a.js" rel="prefetch"><link href="/static/js/chunk-d8561e02.1e366cb3.js" rel="prefetch"><link href="/static/js/chunk-e8078048.ce29b8d4.js" rel="prefetch"><link href="/static/js/chunk-fa20b8a0.78555b70.js" rel="prefetch"><link href="/static/css/app.e06a419a.css" rel="preload" as="style"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="preload" as="style"><link href="/static/js/app.dfa8f1e3.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.30e3a6cb.js" rel="preload" as="script"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="stylesheet"><link href="/static/css/app.e06a419a.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.30e3a6cb.js"></script><script src="/static/js/app.dfa8f1e3.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-0918db6a"],{"3cbf":function(e,t,r){},"4de4":function(e,t,r){"use strict";var n=r("23e7"),c=r("b727").filter,a=r("1dde"),o=r("ae40"),i=a("filter"),u=o("filter");n({target:"Array",proto:!0,forced:!i||!u},{filter:function(e){return c(this,e,arguments.length>1?arguments[1]:void 0)}})},5530:function(e,t,r){"use strict";r.d(t,"a",(function(){return a}));r("a4d3"),r("4de4"),r("4160"),r("e439"),r("dbb4"),r("b64b"),r("159b");function n(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function c(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function a(e){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{};t%2?c(Object(r),!0).forEach((function(t){n(e,t,r[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):c(Object(r)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(r,t))}))}return e}},a79d9:function(e,t,r){"use strict";var n=r("7a23"),c=Object(n["K"])("data-v-1502d8a8");Object(n["u"])("data-v-1502d8a8");var a={class:"torrent-container"},o={class:"header-container"},i={class:"view-container"};Object(n["s"])();var u=c((function(e,t,r,c,u,d){var b=Object(n["z"])("Header"),s=Object(n["z"])("TorrentView");return Object(n["r"])(),Object(n["e"])("div",a,[Object(n["h"])("div",o,[Object(n["h"])(b,{onTorrentAdd:t[1]||(t[1]=function(e){return d.download(e)})})]),Object(n["h"])("div",i,[Object(n["h"])(s,{"plugin-name":r.pluginName},null,8,["plugin-name"])])])})),d=(r("96cf"),r("1da1")),b=Object(n["K"])("data-v-6133f14d");Object(n["u"])("data-v-6133f14d");var s={class:"row"},f={class:"col-s-12 col-m-9 col-l-7 left side"},l={class:"search-box"};Object(n["s"])();var O=b((function(e,t,r,c,a,o){return Object(n["r"])(),Object(n["e"])("div",{class:["header",{"with-filter":e.filterVisible}]},[Object(n["h"])("div",s,[Object(n["h"])("div",f,[Object(n["h"])("form",{onSubmit:t[2]||(t[2]=Object(n["J"])((function(t){return e.$emit("torrent-add",a.torrentURL)}),["prevent"]))},[Object(n["h"])("label",l,[Object(n["I"])(Object(n["h"])("input",{type:"search",placeholder:"Add torrent URL","onUpdate:modelValue":t[1]||(t[1]=function(e){return a.torrentURL=e})},null,512),[[n["F"],a.torrentURL]])])],32)])])],2)})),p={name:"Header",emits:["torrent-add"],data:function(){return{torrentURL:""}}};r("f774");p.render=O,p.__scopeId="data-v-6133f14d";var j=p,v=r("0cc1"),h=r("3e54"),w={name:"Panel",components:{TorrentView:v["a"],Header:j},mixins:[h["a"]],props:{pluginName:{type:String,required:!0}},methods:{download:function(e){var t=this;return Object(d["a"])(regeneratorRuntime.mark((function r(){return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:return r.next=2,t.request("".concat(t.pluginName,".download"),{torrent:e});case 2:case"end":return r.stop()}}),r)})))()}}};r("b170");w.render=u,w.__scopeId="data-v-1502d8a8";t["a"]=w},b170:function(e,t,r){"use strict";r("3cbf")},ba28:function(e,t,r){},dbb4:function(e,t,r){var n=r("23e7"),c=r("83ab"),a=r("56ef"),o=r("fc6a"),i=r("06cf"),u=r("8418");n({target:"Object",stat:!0,sham:!c},{getOwnPropertyDescriptors:function(e){var t,r,n=o(e),c=i.f,d=a(n),b={},s=0;while(d.length>s)r=c(n,t=d[s++]),void 0!==r&&u(b,t,r);return b}})},e439:function(e,t,r){var n=r("23e7"),c=r("d039"),a=r("fc6a"),o=r("06cf").f,i=r("83ab"),u=c((function(){o(1)})),d=!i||u;n({target:"Object",stat:!0,forced:d,sham:!i},{getOwnPropertyDescriptor:function(e,t){return o(a(e),t)}})},f774:function(e,t,r){"use strict";r("ba28")}}]);
//# sourceMappingURL=chunk-0918db6a.cd3d5e70.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-4bc2706b"],{"3cbf":function(e,t,n){},a79d9:function(e,t,n){"use strict";var r=n("7a23"),a=Object(r["K"])("data-v-1502d8a8");Object(r["u"])("data-v-1502d8a8");var c={class:"torrent-container"},o={class:"header-container"},i={class:"view-container"};Object(r["s"])();var d=a((function(e,t,n,a,d,u){var s=Object(r["z"])("Header"),b=Object(r["z"])("TorrentView");return Object(r["r"])(),Object(r["e"])("div",c,[Object(r["h"])("div",o,[Object(r["h"])(s,{onTorrentAdd:t[1]||(t[1]=function(e){return u.download(e)})})]),Object(r["h"])("div",i,[Object(r["h"])(b,{"plugin-name":n.pluginName},null,8,["plugin-name"])])])})),u=(n("96cf"),n("1da1")),s=Object(r["K"])("data-v-6133f14d");Object(r["u"])("data-v-6133f14d");var b={class:"row"},l={class:"col-s-12 col-m-9 col-l-7 left side"},f={class:"search-box"};Object(r["s"])();var j=s((function(e,t,n,a,c,o){return Object(r["r"])(),Object(r["e"])("div",{class:["header",{"with-filter":e.filterVisible}]},[Object(r["h"])("div",b,[Object(r["h"])("div",l,[Object(r["h"])("form",{onSubmit:t[2]||(t[2]=Object(r["J"])((function(t){return e.$emit("torrent-add",c.torrentURL)}),["prevent"]))},[Object(r["h"])("label",f,[Object(r["I"])(Object(r["h"])("input",{type:"search",placeholder:"Add torrent URL","onUpdate:modelValue":t[1]||(t[1]=function(e){return c.torrentURL=e})},null,512),[[r["F"],c.torrentURL]])])],32)])])],2)})),p={name:"Header",emits:["torrent-add"],data:function(){return{torrentURL:""}}};n("f774");p.render=j,p.__scopeId="data-v-6133f14d";var O=p,v=n("0cc1"),h=n("3e54"),m={name:"Panel",components:{TorrentView:v["a"],Header:O},mixins:[h["a"]],props:{pluginName:{type:String,required:!0}},methods:{download:function(e){var t=this;return Object(u["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.next=2,t.request("".concat(t.pluginName,".download"),{torrent:e});case 2:case"end":return n.stop()}}),n)})))()}}};n("b170");m.render=d,m.__scopeId="data-v-1502d8a8";t["a"]=m},b170:function(e,t,n){"use strict";n("3cbf")},ba28:function(e,t,n){},f774:function(e,t,n){"use strict";n("ba28")}}]);
//# sourceMappingURL=chunk-4bc2706b.38882fe9.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-545459d0"],{8285:function(e,n,u){"use strict";var t=u("7a23"),o=Object(t["K"])("data-v-12a0983b"),i=o((function(e,n,u,o,i,a){return Object(t["r"])(),Object(t["e"])("label",null,[Object(t["h"])("input",{class:"slider",type:"range",min:u.range[0],max:u.range[1],value:u.value,disabled:u.disabled,onChange:n[1]||(n[1]=function(n){return e.$emit("input",n)}),onMouseup:n[2]||(n[2]=function(n){return e.$emit("mouseup",n)}),onInput:n[3]||(n[3]=function(n){return e.$emit("input",n)}),onMousedown:n[4]||(n[4]=function(n){return e.$emit("mousedown",n)}),onTouch:n[5]||(n[5]=function(n){return e.$emit("input",n)}),onTouchstart:n[6]||(n[6]=function(n){return e.$emit("mousedown",n)}),onTouchend:n[7]||(n[7]=function(n){return e.$emit("mouseup",n)})},null,40,["min","max","value","disabled"])])})),a=(u("a9e3"),{name:"Slider",emits:["input","mouseup","mousedown"],props:{value:{type:Number},disabled:{type:Boolean,default:!1},range:{type:Array,default:function(){return[0,100]}}}});u("ee52");a.render=i,a.__scopeId="data-v-12a0983b";n["a"]=a},e1773:function(e,n,u){},ee52:function(e,n,u){"use strict";u("e1773")}}]);
//# sourceMappingURL=chunk-545459d0.da1ea7e5.js.map

View file

@ -1 +0,0 @@
{"version":3,"sources":["webpack:///./src/components/elements/Slider.vue","webpack:///./src/components/elements/Slider.vue?7dba","webpack:///./src/components/elements/Slider.vue?5806"],"names":["class","type","min","range","max","value","disabled","$emit","$event","name","emits","props","Number","Boolean","default","Array","render","__scopeId"],"mappings":"uNACE,eAKQ,cAJN,eAGqF,SAH9EA,MAAM,SAASC,KAAK,QAASC,IAAK,EAAAC,MAAK,GAAMC,IAAK,EAAAD,MAAK,GAAME,MAAO,EAAAA,MAAQC,SAAU,EAAAA,SACrF,SAAM,+BAAE,EAAAC,MAAK,QAAUC,KAAU,UAAO,+BAAE,EAAAD,MAAK,UAAYC,KAAU,QAAK,+BAAE,EAAAD,MAAK,QAAUC,KAC3F,YAAS,+BAAE,EAAAD,MAAK,YAAcC,KAAU,QAAK,+BAAE,EAAAD,MAAK,QAAUC,KAC9D,aAAU,+BAAE,EAAAD,MAAK,YAAcC,KAAU,WAAQ,+BAAE,EAAAD,MAAK,UAAYC,M,+CAKjE,G,UAAA,CACbC,KAAM,SACNC,MAAO,CAAC,QAAS,UAAW,aAC5BC,MAAO,CACLN,MAAO,CACLJ,KAAMW,QAGRN,SAAU,CACRL,KAAMY,QACNC,SAAS,GAGXX,MAAO,CACLF,KAAMc,MACND,QAAS,iBAAM,CAAC,EAAG,U,UCpBzB,EAAOE,OAAS,EAChB,EAAOC,UAAY,kBAEJ,U,0DCRf","file":"static/js/chunk-545459d0.da1ea7e5.js","sourcesContent":["<template>\n <label>\n <input class=\"slider\" type=\"range\" :min=\"range[0]\" :max=\"range[1]\" :value=\"value\" :disabled=\"disabled\"\n @change=\"$emit('input', $event)\" @mouseup=\"$emit('mouseup', $event)\" @input=\"$emit('input', $event)\"\n @mousedown=\"$emit('mousedown', $event)\" @touch=\"$emit('input', $event)\"\n @touchstart=\"$emit('mousedown', $event)\" @touchend=\"$emit('mouseup', $event)\">\n </label>\n</template>\n\n<script>\nexport default {\n name: \"Slider\",\n emits: ['input', 'mouseup', 'mousedown'],\n props: {\n value: {\n type: Number,\n },\n\n disabled: {\n type: Boolean,\n default: false,\n },\n\n range: {\n type: Array,\n default: () => [0, 100],\n },\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.slider {\n @include appearance(none);\n @include transition(opacity .2s);\n width: 100%;\n height: 1em;\n border-radius: 0.33em;\n background: $slider-bg;\n outline: none;\n\n @mixin slider-thumb {\n @include appearance(none);\n width: 1.5em;\n height: 1.5em;\n border-radius: 50%;\n border: 0;\n background: $slider-thumb-bg;\n cursor: pointer;\n }\n\n &::-webkit-slider-thumb { @include slider-thumb; }\n &::-moz-range-thumb { @include slider-thumb; }\n &::-moz-range-track { @include appearance(none); }\n\n &::-webkit-progress-value,\n &::-moz-range-progress {\n background: $slider-progress-bg;\n height: 1em;\n }\n\n &[disabled] {\n &::-webkit-progress-value,\n &::-moz-range-progress {\n background: none;\n }\n\n &::-webkit-slider-thumb,\n &::-moz-range-thumb {\n display: none;\n width: 0;\n }\n }\n}\n</style>","import { render } from \"./Slider.vue?vue&type=template&id=12a0983b&scoped=true&bindings={\\\"value\\\":\\\"props\\\",\\\"disabled\\\":\\\"props\\\",\\\"range\\\":\\\"props\\\"}\"\nimport script from \"./Slider.vue?vue&type=script&lang=js\"\nexport * from \"./Slider.vue?vue&type=script&lang=js\"\n\nimport \"./Slider.vue?vue&type=style&index=0&id=12a0983b&lang=scss&scoped=true\"\nscript.render = render\nscript.__scopeId = \"data-v-12a0983b\"\n\nexport default script","export * from \"-!../../../node_modules/mini-css-extract-plugin/dist/loader.js??ref--8-oneOf-1-0!../../../node_modules/css-loader/dist/cjs.js??ref--8-oneOf-1-1!../../../node_modules/vue-loader-v16/dist/stylePostLoader.js!../../../node_modules/postcss-loader/src/index.js??ref--8-oneOf-1-2!../../../node_modules/sass-loader/dist/cjs.js??ref--8-oneOf-1-3!../../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../../node_modules/vue-loader-v16/dist/index.js??ref--0-1!./Slider.vue?vue&type=style&index=0&id=12a0983b&lang=scss&scoped=true\""],"sourceRoot":""}

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-5841cc7d"],{"4de4":function(e,t,n){"use strict";var r=n("23e7"),o=n("b727").filter,u=n("1dde"),i=n("ae40"),c=u("filter"),a=i("filter");r({target:"Array",proto:!0,forced:!c||!a},{filter:function(e){return o(this,e,arguments.length>1?arguments[1]:void 0)}})},5530:function(e,t,n){"use strict";n.d(t,"a",(function(){return u}));n("a4d3"),n("4de4"),n("4160"),n("e439"),n("dbb4"),n("b64b"),n("159b");function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function u(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?o(Object(n),!0).forEach((function(t){r(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):o(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}},8285:function(e,t,n){"use strict";var r=n("7a23"),o=Object(r["K"])("data-v-12a0983b"),u=o((function(e,t,n,o,u,i){return Object(r["r"])(),Object(r["e"])("label",null,[Object(r["h"])("input",{class:"slider",type:"range",min:n.range[0],max:n.range[1],value:n.value,disabled:n.disabled,onChange:t[1]||(t[1]=function(t){return e.$emit("input",t)}),onMouseup:t[2]||(t[2]=function(t){return e.$emit("mouseup",t)}),onInput:t[3]||(t[3]=function(t){return e.$emit("input",t)}),onMousedown:t[4]||(t[4]=function(t){return e.$emit("mousedown",t)}),onTouch:t[5]||(t[5]=function(t){return e.$emit("input",t)}),onTouchstart:t[6]||(t[6]=function(t){return e.$emit("mousedown",t)}),onTouchend:t[7]||(t[7]=function(t){return e.$emit("mouseup",t)})},null,40,["min","max","value","disabled"])])})),i=(n("a9e3"),{name:"Slider",emits:["input","mouseup","mousedown"],props:{value:{type:Number},disabled:{type:Boolean,default:!1},range:{type:Array,default:function(){return[0,100]}}}});n("ee52");i.render=u,i.__scopeId="data-v-12a0983b";t["a"]=i},dbb4:function(e,t,n){var r=n("23e7"),o=n("83ab"),u=n("56ef"),i=n("fc6a"),c=n("06cf"),a=n("8418");r({target:"Object",stat:!0,sham:!o},{getOwnPropertyDescriptors:function(e){var t,n,r=i(e),o=c.f,f=u(r),s={},b=0;while(f.length>b)n=o(r,t=f[b++]),void 0!==n&&a(s,t,n);return s}})},e1773:function(e,t,n){},e439:function(e,t,n){var r=n("23e7"),o=n("d039"),u=n("fc6a"),i=n("06cf").f,c=n("83ab"),a=o((function(){i(1)})),f=!c||a;r({target:"Object",stat:!0,forced:f,sham:!c},{getOwnPropertyDescriptor:function(e,t){return i(u(e),t)}})},ee52:function(e,t,n){"use strict";n("e1773")}}]);
//# sourceMappingURL=chunk-5841cc7d.2979e631.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-9f884670"],{"0279":function(e,t,a){"use strict";var c=a("7a23"),n=Object(c["K"])("data-v-8fae7678");Object(c["u"])("data-v-8fae7678");var s=Object(c["h"])("div",{class:"switch"},[Object(c["h"])("div",{class:"dot"})],-1),i={class:"label"};Object(c["s"])();var o=n((function(e,t,a,n,o,l){return Object(c["r"])(),Object(c["e"])("div",{class:["power-switch",{disabled:a.disabled}],onClick:t[1]||(t[1]=function(){return l.onInput.apply(l,arguments)})},[Object(c["h"])("input",{type:"checkbox",checked:a.value},null,8,["checked"]),Object(c["h"])("label",null,[s,Object(c["h"])("span",i,[Object(c["y"])(e.$slots,"default")])])],2)})),l={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput:function(e){if(e.stopPropagation(),this.disabled)return!1;this.$emit("input",e)}}};a("5b0a");l.render=o,l.__scopeId="data-v-8fae7678";t["a"]=l},"5b0a":function(e,t,a){"use strict";a("7ef9")},"7ef9":function(e,t,a){}}]);
//# sourceMappingURL=chunk-9f884670.aa7caaf0.js.map

File diff suppressed because one or more lines are too long

View file

@ -18,6 +18,9 @@
"music.mpd": {
"class": "fas fa-music"
},
"music.snapcast": {
"class": "fa fa-volume-up"
},
"torrent": {
"class": "fa fa-magnet"
},

View file

@ -0,0 +1,107 @@
<template>
<div class="row client" :class="{offline: !connected}">
<div class="col-s-12 col-m-3 name" v-text="config.name?.length ? config.name : host.name"
@click="$emit('modal-show', {type: 'client', client: id, group: groupId, host: server.name})">
</div>
<div class="col-s-12 col-m-9 controls">
<div class="col-10 slider-container">
<Slider :range="[0, 100]" :value="config.volume.percent"
@mouseup="$emit('volume-change', {host: server.name, client: id, volume: $event.target.value})" />
</div>
<div class="col-2 switch pull-right">
<ToggleSwitch :value="!config.volume.muted"
@input="$emit('mute-toggle', {host: server.name, client: id, muted: !config.volume.muted})" />
</div>
</div>
</div>
</template>
<script>
import ToggleSwitch from "@/components/elements/ToggleSwitch";
import Slider from "@/components/elements/Slider";
export default {
name: "Client",
components: {Slider, ToggleSwitch},
emits: ['volume-change', 'mute-toggle', 'modal-show'],
props: {
config: {
type: Object,
required: true,
},
connected: {
type: Boolean,
default: false,
},
host: {
type: Object,
required: true,
},
id: {
type: String,
required: true,
},
groupId: {
type: String,
required: true,
},
lastSeen: {
type: Object,
default: () => {},
},
snapclient: {
type: Object,
required: true,
},
server: {
type: Object,
required: true,
},
},
}
</script>
<style lang="scss" scoped>
.client {
@include until($tablet) {
flex-direction: column;
border-bottom: $default-border;
}
.name, .controls {
@include until($tablet) {
width: 100%;
display: flex;
}
}
&.offline {
color: $disabled-fg;
}
&:hover {
background: $hover-bg;
}
.name {
@include until($tablet) {
padding-bottom: .5em;
}
&:hover {
color: $default-hover-fg;
cursor: pointer;
}
}
}
</style>

View file

@ -0,0 +1,91 @@
<template>
<div class="group">
<div class="head">
<div class="col-10 name" @click="$emit('modal-show', {type: 'group', group: id, host: server.name})">
<i class="icon fa" :class="{'fa-play': stream.status === 'playing', 'fa-stop': stream.status !== 'playing'}"></i>
{{ name || stream.id || id }}
</div>
<div class="col-2 switch pull-right">
<ToggleSwitch :value="!muted"
@input="$emit('group-mute-toggle', {host: server.name, group: id, muted: !muted})" />
</div>
</div>
<div class="body">
<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"
:snapclient="client.snapclient"
@modal-show="$emit('modal-show', $event)"
@volume-change="$emit('client-volume-change', $event)"
@mute-toggle="$emit('client-mute-toggle', $event)" />
</div>
</div>
</template>
<script>
import ToggleSwitch from "@/components/elements/ToggleSwitch";
import Client from "@/components/panels/MusicSnapcast/Client";
export default {
name: "Group",
components: {Client, ToggleSwitch},
emits: ['group-mute-toggle', 'modal-show', 'client-volume-change', 'client-mute-toggle'],
props: {
id: {
type: String,
},
clients: {
type: Object,
default: () => {},
},
muted: {
type: Boolean,
},
name: {
type: String,
},
stream: {
type: Object,
},
server: {
type: Object,
},
},
}
</script>
<style lang="scss" scoped>
.group {
.head {
display: flex;
background: $default-bg-4;
border-top: $default-border-2;
border-bottom: $default-border-2;
border-radius: 0;
cursor: pointer;
&:hover {
color: $default-hover-fg;
}
}
.head,
.client {
display: flex;
align-items: center;
padding: 1em .5em;
}
}
</style>

View file

@ -0,0 +1,104 @@
<template>
<div class="host">
<div class="header">
<div class="col-10 name" @click="$emit('modal-show', {type: 'host', host: server.host.name})">
<i class="icon fa fa-server"></i>
{{ server.host.name }}
</div>
<div class="col-2 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>
<div class="group-container" v-if="!collapsed">
<Group v-for="(group, id) in groups" :key="id"
:id="group.id"
:name="group.name"
:server="server.host"
:muted="group.muted"
:clients="group.clients"
:stream="streams[group.stream_id]"
@modal-show="$emit('modal-show', $event)"
@group-mute-toggle="$emit('group-mute-toggle', $event)"
@client-mute-toggle="$emit('client-mute-toggle', $event)"
@client-volume-change="$emit('client-volume-change', $event)"
/>
</div>
</div>
</template>
<script>
import Group from "@/components/panels/MusicSnapcast/Group";
export default {
name: "Host",
emits: ['modal-show', 'group-mute-toggle', 'client-mute-toggle', 'client-volume-change'],
components: {Group},
props: {
groups: {
type: Object,
default: () => {},
},
server: {
type: Object,
default: () => {},
},
streams: {
type: Object,
default: () => {},
},
},
data() {
return {
collapsed: false,
}
},
}
</script>
<style lang="scss" scoped>
.host {
width: 95%;
max-width: 1000px;
margin: 1em auto;
border: $default-border-2;
border-radius: .5em;
box-shadow: $border-shadow-bottom-right;
background: $default-bg-2;
.header {
padding: .5em;
background: $default-bg-5;
border-bottom: $default-border-2;
border-radius: .5em .5em 0 0;
display: flex;
align-items: center;
.name {
text-transform: uppercase;
&:hover {
color: $default-hover-fg;
cursor: pointer;
}
}
.buttons {
margin-bottom: 0;
}
button {
padding: 0;
border: 0;
background: none;
&:hover { color: $default-hover-fg; }
}
}
}
</style>

View file

@ -0,0 +1,500 @@
<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>

View file

@ -0,0 +1,178 @@
<template>
<div class="client-modal">
<div class="info" v-if="client">
<div class="row">
<div class="label col-s-12 col-m-3">ID</div>
<div class="value col-s-12 col-m-9" v-text="client.id"></div>
</div>
<div class="row" v-if="client.config?.name?.length || client.host?.name">
<div class="label col-s-12 col-m-3">Name</div>
<div class="value col-s-12 col-m-9">
<span class="name" v-text="client.config?.name || client.host?.name"></span>
<button title="Rename" @click="renameClient">
<i class="fa fa-edit" />
</button>
</div>
</div>
<div class="row">
<div class="label col-s-12 col-m-3">Connected</div>
<div class="value col-s-12 col-m-9" v-text="client.connected"></div>
</div>
<div class="row">
<div class="label col-s-12 col-m-3">Volume</div>
<div class="value col-s-12 col-m-9">{{ client.config.volume.percent }}%</div>
</div>
<div class="row">
<div class="label col-s-12 col-m-3">Muted</div>
<div class="value col-s-12 col-m-9" v-text="client.config.volume.muted"></div>
</div>
<div class="row">
<div class="label col-s-12 col-m-3">Latency</div>
<div class="value col-s-12 col-m-9" v-text="client.config.latency"></div>
</div>
<div class="row" v-if="client.host.ip && client.host.ip.length">
<div class="label col-s-12 col-m-3">IP Address</div>
<div class="value col-s-12 col-m-9" v-text="client.host.ip"></div>
</div>
<div class="row" v-if="client.host.mac && client.host.mac.length">
<div class="label col-s-12 col-m-3">MAC Address</div>
<div class="value col-s-12 col-m-9" v-text="client.host.mac"></div>
</div>
<div class="row" v-if="client.host.os && client.host.os.length">
<div class="label col-s-12 col-m-3">OS</div>
<div class="value col-s-12 col-m-9" v-text="client.host.os"></div>
</div>
<div class="row" v-if="client.host.arch && client.host.arch.length">
<div class="label col-s-12 col-m-3">Architecture</div>
<div class="value col-s-12 col-m-9" v-text="client.host.arch"></div>
</div>
<div class="row">
<div class="label col-s-12 col-m-3">Client name</div>
<div class="value col-s-12 col-m-9" v-text="client.snapclient.name"></div>
</div>
<div class="row">
<div class="label col-s-12 col-m-3">Client version</div>
<div class="value col-s-12 col-m-9" v-text="client.snapclient.version"></div>
</div>
<div class="row">
<div class="label col-s-12 col-m-3">Protocol version</div>
<div class="value col-s-12 col-m-9" v-text="client.snapclient.protocolVersion"></div>
</div>
</div>
<div class="buttons">
<div class="row">
<button type="button" :disabled="loading" @click="removeClient">
<i class="fas fa-trash" />
<span class="name">Remove client</span>
</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: "ClientModal",
emits: ['remove-client', 'rename-client'],
props: {
loading: {
type: Boolean,
default: false,
},
client: {
type: Object,
},
},
methods: {
removeClient() {
if (!window.confirm('Are you sure that you want to remove this client?'))
return
this.$emit('remove-client')
},
renameClient() {
const name = (window.prompt('New client name',
this.client.config.name?.length ? this.client.config.name : this.client.host.name) || '').trim()
if (!name.length)
return
this.$emit('rename-client', name)
},
}
}
</script>
<style lang="scss" scoped>
.client-modal {
max-height: 75vh;
display: flex;
flex-direction: column;
.info {
height: 80%;
overflow: auto;
}
button {
background: none;
border: none;
padding: 0;
margin: 0 .5em;
&:hover {
color: $default-hover-fg-2;
}
}
.buttons {
height: 20%;
margin: 0 !important;
padding: 0 !important;
.row {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
padding: 0;
&:hover {
background: none;
}
button {
width: 100%;
height: 100%;
padding: 1em;
color: #900;
border-color: #900;
.name {
margin-left: .5em;
}
&:hover {
background: $hover-bg;
}
}
}
}
}
</style>

View file

@ -0,0 +1,161 @@
<template>
<div class="info">
<div class="section name">
<div class="title">Name</div>
<div class="row">
<div class="name-value">
<span class="name" v-text="group.name?.length ? group.name : 'default'" />
<button class="pull-right" title="Rename" @click="renameGroup">
<i class="fa fa-edit" />
</button>
</div>
</div>
</div>
<div class="section clients" v-if="Object.keys(group?.clients || {}).length > 0">
<div class="title">Clients</div>
<div class="row" ref="groupClients" v-for="(client, id) in (clients || {})" :key="id">
<label class="client" :for="'snapcast-client-' + client.id">
<input type="checkbox"
class="client"
:id="`snapcast-client-${client.id}`"
:value="client.id"
:checked="client.id in group.clients"
:disabled="loading"
@input="$emit($event.target.checked ? 'add-client' : 'remove-client', client.id)">
{{ client.host.name }}
</label>
</div>
</div>
<div class="section streams" v-if="group?.stream_id">
<div class="title">Stream</div>
<div class="row">
<div class="label col-3">ID</div>
<div class="value col-9">
<label>
<select ref="streamSelect" @change="$emit('stream-change', $event.target.value)">
<option
v-for="(stream, id) in streams" :key="id"
v-text="streams[group.stream_id].id"
:name="stream.id"
:value="stream.id"
:disabled="loading"
:selected="stream.id === group.stream_id">
</option>
</select>
</label>
</div>
</div>
<div class="row" v-if="streams?.[group.stream_id]?.status">
<div class="label col-m-3">Status</div>
<div class="value col-m-9" v-text="streams[group.stream_id].status"></div>
</div>
<div class="row" v-if="streams?.[group?.stream_id]?.uri?.host">
<div class="label col-s-12 col-m-3">Host</div>
<div class="value col-s-12 col-m-9" v-text="streams[group.stream_id].uri.host"></div>
</div>
<div class="row" v-if="streams?.[group?.stream_id]?.uri?.path">
<div class="label col-s-12 col-m-3">Path</div>
<div class="value col-s-12 col-m-9" v-text="streams[group.stream_id].uri.path"></div>
</div>
<div class="row" v-if="streams?.[group?.stream_id]?.uri?.raw">
<div class="label col-s-12 col-m-3">URI</div>
<div class="value col-s-12 col-m-9" v-text="streams[group.stream_id].uri.raw"></div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "GroupModal",
emits: ['add-client', 'remove-client', 'stream-change', 'rename-group'],
props: {
loading: {
type: Boolean,
default: false,
},
group: {
type: Object,
},
clients: {
type: Object,
},
streams: {
type: Object,
},
},
methods: {
renameGroup() {
const name = (prompt('New group name', this.group.name) || '').trim()
if (!name?.length)
return
this.$emit('rename-group', name)
}
},
}
</script>
<style lang="scss" scoped>
.info {
.row {
display: flex;
align-items: center;
}
.section {
padding: 1.5em;
.row {
align-items: normal;
}
}
label.client {
width: 100%;
}
.title {
font-size: 1em;
padding-left: .5em;
padding-bottom: .5em;
margin-bottom: .5em;
border-bottom: $default-border;
}
.client {
display: flex;
align-items: center;
input {
margin-right: .5em;
}
}
.name-value {
display: flex;
align-items: center;
button {
background: none;
border: none;
margin: 0 1em;
padding: 0;
&:hover {
color: $default-hover-fg-2;
}
}
}
}
</style>

View file

@ -0,0 +1,69 @@
<template>
<div class="info">
<div class="row" v-if="info?.server?.host?.ip?.length">
<div class="label col-3">IP Address</div>
<div class="value col-9" v-text="info.server.host.ip"></div>
</div>
<div class="row" v-if="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?.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?.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?.length">
<div class="label col-3">Architecture</div>
<div class="value col-9" v-text="info.server.host.arch"></div>
</div>
<div class="row" v-if="info?.server?.snapserver?.name?.length">
<div class="label col-3">Server name</div>
<div class="value col-9" v-text="info.server.snapserver.name"></div>
</div>
<div class="row" v-if="info?.server?.snapserver?.version?.length">
<div class="label col-3">Server version</div>
<div class="value col-9" v-text="info.server.snapserver.version"></div>
</div>
<div class="row" v-if="info?.server?.snapserver?.protocolVersion">
<div class="label col-3">Protocol version</div>
<div class="value col-9" v-text="info.server.snapserver.protocolVersion"></div>
</div>
<div class="row" v-if="info?.server?.snapserver?.controlProtocolVersion">
<div class="label col-3">Control protocol version</div>
<div class="value col-9" v-text="info.server.snapserver.controlProtocolVersion"></div>
</div>
</div>
</template>
<script>
export default {
name: "HostModal",
props: {
info: {
type: Object,
default: () => {},
}
}
}
</script>
<style scoped>
</style>

View file

@ -99,6 +99,7 @@ $widths: (
.pull-right {
text-align: right;
float: right;
justify-content: right;
}
.hidden {

View file

@ -50,6 +50,7 @@ $border-shadow-top: 0 -2.5px 4px 0 $default-shadow-color;
$border-shadow-bottom: 0 3px 2px -1px $default-shadow-color;
$border-shadow-left: -2.5px 0 4px 0 $default-shadow-color;
$border-shadow-right: 2.5px 0 4px 0 $default-shadow-color;
$border-shadow-bottom-right: 2.5px 2.5px 3px 0 $default-shadow-color;
//// Modals
$modal-header-bg: #e0e0e0 !default;
@ -76,6 +77,9 @@ $default-hover-fg-2: #38cf80 !default;
$hover-bg: #bef6da !default;
$active-bg: #8fefb7 !default;
/// Disabled
$disabled-fg: rgb(155, 155, 155);
/// Navigator
$nav-bg: #002626 !default;
$nav-fg: #e8f8e8 !default;

View file

@ -92,8 +92,8 @@ class MusicSnapcastPlugin(Plugin):
def _status(self, sock):
request = {
'id': self._get_req_id(),
'jsonrpc':'2.0',
'method':'Server.GetStatus'
'jsonrpc': '2.0',
'method': 'Server.GetStatus'
}
self._send(sock, request)
@ -212,9 +212,10 @@ class MusicSnapcastPlugin(Plugin):
return self._status(sock)
finally:
try: sock.close()
except: pass
try:
sock.close()
except:
pass
@action
def mute(self, client=None, group=None, mute=None, host=None, port=None):
@ -247,7 +248,7 @@ class MusicSnapcastPlugin(Plugin):
sock = self._connect(host or self.host, port or self.port)
request = {
'id': self._get_req_id(),
'jsonrpc':'2.0',
'jsonrpc': '2.0',
'method': 'Group.SetMute' if group else 'Client.SetVolume',
'params': {}
}
@ -268,8 +269,10 @@ class MusicSnapcastPlugin(Plugin):
self._send(sock, request)
return self._recv(sock)
finally:
try: sock.close()
except: pass
try:
sock.close()
except:
pass
@action
def volume(self, client, volume=None, delta=None, mute=None, host=None,
@ -306,7 +309,7 @@ class MusicSnapcastPlugin(Plugin):
sock = self._connect(host or self.host, port or self.port)
request = {
'id': self._get_req_id(),
'jsonrpc':'2.0',
'jsonrpc': '2.0',
'method': 'Client.SetVolume',
'params': {}
}
@ -336,8 +339,10 @@ class MusicSnapcastPlugin(Plugin):
self._send(sock, request)
return self._recv(sock)
finally:
try: sock.close()
except: pass
try:
sock.close()
except:
pass
@action
def set_client_name(self, client, name, host=None, port=None):
@ -363,7 +368,7 @@ class MusicSnapcastPlugin(Plugin):
sock = self._connect(host or self.host, port or self.port)
request = {
'id': self._get_req_id(),
'jsonrpc':'2.0',
'jsonrpc': '2.0',
'method': 'Client.SetName',
'params': {}
}
@ -374,8 +379,50 @@ class MusicSnapcastPlugin(Plugin):
self._send(sock, request)
return self._recv(sock)
finally:
try: sock.close()
except: pass
try:
sock.close()
except:
pass
@action
def set_group_name(self, group, name, host=None, port=None):
"""
Set/change the name of a group
:param group: Group ID to rename
:type group: str
:param name: New name
:type name: str
:param host: Snapcast server (default: default configured host)
:type host: str
:param port: Snapcast server port (default: default configured port)
:type port: int
"""
sock = None
try:
sock = self._connect(host or self.host, port or self.port)
request = {
'id': self._get_req_id(),
'jsonrpc': '2.0',
'method': 'Group.SetName',
'params': {
'id': group,
'name': name,
}
}
self._send(sock, request)
return self._recv(sock)
finally:
try:
sock.close()
except:
pass
@action
def set_latency(self, client, latency, host=None, port=None):
@ -401,7 +448,7 @@ class MusicSnapcastPlugin(Plugin):
sock = self._connect(host or self.host, port or self.port)
request = {
'id': self._get_req_id(),
'jsonrpc':'2.0',
'jsonrpc': '2.0',
'method': 'Client.SetLatency',
'params': {
'latency': latency
@ -413,8 +460,10 @@ class MusicSnapcastPlugin(Plugin):
self._send(sock, request)
return self._recv(sock)
finally:
try: sock.close()
except: pass
try:
sock.close()
except:
pass
@action
def delete_client(self, client, host=None, port=None):
@ -437,7 +486,7 @@ class MusicSnapcastPlugin(Plugin):
sock = self._connect(host or self.host, port or self.port)
request = {
'id': self._get_req_id(),
'jsonrpc':'2.0',
'jsonrpc': '2.0',
'method': 'Server.DeleteClient',
'params': {}
}
@ -447,8 +496,10 @@ class MusicSnapcastPlugin(Plugin):
self._send(sock, request)
return self._recv(sock)
finally:
try: sock.close()
except: pass
try:
sock.close()
except:
pass
@action
def group_set_clients(self, group, clients, host=None, port=None):
@ -475,7 +526,7 @@ class MusicSnapcastPlugin(Plugin):
group = self._get_group(sock, group)
request = {
'id': self._get_req_id(),
'jsonrpc':'2.0',
'jsonrpc': '2.0',
'method': 'Group.SetClients',
'params': {
'id': group['id'],
@ -490,9 +541,10 @@ class MusicSnapcastPlugin(Plugin):
self._send(sock, request)
return self._recv(sock)
finally:
try: sock.close()
except: pass
try:
sock.close()
except:
pass
@action
def group_set_stream(self, group, stream_id, host=None, port=None):
@ -519,7 +571,7 @@ class MusicSnapcastPlugin(Plugin):
group = self._get_group(sock, group)
request = {
'id': self._get_req_id(),
'jsonrpc':'2.0',
'jsonrpc': '2.0',
'method': 'Group.SetStream',
'params': {
'id': group['id'],
@ -530,9 +582,10 @@ class MusicSnapcastPlugin(Plugin):
self._send(sock, request)
return self._recv(sock)
finally:
try: sock.close()
except: pass
try:
sock.close()
except:
pass
@action
def get_backend_hosts(self):
@ -575,7 +628,7 @@ class MusicSnapcastPlugin(Plugin):
def _worker(host, port):
try:
if exclude_local and (host == 'localhost'
or host == Config.get('device_id')):
or host == Config.get('device_id')):
return
server_status = self.status(host=host, port=port).output
@ -586,13 +639,13 @@ class MusicSnapcastPlugin(Plugin):
return
group = [g for g in server_status.get('groups', {})
if g.get('id') == client_status.get('group_id')].pop(0)
if g.get('id') == client_status.get('group_id')].pop(0)
if group.get('muted'):
return
stream = [s for s in server_status.get('streams')
if s.get('id') == group.get('stream_id')].pop(0)
if s.get('id') == group.get('stream_id')].pop(0)
if stream.get('status') != 'playing':
return
@ -601,12 +654,12 @@ class MusicSnapcastPlugin(Plugin):
except Exception as e:
self.logger.warning(('Error while retrieving the status of ' +
'Snapcast host at {}:{}: {}').format(
host, port, str(e)))
host, port, str(e)))
workers = []
for host, port in backend_hosts.items():
w = threading.Thread(target=_worker, args=(host,port))
w = threading.Thread(target=_worker, args=(host, port))
w.start()
workers.append(w)
@ -616,5 +669,4 @@ class MusicSnapcastPlugin(Plugin):
return {'hosts': playing_hosts}
# vim:sw=4:ts=4:et:

View file

@ -142,9 +142,6 @@ websocket-client
# mpv player plugin
# python-mpv
# SCSS/SASS to CSS compiler for web pages style
pyScss
# Support for NFC tags
# nfcpy >= 1.0
# ndeflib

View file

@ -58,7 +58,6 @@ setup(
'redis',
'requests',
'croniter',
'pyScss',
'sqlalchemy',
'websockets',
'websocket-client',