music.mpd panel migration - WIP
This commit is contained in:
parent
bc3e0b8634
commit
b4fc734a15
66 changed files with 1642 additions and 128 deletions
2
platypush/backend/http/dist/index.html
vendored
2
platypush/backend/http/dist/index.html
vendored
|
@ -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-24ff873d.5ef32028.css" rel="prefetch"><link href="/static/css/chunk-35b45d59.bea59a51.css" rel="prefetch"><link href="/static/css/chunk-45939517.1062df39.css" rel="prefetch"><link href="/static/css/chunk-4bbbb9a3.431b3300.css" rel="prefetch"><link href="/static/css/chunk-53360c78.2281ab32.css" rel="prefetch"><link href="/static/css/chunk-62a3d08e.495b15c5.css" rel="prefetch"><link href="/static/css/chunk-6fa461b6.1fb05517.css" rel="prefetch"><link href="/static/css/chunk-e8078048.6c400707.css" rel="prefetch"><link href="/static/js/chunk-24ff873d.f955ad3b.js" rel="prefetch"><link href="/static/js/chunk-2d2091df.8d32d3ba.js" rel="prefetch"><link href="/static/js/chunk-35b45d59.d541d43f.js" rel="prefetch"><link href="/static/js/chunk-45939517.38162e50.js" rel="prefetch"><link href="/static/js/chunk-4bbbb9a3.6f0e4975.js" rel="prefetch"><link href="/static/js/chunk-53360c78.54e2e626.js" rel="prefetch"><link href="/static/js/chunk-62a3d08e.8fc4fd3a.js" rel="prefetch"><link href="/static/js/chunk-6fa461b6.d6fffa0d.js" rel="prefetch"><link href="/static/js/chunk-e8078048.e668de5f.js" rel="prefetch"><link href="/static/css/app.5408c936.css" rel="preload" as="style"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="preload" as="style"><link href="/static/js/app.41ee3441.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.1eac7b43.js" rel="preload" as="script"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="stylesheet"><link href="/static/css/app.5408c936.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.1eac7b43.js"></script><script src="/static/js/app.41ee3441.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-24ff873d.64d9bc0b.css" rel="prefetch"><link href="/static/css/chunk-35b45d59.0c4a18da.css" rel="prefetch"><link href="/static/css/chunk-37df3df6.fe6f1cbc.css" rel="prefetch"><link href="/static/css/chunk-3d60f62e.2026dd4f.css" rel="prefetch"><link href="/static/css/chunk-45939517.e4a1ddf3.css" rel="prefetch"><link href="/static/css/chunk-4bbbb9a3.3108d379.css" rel="prefetch"><link href="/static/css/chunk-53360c78.c486a396.css" rel="prefetch"><link href="/static/css/chunk-545459d0.009b6a70.css" rel="prefetch"><link href="/static/css/chunk-62a3d08e.6cb54f10.css" rel="prefetch"><link href="/static/css/chunk-d8561e02.b52f89a0.css" rel="prefetch"><link href="/static/css/chunk-e8078048.c6785c78.css" rel="prefetch"><link href="/static/js/chunk-24ff873d.f955ad3b.js" rel="prefetch"><link href="/static/js/chunk-2d2091df.b017f6d4.js" rel="prefetch"><link href="/static/js/chunk-2d21da1a.867fde19.js" rel="prefetch"><link href="/static/js/chunk-35b45d59.d541d43f.js" rel="prefetch"><link href="/static/js/chunk-37df3df6.8441b420.js" rel="prefetch"><link href="/static/js/chunk-3d60f62e.8cc48f2d.js" rel="prefetch"><link href="/static/js/chunk-45939517.38162e50.js" rel="prefetch"><link href="/static/js/chunk-4bbbb9a3.6f0e4975.js" rel="prefetch"><link href="/static/js/chunk-53360c78.54e2e626.js" rel="prefetch"><link href="/static/js/chunk-545459d0.aa1e42a3.js" rel="prefetch"><link href="/static/js/chunk-62a3d08e.8fc4fd3a.js" rel="prefetch"><link href="/static/js/chunk-d8561e02.78e44394.js" rel="prefetch"><link href="/static/js/chunk-e8078048.e668de5f.js" rel="prefetch"><link href="/static/css/app.e87398b0.css" rel="preload" as="style"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="preload" as="style"><link href="/static/js/app.4ae7a178.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.63de75fe.js" rel="preload" as="script"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="stylesheet"><link href="/static/css/app.e87398b0.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.63de75fe.js"></script><script src="/static/js/app.4ae7a178.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
15
platypush/backend/http/dist/static/css/chunk-37df3df6.fe6f1cbc.css
vendored
Normal file
15
platypush/backend/http/dist/static/css/chunk-37df3df6.fe6f1cbc.css
vendored
Normal file
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
1
platypush/backend/http/dist/static/css/chunk-545459d0.009b6a70.css
vendored
Normal file
1
platypush/backend/http/dist/static/css/chunk-545459d0.009b6a70.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/css/chunk-d8561e02.b52f89a0.css
vendored
Normal file
1
platypush/backend/http/dist/static/css/chunk-d8561e02.b52f89a0.css
vendored
Normal file
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
2
platypush/backend/http/dist/static/js/app.4ae7a178.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/app.4ae7a178.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/app.4ae7a178.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/app.4ae7a178.js.map
vendored
Normal file
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
2
platypush/backend/http/dist/static/js/chunk-2d2091df.b017f6d4.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-2d2091df.b017f6d4.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-2d2091df.b017f6d4.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-2d2091df.b017f6d4.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-2d21da1a.867fde19.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-2d21da1a.867fde19.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-2d21da1a.867fde19.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-2d21da1a.867fde19.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-37df3df6.8441b420.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-37df3df6.8441b420.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-37df3df6.8441b420.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-37df3df6.8441b420.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-3d60f62e.8cc48f2d.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-3d60f62e.8cc48f2d.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-3d60f62e.8cc48f2d.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-3d60f62e.8cc48f2d.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-545459d0.aa1e42a3.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-545459d0.aa1e42a3.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-545459d0"],{8285:function(e,n,u){"use strict";var t=u("7a23"),o=Object(t["J"])("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.aa1e42a3.js.map
|
1
platypush/backend/http/dist/static/js/chunk-545459d0.aa1e42a3.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-545459d0.aa1e42a3.js.map
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"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.aa1e42a3.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":""}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-d8561e02.78e44394.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-d8561e02.78e44394.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-d8561e02"],{"49c1":function(e,t,n){},"9e1b":function(e,t,n){"use strict";n("49c1")},dabe:function(e,t,n){"use strict";n.r(t);var i=n("7a23"),c=Object(i["J"])("data-v-3565b88b");Object(i["u"])("data-v-3565b88b");var o={class:"plugin"};Object(i["s"])();var a=c((function(e,t,n,c,a,r){var s=Object(i["z"])("Loading");return Object(i["r"])(),Object(i["e"])("div",o,[a.loading?(Object(i["r"])(),Object(i["e"])(s,{key:0})):a.component?(Object(i["r"])(),Object(i["e"])(Object(i["A"])(a.component),{key:1,config:a.config},null,8,["config"])):Object(i["f"])("",!0)])})),r=(n("a15b"),n("d81d"),n("fb6a"),n("d3b7"),n("ac1f"),n("1276"),n("96cf"),n("1da1")),s=n("3e54"),u=n("3a5e"),p={name:"Plugin",components:{Loading:u["a"]},mixins:[s["a"]],props:{pluginName:{type:String,required:!0}},data:function(){return{loading:!1,component:null,config:{}}},computed:{componentName:function(){return this.pluginName.split(".").map((function(e){return e[0].toUpperCase()+e.slice(1)})).join("")}},methods:{refresh:function(){var e=Object(r["a"])(regeneratorRuntime.mark((function e(){var t,c=this;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return this.loading=!0,e.prev=1,this.component=Object(i["i"])((function(){return n("0f0c")("./".concat(c.componentName,"/Index"))})),this.$options.components[this.componentName]=this.component,e.next=6,this.request("config.get_plugins");case 6:if(e.t2=t=e.sent,e.t1=null===e.t2,e.t1){e.next=10;break}e.t1=void 0===t;case 10:if(!e.t1){e.next=14;break}e.t3=void 0,e.next=15;break;case 14:e.t3=t[this.pluginName];case 15:if(e.t0=e.t3,e.t0){e.next=18;break}e.t0={};case 18:this.config=e.t0;case 19:return e.prev=19,this.loading=!1,e.finish(19);case 22:case"end":return e.stop()}}),e,this,[[1,,19,22]])})));function t(){return e.apply(this,arguments)}return t}()},mounted:function(){this.refresh()}};n("9e1b");p.render=a,p.__scopeId="data-v-3565b88b";t["default"]=p}}]);
|
||||
//# sourceMappingURL=chunk-d8561e02.78e44394.js.map
|
1
platypush/backend/http/dist/static/js/chunk-d8561e02.78e44394.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-d8561e02.78e44394.js.map
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":3,"sources":["webpack:///./src/components/widgets/Plugin/Index.vue?bbf0","webpack:///./src/components/widgets/Plugin/Index.vue","webpack:///./src/components/widgets/Plugin/Index.vue?fc8a"],"names":["class","loading","component","config","name","components","Loading","mixins","Utils","props","pluginName","type","String","required","data","computed","componentName","this","split","map","t","toUpperCase","slice","join","methods","refresh","$options","request","mounted","render","__scopeId"],"mappings":"2IAAA,W,sICCOA,MAAM,U,wGAAX,eAGM,MAHN,EAGM,CAFW,EAAAC,S,iBAAf,eAA0B,YAC6B,EAAAC,W,iBAAvD,eAAoE,eAApD,EAAAA,WAAS,C,MAAGC,OAAQ,EAAAA,Q,2JASzB,GACbC,KAAM,SACNC,WAAY,CAACC,UAAA,MACbC,OAAQ,CAACC,EAAA,MACTC,MAAO,CAELC,WAAY,CACVC,KAAMC,OACNC,UAAU,IAIdC,KAZa,WAaX,MAAO,CACLb,SAAS,EACTC,UAAW,KACXC,OAAQ,KAIZY,SAAU,CACRC,cADQ,WAEN,OAAOC,KAAKP,WAAWQ,MAAM,KAAKC,KAAI,SAACC,GAAD,OAAOA,EAAE,GAAGC,cAAgBD,EAAEE,MAAM,MAAIC,KAAK,MAIvFC,QAAS,CACPC,QAAS,WAAF,8CAAE,kHACPR,KAAKhB,SAAU,EADR,SAILgB,KAAKf,UAAY,gBAAqB,kBAAM,UAAO,YAAuB,EAAKc,cAAnC,cAC5CC,KAAKS,SAASrB,WAAWY,KAAKD,eAAiBC,KAAKf,UAL/C,SAMgBe,KAAKU,QAAQ,sBAN7B,0JAMS,EAA6CV,KAAKP,YAN3D,gDAM0E,GAN1E,QAMLO,KAAKd,OANA,8BAQLc,KAAKhB,SAAU,EARV,2EAAF,qDAAE,IAaX2B,QAAS,WACPX,KAAKQ,Y,UChDT,EAAOI,OAAS,EAChB,EAAOC,UAAY,kBAEJ","file":"static/js/chunk-d8561e02.78e44394.js","sourcesContent":["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!./Index.vue?vue&type=style&index=0&id=3565b88b&lang=scss&scoped=true\"","<template>\n <div class=\"plugin\">\n <Loading v-if=\"loading\" />\n <component :is=\"component\" :config=\"config\" v-else-if=\"component\" />\n </div>\n</template>\n\n<script>\nimport Utils from \"@/Utils\";\nimport Loading from \"@/components/Loading\";\nimport {defineAsyncComponent} from \"vue\";\n\nexport default {\n name: \"Plugin\",\n components: {Loading},\n mixins: [Utils],\n props: {\n // Name of the plugin view to be loaded\n pluginName: {\n type: String,\n required: true,\n },\n },\n\n data() {\n return {\n loading: false,\n component: null,\n config: {},\n }\n },\n\n computed: {\n componentName() {\n return this.pluginName.split('.').map((t) => t[0].toUpperCase() + t.slice(1)).join('')\n },\n },\n\n methods: {\n refresh: async function() {\n this.loading = true\n\n try {\n this.component = defineAsyncComponent(() => import(`@/components/panels/${this.componentName}/Index`))\n this.$options.components[this.componentName] = this.component\n this.config = (await this.request('config.get_plugins'))?.[this.pluginName] || {}\n } finally {\n this.loading = false\n }\n },\n },\n\n mounted: function() {\n this.refresh()\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.plugin {\n margin: -1em 0 0 -1em !important;\n padding: 0;\n width: calc(100% + 2em);\n height: calc(100% + 2em);\n}\n</style>\n","import { render } from \"./Index.vue?vue&type=template&id=3565b88b&scoped=true&bindings={\\\"pluginName\\\":\\\"props\\\",\\\"loading\\\":\\\"data\\\",\\\"component\\\":\\\"data\\\",\\\"config\\\":\\\"data\\\",\\\"componentName\\\":\\\"options\\\",\\\"refresh\\\":\\\"options\\\"}\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\n\nimport \"./Index.vue?vue&type=style&index=0&id=3565b88b&lang=scss&scoped=true\"\nscript.render = render\nscript.__scopeId = \"data-v-3565b88b\"\n\nexport default script"],"sourceRoot":""}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,21 +1,27 @@
|
|||
# webapp
|
||||
# Platypush web app
|
||||
|
||||
The UI for Platypush is built with Vue 3. The production-ready files are distributed with the package under `platypush/backend/http/dist`. If you want to change/debug/redistribute some parts of the UI you'll need `npm` installed. The directory of this file is the root of the web app project and all the following commands should be typed from here.
|
||||
|
||||
## Project setup
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
### Compilation and hot-reload for development
|
||||
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
"icons": {
|
||||
"light.hue": {
|
||||
"class": "fas fa-lightbulb"
|
||||
},
|
||||
"music.mpd": {
|
||||
"class": "fas fa-music"
|
||||
}
|
||||
}
|
||||
}
|
468
platypush/backend/http/webapp/src/components/Media/Controls.vue
Normal file
468
platypush/backend/http/webapp/src/components/Media/Controls.vue
Normal file
|
@ -0,0 +1,468 @@
|
|||
<template>
|
||||
<div class="extension fade-in" :class="{hidden: !expanded}">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="buttons">
|
||||
<button @click="$emit('previous')" title="Play previous track" v-if="buttons.previous">
|
||||
<i class="icon fa fa-step-backward"></i>
|
||||
</button>
|
||||
<button @click="$emit('stop')" v-if="buttons.stop && status.state !== 'stop'" title="Stop playback">
|
||||
<i class="icon fa fa-stop"></i>
|
||||
</button>
|
||||
<button @click="$emit('next')" title="Play next track" v-if="buttons.next">
|
||||
<i class="icon fa fa-step-forward"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-9 volume-container">
|
||||
<div class="col-1">
|
||||
<button :disabled="status.muted == null" @click="$emit(status.muted ? 'unmute' : 'mute')">
|
||||
<i class="icon fa fa-volume-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-11 volume-slider">
|
||||
<Slider :value="status.volume" :range="volumeRange" :disabled="status.volume == null"
|
||||
@mouseup="$emit('set-volume', $event.target.value)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-3 list-controls">
|
||||
<button @click="$emit('consume', !status.consume)" :class="{enabled: status.consume}"
|
||||
title="Toggle consume mode" v-if="buttons.consume">
|
||||
<i class="icon fa fa-utensils"></i>
|
||||
</button>
|
||||
|
||||
<button @click="$emit('random', !status.random)" :class="{enabled: status.random}"
|
||||
title="Toggle shuffle" v-if="buttons.random">
|
||||
<i class="icon fa fa-random"></i>
|
||||
</button>
|
||||
|
||||
<button @click="$emit('repeat', !status.repeat)" :class="{enabled: status.repeat}"
|
||||
title="Toggle repeat" v-if="buttons.repeat">
|
||||
<i class="icon fa fa-redo"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-s-2 col-m-1 time">
|
||||
<span class="elapsed-time"
|
||||
v-text="elapsed != null && status.state !== 'stop' ? convertTime(elapsed) : '-:--'"></span>
|
||||
</div>
|
||||
<div class="col-s-8 col-m-10">
|
||||
<Slider :value="elapsed" :range="[0, duration]" :disabled="!duration || status.state === 'stop'"
|
||||
@mouseup="$emit('seek', $event.target.value)" />
|
||||
</div>
|
||||
<div class="col-s-2 col-m-1 time">
|
||||
<span class="total-time"
|
||||
v-text="duration && status.state !== 'stop' ? convertTime(duration) : '-:--'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="playback-controls mobile tablet col-2">
|
||||
<button @click="$emit(status.state === 'play' ? 'pause' : 'play')"
|
||||
:title="status.state === 'play' ? 'Pause' : 'Play'">
|
||||
<i class="icon play-pause fa fa-pause" v-if="status.state === 'play'"></i>
|
||||
<i class="icon play-pause fa fa-play" v-else></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="track-container col-s-8 col-m-8 col-l-3">
|
||||
<div class="track-info" v-if="track && status?.state !== 'stop'">
|
||||
<div class="title">
|
||||
<a href="#" v-text="track.title" @click="$emit('search', {album: track.album})" v-if="track.album"></a>
|
||||
<span v-text="track.title" v-else></span>
|
||||
</div>
|
||||
<div class="artist" v-if="track.artist">
|
||||
<a href="#" v-text="track.artist" @click="$emit('search', {artist: track.artist})"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="playback-controls desktop col-6">
|
||||
<div class="row buttons">
|
||||
<button @click="$emit('previous')" title="Play previous track" v-if="buttons.previous">
|
||||
<i class="icon fa fa-step-backward"></i>
|
||||
</button>
|
||||
<button @click="$emit(status.state === 'play' ? 'pause' : 'play')"
|
||||
:title="status.state === 'play' ? 'Pause' : 'Play'">
|
||||
<i class="icon play-pause fa fa-pause" v-if="status.state === 'play'"></i>
|
||||
<i class="icon play-pause fa fa-play" v-else></i>
|
||||
</button>
|
||||
<button @click="$emit('stop')" v-if="buttons.stop && status.state !== 'stop'" title="Stop playback">
|
||||
<i class="icon fa fa-stop"></i>
|
||||
</button>
|
||||
<button @click="$emit('next')" title="Play next track" v-if="buttons.next">
|
||||
<i class="icon fa fa-step-forward"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-1 time">
|
||||
<span class="elapsed-time"
|
||||
v-text="elapsed != null && status.state !== 'stop' ? convertTime(elapsed) : '-:--'"></span>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<Slider :value="elapsed" :range="[0, duration]" :disabled="!duration || status.state === 'stop'"
|
||||
@mouseup="$emit('seek', $event.target.value)" />
|
||||
</div>
|
||||
<div class="col-1 time">
|
||||
<span class="total-time"
|
||||
v-text="duration && status.state !== 'stop' ? convertTime(duration) : '-:--'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-2 pull-right mobile tablet right-buttons">
|
||||
<button @click="expanded = !expanded" :title="expanded ? 'Show more controls' : 'Hide extra controls'">
|
||||
<i class="fas" :class="[`fa-chevron-${expanded ? 'down' : 'up'}`]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-3 pull-right desktop">
|
||||
<div class="row list-controls">
|
||||
<button @click="$emit('consume')" :class="{enabled: status.consume}" title="Toggle consume mode" v-if="buttons.consume">
|
||||
<i class="icon fa fa-utensils"></i>
|
||||
</button>
|
||||
<button @click="$emit('random')" :class="{enabled: status.random}" title="Toggle shuffle" v-if="buttons.random">
|
||||
<i class="icon fa fa-random"></i>
|
||||
</button>
|
||||
<button @click="$emit('repeat')" :class="{enabled: status.repeat}" title="Toggle repeat" v-if="buttons.repeat">
|
||||
<i class="icon fa fa-redo"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row volume-container">
|
||||
<div class="col-2">
|
||||
<button :disabled="status.muted == null" @click="$emit(status.muted ? 'unmute' : 'mute')">
|
||||
<i class="icon fa fa-volume-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<Slider :value="status.volume" :range="volumeRange" :disabled="status.volume == null"
|
||||
@mouseup="$emit('set-volume', $event.target.value)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Utils from "@/Utils"
|
||||
import MediaUtils from "@/components/Media/Utils";
|
||||
import Slider from "@/components/elements/Slider";
|
||||
|
||||
export default {
|
||||
name: "Controls",
|
||||
components: {Slider},
|
||||
mixins: [Utils, MediaUtils],
|
||||
emits: ['search', 'previous', 'next', 'play', 'pause', 'stop', 'seek', 'consume', 'random', 'repeat',
|
||||
'set-volume', 'mute', 'unmute'],
|
||||
|
||||
props: {
|
||||
track: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
status: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
// Enabled playback buttons
|
||||
buttons: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
previous: true,
|
||||
next: true,
|
||||
stop: true,
|
||||
consume: true,
|
||||
random: true,
|
||||
repeat: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Volume range
|
||||
volumeRange: {
|
||||
type: Array,
|
||||
default: () => [0, 100],
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
lastSync: 0,
|
||||
elapsed: this.status?.elapsed,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
duration() {
|
||||
return this.status?.duration != null ? this.status.duration : this.track?.duration
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getTime() {
|
||||
return (new Date()).getTime() / 1000
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const self = this
|
||||
|
||||
this.$watch(() => self.track, (track) => {
|
||||
if (!track || self.status?.state !== 'play')
|
||||
self.lastSync = this.getTime()
|
||||
})
|
||||
|
||||
this.$watch(() => self.status, () => {
|
||||
self.lastSync = this.getTime()
|
||||
})
|
||||
|
||||
setInterval(() => {
|
||||
if (self.status?.state === 'play')
|
||||
self.elapsed = (self.status?.elapsed || 0) + Math.round(this.getTime() - self.lastSync)
|
||||
}, 1000)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'vars.scss';
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
&:hover {
|
||||
border: 0;
|
||||
|
||||
.icon {
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
|
||||
&.enabled {
|
||||
color: $selected-fg;
|
||||
}
|
||||
}
|
||||
|
||||
.extension {
|
||||
box-shadow: $border-shadow-bottom;
|
||||
flex-direction: column;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
|
||||
@include until($desktop) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.volume-container,
|
||||
.list-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
padding: 0 .25em;
|
||||
}
|
||||
}
|
||||
|
||||
.list-controls {
|
||||
margin-top: -.5em;
|
||||
flex-flow: row-reverse;
|
||||
}
|
||||
|
||||
.time {
|
||||
&:first-child {
|
||||
margin-left: .25em;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
margin-left: 2.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 100%;
|
||||
height: $media-ctrl-panel-height;
|
||||
display: flex;
|
||||
padding: 1em .5em;
|
||||
overflow: hidden;
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.track-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-left: 0;
|
||||
|
||||
@include until($tablet) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: initial;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
|
||||
.artist, .title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.artist {
|
||||
opacity: 0.6;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: normal;
|
||||
font-size: 1em;
|
||||
letter-spacing: .05em;
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
.playback-controls {
|
||||
&.mobile {
|
||||
display: none;
|
||||
|
||||
@include until($tablet) {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.tablet {
|
||||
display: none;
|
||||
|
||||
@media screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
height: 50%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
margin: 0 .75em;
|
||||
|
||||
.play-pause {
|
||||
color: $play-btn-fg;
|
||||
font-size: 1.75em;
|
||||
|
||||
&:hover {
|
||||
color: $default-hover-fg-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-controls {
|
||||
height: 50%;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
flex-flow: row-reverse;
|
||||
}
|
||||
|
||||
.mobile.right-buttons {
|
||||
@include until ($desktop) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.volume-container {
|
||||
button {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.seek-slider {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
width: 75%;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: .7em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.elapsed-time {
|
||||
text-align: right;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
@include from($tablet) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tablet {
|
||||
@media screen and (max-width: $tablet), screen and (min-width: $desktop - 1) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop {
|
||||
@include until($desktop) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
28
platypush/backend/http/webapp/src/components/Media/Utils.vue
Normal file
28
platypush/backend/http/webapp/src/components/Media/Utils.vue
Normal file
|
@ -0,0 +1,28 @@
|
|||
<script>
|
||||
export default {
|
||||
name: "Utils",
|
||||
methods: {
|
||||
convertTime(time) {
|
||||
time = parseFloat(time); // Normalize strings
|
||||
const t = {}
|
||||
t.h = '' + parseInt(time/3600)
|
||||
t.m = '' + parseInt(time/60 - t.h*60)
|
||||
t.s = '' + parseInt(time - (t.h*3600 + t.m*60))
|
||||
|
||||
for (const attr of ['m','s']) {
|
||||
if (parseInt(t[attr]) < 10) {
|
||||
t[attr] = '0' + t[attr]
|
||||
}
|
||||
}
|
||||
|
||||
const ret = []
|
||||
if (parseInt(t.h)) {
|
||||
ret.push(t.h)
|
||||
}
|
||||
|
||||
ret.push(t.m, t.s)
|
||||
return ret.join(':')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
64
platypush/backend/http/webapp/src/components/Media/View.vue
Normal file
64
platypush/backend/http/webapp/src/components/Media/View.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="media-container">
|
||||
<div class="view-container">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="controls-container">
|
||||
<Controls :status="status" :track="track" @play="$emit('play', $event)" @pause="$emit('pause', $event)"
|
||||
@stop="$emit('stop')" @previous="$emit('previous')" @next="$emit('next')" @seek="$emit('seek', $event)"
|
||||
@set-volume="$emit('set-volume', $event)" @consume="$emit('consume', $event)"
|
||||
@repeat="$emit('repeat', $event)" @random="$emit('random', $event)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Controls from "@/components/Media/Controls";
|
||||
|
||||
export default {
|
||||
name: "View",
|
||||
components: {Controls},
|
||||
emits: ['play', 'pause', 'stop', 'next', 'previous', 'set-volume', 'seek', 'consume', 'random', 'repeat'],
|
||||
props: {
|
||||
pluginName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
status: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
track: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'vars.scss';
|
||||
|
||||
.media-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
.view-container {
|
||||
height: calc(100% - #{$media-ctrl-panel-height});
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.controls-container {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
border-top: $default-border-2;
|
||||
background: $default-bg-2;
|
||||
box-shadow: $border-shadow-top;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1 @@
|
|||
$media-ctrl-panel-height: 5.5em;
|
|
@ -5,16 +5,18 @@
|
|||
<span class="hostname" v-if="hostname" v-text="hostname" />
|
||||
</div>
|
||||
|
||||
<li v-for="name in Object.keys(panels)" :key="name" class="entry" :class="{selected: name === selectedPanel}"
|
||||
:title="name" @click="onItemClick(name)">
|
||||
<a :href="`/#${name}`">
|
||||
<ul>
|
||||
<li v-for="name in Object.keys(panels).sort()" :key="name" class="entry" :class="{selected: name === selectedPanel}"
|
||||
:title="name" @click="onItemClick(name)">
|
||||
<a :href="`/#${name}`">
|
||||
<span class="icon">
|
||||
<i :class="icons[name].class" v-if="icons[name]?.class" />
|
||||
<i class="fas fa-puzzle-piece" v-else />
|
||||
</span>
|
||||
<span class="name" v-if="!collapsed">{{ displayName(name) }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<span class="name" v-if="!collapsed">{{ displayName(name) }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
|
@ -95,7 +97,7 @@ nav {
|
|||
background: $nav-bg;
|
||||
color: $nav-fg;
|
||||
box-shadow: $nav-box-shadow-main;
|
||||
margin-right: 4px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
li {
|
||||
|
@ -153,11 +155,14 @@ nav {
|
|||
}
|
||||
|
||||
&.collapsed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
width: 2.5em;
|
||||
min-width: unset;
|
||||
max-width: unset;
|
||||
background: initial;
|
||||
background: $nav-collapsed-bg;
|
||||
color: $nav-collapsed-fg;
|
||||
box-shadow: $nav-box-shadow-collapsed;
|
||||
|
||||
|
@ -180,36 +185,40 @@ nav {
|
|||
|
||||
.toggler {
|
||||
text-align: center;
|
||||
@media screen and (min-width: $tablet) {
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
|
||||
&.selected,
|
||||
&:hover {
|
||||
border-radius: 1em;
|
||||
margin: 0 0.2em;
|
||||
}
|
||||
li {
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
|
||||
&.selected {
|
||||
background: $nav-entry-collapsed-selected-bg;
|
||||
}
|
||||
&.selected,
|
||||
&:hover {
|
||||
border-radius: 1em;
|
||||
margin: 0 0.2em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $nav-entry-collapsed-hover-bg;
|
||||
}
|
||||
&.selected {
|
||||
background: $nav-entry-collapsed-selected-bg;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
&:hover {
|
||||
background: $nav-entry-collapsed-hover-bg;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
display: none;
|
||||
.icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<div class="dropdown-container" ref="container">
|
||||
<button :title="title" ref="button" @click.stop="toggle">
|
||||
<i class="icon" :class="iconClass" v-if="iconClass" />
|
||||
<span class="text" v-text="text" v-if="text" />
|
||||
</button>
|
||||
|
||||
<div class="dropdown fade-in" :id="id" :class="{hidden: !visible}" ref="dropdown">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Dropdown",
|
||||
emits: ['click'],
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
iconClass: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
text: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
documentClickHndl(event) {
|
||||
if (!this.visible)
|
||||
return
|
||||
|
||||
let element = event.target
|
||||
while (element) {
|
||||
if (element === this.$refs.dropdown.element) {
|
||||
return
|
||||
}
|
||||
|
||||
element = element.parentElement
|
||||
}
|
||||
|
||||
this.close()
|
||||
},
|
||||
|
||||
close() {
|
||||
this.visible = false
|
||||
document.removeEventListener('click', this.documentClickHndl)
|
||||
},
|
||||
|
||||
open() {
|
||||
document.addEventListener('click', this.documentClickHndl)
|
||||
this.visible = true
|
||||
|
||||
setTimeout(() => {
|
||||
const element = this.$refs.dropdown
|
||||
element.style.left = 0
|
||||
element.style.top = parseFloat(getComputedStyle(this.$refs.button).height) + 'px'
|
||||
|
||||
const maxOffset = 45
|
||||
const maxLeft = window.innerWidth - maxOffset
|
||||
const left = this.$refs.container.offsetLeft + element.offsetLeft
|
||||
const width = element.clientWidth
|
||||
|
||||
if (left + width >= maxLeft) {
|
||||
element.style.left = -(parseFloat(getComputedStyle(this.$refs.button).width) + maxOffset) + 'px'
|
||||
}
|
||||
}, 10)
|
||||
},
|
||||
|
||||
toggle() {
|
||||
this.visible ? this.close() : this.open()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
background: $dropdown-bg;
|
||||
border-radius: .25em;
|
||||
border: $default-border-3;
|
||||
box-shadow: $dropdown-shadow;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="row item" @click="clicked">
|
||||
<div class="col-1 icon">
|
||||
<i :class="iconClass" v-if="iconClass" />
|
||||
</div>
|
||||
<div class="col-11 text" v-text="text" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DropdownItem",
|
||||
props: {
|
||||
iconClass: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
text: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
clicked(event) {
|
||||
this.$parent.$emit('click', event)
|
||||
this.$parent.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.item {
|
||||
display: flex;
|
||||
padding: .5em .25em;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: $dropdown-disabled-color;
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -129,7 +129,6 @@ export default {
|
|||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.3;
|
||||
@mixin no-thumb {
|
||||
display: none;
|
||||
width: 0;
|
||||
|
|
|
@ -60,8 +60,6 @@ export default {
|
|||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.3;
|
||||
|
||||
&::-webkit-progress-value,
|
||||
&::-moz-range-progress {
|
||||
background: none;
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div class="header">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Header"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'vars.scss';
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
height: $music-header-height;
|
||||
background: $menu-panel-bg;
|
||||
padding: .5em;
|
||||
box-shadow: $border-shadow-bottom;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<Loading v-if="loading" />
|
||||
|
||||
<MediaView :plugin-name="pluginName" :status="status" :track="track" @play="$emit('play', $event)"
|
||||
@pause="$emit('pause')" @stop="$emit('stop')" @previous="$emit('previous')" @next="$emit('next')"
|
||||
@set-volume="$emit('set-volume', $event)" @seek="$emit('seek', $event)" @consume="$emit('consume', $event)"
|
||||
@repeat="$emit('repeat', $event)" @random="$emit('random', $event)" v-else>
|
||||
<main>
|
||||
<div class="nav-container">
|
||||
<Nav :selected-view="selectedView" @input="selectedView = $event" />
|
||||
</div>
|
||||
|
||||
<div class="view-container">
|
||||
<Playlist :tracks="tracks" :status="status" :loading="loading" v-if="selectedView === 'playing'"
|
||||
@play="$emit('play', $event)" @clear="$emit('clear')" />
|
||||
</div>
|
||||
</main>
|
||||
</MediaView>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MediaView from "@/components/Media/View";
|
||||
import Nav from "@/components/panels/Music/Nav";
|
||||
import Playlist from "@/components/panels/Music/Playlist";
|
||||
import Utils from "@/Utils";
|
||||
|
||||
export default {
|
||||
name: "Music",
|
||||
emits: ['play', 'pause', 'stop', 'clear', 'previous', 'next', 'set-volume', 'seek', 'consume', 'repeat', 'random',
|
||||
'status-update', 'playlist-update', 'new-playing-track'],
|
||||
mixins: [Utils],
|
||||
components: {Nav, MediaView, Playlist},
|
||||
props: {
|
||||
pluginName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
tracks: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
status: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedView: 'playing',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
track() {
|
||||
if (this.status?.playingPos == null)
|
||||
return null
|
||||
|
||||
return this.tracks[this.status.playingPos]
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async onStatusEvent(event) {
|
||||
if (event.plugin_name !== this.pluginName)
|
||||
return
|
||||
|
||||
this.$emit('status-update', event)
|
||||
},
|
||||
|
||||
async onPlaylistEvent(event) {
|
||||
if (event.plugin_name !== this.pluginName)
|
||||
return
|
||||
|
||||
this.$emit('playlist-update', event)
|
||||
},
|
||||
|
||||
async onNewPlayingTrack(event) {
|
||||
if (event.plugin_name !== this.pluginName)
|
||||
return
|
||||
|
||||
this.notify({
|
||||
title: event.track?.artist,
|
||||
text: event.track?.title,
|
||||
iconClass: 'fa fa-play',
|
||||
})
|
||||
|
||||
this.$emit('new-playing-track', event)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.subscribe(this.onStatusEvent, 'on-status-update',
|
||||
'platypush.message.event.music.MusicPlayEvent',
|
||||
'platypush.message.event.music.MusicPauseEvent',
|
||||
'platypush.message.event.music.MusicStopEvent',
|
||||
'platypush.message.event.music.SeekChangeEvent',
|
||||
'platypush.message.event.music.VolumeChangeEvent',
|
||||
'platypush.message.event.music.MuteChangeEvent',
|
||||
'platypush.message.event.music.PlaybackRepeatModeChangeEvent',
|
||||
'platypush.message.event.music.PlaybackRandomModeChangeEvent',
|
||||
'platypush.message.event.music.PlaybackConsumeModeChangeEvent',
|
||||
'platypush.message.event.music.PlaybackSingleModeChangeEvent',
|
||||
)
|
||||
|
||||
this.subscribe(this.onPlaylistEvent, 'on-playlist-update',
|
||||
'platypush.message.event.music.PlaylistChangeEvent')
|
||||
|
||||
this.subscribe(this.onPlaylistEvent, 'on-new-playing-track',
|
||||
'platypush.message.event.music.NewPlayingTrackEvent')
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
this.unsubscribe('on-status-update')
|
||||
this.unsubscribe('on-playlist-update')
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
main {
|
||||
height: 100%;
|
||||
background: $background-color;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.view-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<nav>
|
||||
<li v-for="(view, name) in views" :key="name" :title="view.displayName"
|
||||
:class="{selected: name === selectedView}" @click="$emit('input', name)">
|
||||
<i :class="view.iconClass" />
|
||||
</li>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Nav",
|
||||
emits: ['input'],
|
||||
props: {
|
||||
selectedView: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
views: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
playing: {
|
||||
iconClass: 'fas fa-play',
|
||||
displayName: 'Now Playing',
|
||||
},
|
||||
|
||||
search: {
|
||||
iconClass: 'fas fa-search',
|
||||
displayName: 'Search',
|
||||
},
|
||||
|
||||
playlists: {
|
||||
iconClass: 'fas fa-list-ul',
|
||||
displayName: 'Playlists',
|
||||
},
|
||||
|
||||
library: {
|
||||
iconClass: 'fas fa-compact-disc',
|
||||
displayName: 'Library',
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'vars.scss';
|
||||
|
||||
nav {
|
||||
width: $music-nav-width;
|
||||
height: 100%;
|
||||
background: $nav-collapsed-bg;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 2.5px 0 4.5px 2px $nav-collapsed-fg;
|
||||
margin-left: 2.5px;
|
||||
overflow: hidden;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
padding: .6em;
|
||||
opacity: 0.7;
|
||||
|
||||
&.selected,
|
||||
&:hover {
|
||||
border-radius: 1.2em;
|
||||
margin: 0 0.2em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $nav-entry-collapsed-hover-bg;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: $nav-entry-collapsed-selected-bg;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,206 @@
|
|||
<template>
|
||||
<Loading v-if="loading" />
|
||||
|
||||
<div class="playlist fade-in" v-else>
|
||||
<div class="header-container">
|
||||
<MusicHeader>
|
||||
<div class="col-8 filter">
|
||||
<label>
|
||||
<input type="search" placeholder="Filter">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="col-4 buttons">
|
||||
<button title="Add item" @click="$refs.addToPlaylistModal.visible = true">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
|
||||
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h">
|
||||
<DropdownItem text="Save as playlist" icon-class="fa fa-save" :disabled="!tracks?.length"
|
||||
@click="$refs.savePlaylistModal.visible = true" />
|
||||
<DropdownItem text="Swap tracks" icon-class="fa fa-retweet" :disabled="tracks?.length !== 2 || !selectionMode" />
|
||||
<DropdownItem :text="selectionMode ? 'End selection' : 'Start selection'" icon-class="far fa-check-square"
|
||||
:disabled="!tracks?.length" @click="selectionMode = !selectionMode" />
|
||||
<DropdownItem :text="selectedTracks?.length === tracks?.length ? 'Unselect all' : 'Select all'"
|
||||
icon-class="fa fa-check-double" :disabled="!tracks?.length"
|
||||
@click="selectedTracks = [...Array(tracks.length).keys()]" />
|
||||
<DropdownItem text="Clear playlist" icon-class="fa fa-ban" :disabled="!tracks?.length" @click="$emit('clear')" />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</MusicHeader>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<div class="no-content" v-if="!tracks?.length">
|
||||
No tracks are loaded
|
||||
</div>
|
||||
|
||||
<div class="row track" v-for="(track, i) in tracks" :key="i" @dblclick="$emit('play', {pos: i})">
|
||||
<div class="col-10">
|
||||
<div class="title" v-text="track.title || '[No Title]'" />
|
||||
<div class="artist" v-text="track.artist || '[No Artist]'" />
|
||||
<div class="album" v-text="track.album" v-if="track.album" />
|
||||
</div>
|
||||
|
||||
<div class="col-2 right-side">
|
||||
<span class="duration" v-text="convertTime(track.time)" />
|
||||
|
||||
<span class="actions">
|
||||
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h">
|
||||
<DropdownItem text="Play" icon-class="fa fa-play" @click="$emit('play', {pos: i})" />
|
||||
<DropdownItem text="Add to playlist" icon-class="fa fa-list-ul" />
|
||||
</Dropdown>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal :visible="false" ref="addToPlaylistModal">
|
||||
</Modal>
|
||||
|
||||
<Modal :visible="false" ref="savePlaylistModal">
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "@/components/Modal";
|
||||
import MusicHeader from "@/components/panels/Music/Header";
|
||||
import MediaUtils from "@/components/Media/Utils";
|
||||
import Dropdown from "@/components/elements/Dropdown";
|
||||
import DropdownItem from "@/components/elements/DropdownItem";
|
||||
|
||||
export default {
|
||||
name: "Playlist",
|
||||
mixins: [MediaUtils],
|
||||
components: {DropdownItem, Dropdown, Modal, MusicHeader},
|
||||
emits: ['play', 'clear', 'add-to-playlist'],
|
||||
props: {
|
||||
tracks: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
status: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectionMode: false,
|
||||
selectedTracks: [],
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'vars.scss';
|
||||
|
||||
.playlist {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header-container {
|
||||
button {
|
||||
border: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.filter {
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
height: calc(100% - #{$music-header-height});
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.no-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.track {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: .75em .25em .25em .25em;
|
||||
box-shadow: 0 2.5px 2px -1px $default-shadow-color;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.artist, .album {
|
||||
display: inline-flex;
|
||||
opacity: 0.7;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.artist {
|
||||
margin-right: .25em;
|
||||
}
|
||||
|
||||
.album {
|
||||
@include until($tablet) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "\2022";
|
||||
margin-right: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
.right-side {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.duration,
|
||||
.actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.duration {
|
||||
font-size: .85em;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.actions {
|
||||
::v-deep button {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep button {
|
||||
background: none;
|
||||
padding: .5em .75em;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,2 @@
|
|||
$music-header-height: 3.3em;
|
||||
$music-nav-width: 2.8em;
|
|
@ -0,0 +1,167 @@
|
|||
<template>
|
||||
<Loading v-if="loading" />
|
||||
<MusicPlugin plugin-name="music.mpd" :loading="loading" :config="config" :tracks="tracks" :status="status"
|
||||
@play="play" @pause="pause" @stop="stop" @previous="previous" @next="next" @clear="clear"
|
||||
@set-volume="setVolume" @seek="seek" @consume="consume" @random="random" @repeat="repeat"
|
||||
@status-update="refreshStatus(true)" @playlist-update="refreshTracks(true)"
|
||||
@new-playing-track="refreshStatus(true)" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MusicPlugin from "@/components/panels/Music/Index";
|
||||
import Utils from "@/Utils";
|
||||
import Loading from "@/components/Loading";
|
||||
|
||||
export default {
|
||||
name: "MusicMpd",
|
||||
components: {Loading, MusicPlugin},
|
||||
mixins: [Utils],
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
tracks: [],
|
||||
status: {},
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async refreshTracks(background) {
|
||||
if (!background)
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
this.tracks = await this.request('music.mpd.playlistinfo')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async refreshStatus(background) {
|
||||
if (!background)
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
this.status = Object.entries(await this.request('music.mpd.status')).reduce((obj, [k, v]) => {
|
||||
switch (k) {
|
||||
case 'bitrate':
|
||||
case 'volume':
|
||||
obj[k] = parseInt(v)
|
||||
break
|
||||
|
||||
case 'consume':
|
||||
case 'random':
|
||||
case 'repeat':
|
||||
case 'single':
|
||||
obj[k] = !!parseInt(v)
|
||||
break
|
||||
|
||||
case 'song':
|
||||
obj['playingPos'] = parseInt(v)
|
||||
break
|
||||
|
||||
case 'time':
|
||||
[obj['elapsed'], obj['duration']] = v.split(':').map(t => parseInt(t))
|
||||
break
|
||||
|
||||
case 'elapsed':
|
||||
break
|
||||
|
||||
default:
|
||||
obj[k] = v
|
||||
break
|
||||
}
|
||||
|
||||
return obj
|
||||
}, {})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async refresh(background) {
|
||||
if (!background)
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
await Promise.all([this.refreshTracks(background), this.refreshStatus(background)])
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async play(event) {
|
||||
if (event?.pos != null) {
|
||||
await this.request('music.mpd.play_pos', {pos: event.pos})
|
||||
} else {
|
||||
await this.request('music.mpd.play')
|
||||
}
|
||||
|
||||
await this.refreshStatus(true)
|
||||
},
|
||||
|
||||
async pause() {
|
||||
await this.request('music.mpd.pause')
|
||||
await this.refreshStatus(true)
|
||||
},
|
||||
|
||||
async stop() {
|
||||
await this.request('music.mpd.stop')
|
||||
await this.refreshStatus(true)
|
||||
},
|
||||
|
||||
async previous() {
|
||||
await this.request('music.mpd.previous')
|
||||
await this.refreshStatus(true)
|
||||
},
|
||||
|
||||
async next() {
|
||||
await this.request('music.mpd.next')
|
||||
await this.refreshStatus(true)
|
||||
},
|
||||
|
||||
async clear() {
|
||||
await this.request('music.mpd.clear')
|
||||
await Promise.all([this.refreshStatus(true), this.refreshTracks(true)])
|
||||
},
|
||||
|
||||
async setVolume(volume) {
|
||||
if (volume === this.status.volume)
|
||||
return
|
||||
|
||||
await this.request('music.mpd.set_volume', {volume: volume})
|
||||
await this.refreshStatus(true)
|
||||
},
|
||||
|
||||
async seek(pos) {
|
||||
await this.request('music.mpd.seek', {position: pos})
|
||||
await this.refreshStatus(true)
|
||||
},
|
||||
|
||||
async repeat(value) {
|
||||
await this.request('music.mpd.repeat', {value: value})
|
||||
await this.refreshStatus(true)
|
||||
},
|
||||
|
||||
async random(value) {
|
||||
await this.request('music.mpd.random', {value: value})
|
||||
await this.refreshStatus(true)
|
||||
},
|
||||
|
||||
async consume(value) {
|
||||
await this.request('music.mpd.consume', {value: value})
|
||||
await this.refreshStatus(true)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.refresh()
|
||||
},
|
||||
}
|
||||
</script>
|
7
platypush/backend/http/webapp/src/style/common.scss
Normal file
7
platypush/backend/http/webapp/src/style/common.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
@import "themes/light.scss";
|
||||
|
||||
@import "mixins.scss";
|
||||
@import "layout.scss";
|
||||
@import "components.scss";
|
||||
@import "inputs.scss";
|
||||
@import "animations.scss";
|
33
platypush/backend/http/webapp/src/style/inputs.scss
Normal file
33
platypush/backend/http/webapp/src/style/inputs.scss
Normal file
|
@ -0,0 +1,33 @@
|
|||
.input-icon {
|
||||
position: absolute;
|
||||
min-width: .3em;
|
||||
padding: .1em;
|
||||
color: $input-icon-fg;
|
||||
}
|
||||
|
||||
input[type=text],
|
||||
input[type=password],
|
||||
input[type=search],
|
||||
input[type=number] {
|
||||
border: $default-border-2;
|
||||
border-radius: .5em;
|
||||
padding: .25em;
|
||||
|
||||
&:hover {
|
||||
border: $input-icon-hover-border;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: $input-icon-focus-border;
|
||||
}
|
||||
|
||||
&.with-icon {
|
||||
padding-left: .3em;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=search],
|
||||
input[type=text] {
|
||||
border-radius: 1em;
|
||||
padding: .25em .5em;
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
$widths: (
|
||||
s: '(max-width: 720px)',
|
||||
m: '(max-width: 1024px) and (min-width: 720px)',
|
||||
l: '(min-width: 1024px)',
|
||||
s: (max-width: $tablet),
|
||||
m: (min-width: $tablet),
|
||||
l: (min-width: $desktop),
|
||||
xl: (min-width: $widescreen),
|
||||
xxl: (min-width: $fullhd),
|
||||
);
|
||||
|
||||
@for $i from 1 through 12 {
|
||||
|
@ -102,3 +104,10 @@ $widths: (
|
|||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.no-content {
|
||||
display: flex;
|
||||
font-size: 1.5em;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
|
@ -39,7 +39,11 @@ $default-border-2: 1px solid $border-color-2 !default;
|
|||
$default-border-3: 1px solid $border-color-3 !default;
|
||||
|
||||
//// Shadows
|
||||
$border-shadow-bottom: 0 3px 2px -1px #c0c0c0;
|
||||
$default-shadow-color: #c0c0c0 !default;
|
||||
$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;
|
||||
|
||||
//// Modals
|
||||
$modal-header-bg: #e0e0e0 !default;
|
||||
|
@ -75,6 +79,7 @@ $nav-entry-collapsed-hover-bg: rgba(160, 245, 178, 0.60) !default;
|
|||
$nav-box-shadow-main: 1px 0 2px #002626;
|
||||
$nav-box-shadow-entry: 0 0 1px 1px #103824 !default;
|
||||
$nav-box-shadow-collapsed: 1px 0 2px 1px #bbb !default;
|
||||
$nav-collapsed-bg: white;
|
||||
$nav-collapsed-fg: #5e5e5e;
|
||||
|
||||
/// Panel/menu components
|
||||
|
@ -101,3 +106,16 @@ $slider-thumb-bg: rgba(0,215,80,1.0) !default;
|
|||
$slider-thumb-disabled-bg: rgba(0,215,80,0.3) !default;
|
||||
$slider-hover-on-hover-bg: #d2d2d2 !default;
|
||||
$slider-progress-bg: rgba(0,215,80,0.2) !default;
|
||||
|
||||
//// Input element
|
||||
$input-icon-fg: #888;
|
||||
$input-icon-focus-border: 1px solid rgba(127, 216, 95, 0.83);
|
||||
$input-icon-hover-border: 1px solid rgba(159, 180, 152, 0.83);
|
||||
|
||||
//// Media elements
|
||||
$play-btn-fg: #27ee5e !default;
|
||||
|
||||
//// Dropdown element
|
||||
$dropdown-bg: rgb(241, 243, 242) !default;
|
||||
$dropdown-disabled-color: #999 !default;
|
||||
$dropdown-shadow: 1px 1px 1px #bbb !default;
|
||||
|
|
8
platypush/backend/http/webapp/src/ui.js
Normal file
8
platypush/backend/http/webapp/src/ui.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
getMousePos() {
|
||||
return [
|
||||
window.event.clientX,
|
||||
window.event.clientY,
|
||||
]
|
||||
}
|
||||
}
|
|
@ -5,6 +5,14 @@ export default {
|
|||
isMobile() {
|
||||
return window.matchMedia("only screen and (max-width: 760px)").matches
|
||||
},
|
||||
|
||||
isTablet() {
|
||||
return !this.isMobile() && window.matchMedia("only screen and (max-width: 960px)").matches
|
||||
},
|
||||
|
||||
isDesktop() {
|
||||
return window.matchMedia("only screen and (min-width: 1152px)").matches
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -65,7 +65,6 @@ body {
|
|||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@select="selectedPanel = $event" v-else />
|
||||
|
||||
<div class="canvas">
|
||||
<div class="panel" v-for="(panel, name) in components" :key="name">
|
||||
<div class="panel" :class="{hidden: name !== selectedPanel}" v-for="(panel, name) in components" :key="name">
|
||||
<component :is="panel.component" :config="panel.config" :plugin-name="name" v-if="name === selectedPanel" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,11 +7,7 @@ module.exports = {
|
|||
additionalData: `
|
||||
@import '~bulma';
|
||||
@import '~w3css/w3.css';
|
||||
@import "@/style/mixins.scss";
|
||||
@import "@/style/themes/light.scss";
|
||||
@import "@/style/layout.scss";
|
||||
@import "@/style/components.scss";
|
||||
@import "@/style/animations.scss";
|
||||
@import "@/style/common.scss";
|
||||
`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,19 +131,19 @@ class MusicMopidyBackend(Backend):
|
|||
track = self._parse_track(track)
|
||||
if not track:
|
||||
return
|
||||
self.bus.post(MusicPauseEvent(status=status, track=track))
|
||||
self.bus.post(MusicPauseEvent(status=status, track=track, plugin_name='music.mpd'))
|
||||
elif event == 'track_playback_resumed':
|
||||
status['state'] = 'play'
|
||||
track = self._parse_track(track)
|
||||
if not track:
|
||||
return
|
||||
self.bus.post(MusicPlayEvent(status=status, track=track))
|
||||
self.bus.post(MusicPlayEvent(status=status, track=track, plugin_name='music.mpd'))
|
||||
elif event == 'track_playback_ended' or (
|
||||
event == 'playback_state_changed'
|
||||
and msg.get('new_state') == 'stopped'):
|
||||
status['state'] = 'stop'
|
||||
track = self._parse_track(track)
|
||||
self.bus.post(MusicStopEvent(status=status, track=track))
|
||||
self.bus.post(MusicStopEvent(status=status, track=track, plugin_name='music.mpd'))
|
||||
elif event == 'track_playback_started':
|
||||
track = self._parse_track(track)
|
||||
if not track:
|
||||
|
@ -152,7 +152,7 @@ class MusicMopidyBackend(Backend):
|
|||
status['state'] = 'play'
|
||||
status['position'] = 0.0
|
||||
status['time'] = track.get('time')
|
||||
self.bus.post(NewPlayingTrackEvent(status=status, track=track))
|
||||
self.bus.post(NewPlayingTrackEvent(status=status, track=track, plugin_name='music.mpd'))
|
||||
elif event == 'stream_title_changed':
|
||||
m = re.match('^\s*(.+?)\s+-\s+(.*)\s*$', msg.get('title', ''))
|
||||
if not m:
|
||||
|
@ -162,35 +162,35 @@ class MusicMopidyBackend(Backend):
|
|||
track['title'] = m.group(2)
|
||||
status['state'] = 'play'
|
||||
status['position'] = 0.0
|
||||
self.bus.post(NewPlayingTrackEvent(status=status, track=track))
|
||||
self.bus.post(NewPlayingTrackEvent(status=status, track=track, plugin_name='music.mpd'))
|
||||
elif event == 'volume_changed':
|
||||
status['volume'] = msg.get('volume')
|
||||
self.bus.post(VolumeChangeEvent(volume=status['volume'],
|
||||
status=status, track=track))
|
||||
self.bus.post(VolumeChangeEvent(volume=status['volume'], status=status, track=track,
|
||||
plugin_name='music.mpd'))
|
||||
elif event == 'mute_changed':
|
||||
status['mute'] = msg.get('mute')
|
||||
self.bus.post(MuteChangeEvent(mute=status['mute'],
|
||||
status=status, track=track))
|
||||
self.bus.post(MuteChangeEvent(mute=status['mute'], status=status, track=track,
|
||||
plugin_name='music.mpd'))
|
||||
elif event == 'seeked':
|
||||
status['position'] = msg.get('time_position')/1000
|
||||
self.bus.post(SeekChangeEvent(position=status['position'],
|
||||
status=status, track=track))
|
||||
self.bus.post(SeekChangeEvent(position=status['position'], status=status, track=track,
|
||||
plugin_name='music.mpd'))
|
||||
elif event == 'tracklist_changed':
|
||||
tracklist = [self._parse_track(t, pos=i)
|
||||
for i, t in enumerate(self._communicate({
|
||||
'method': 'core.tracklist.get_tl_tracks'}))]
|
||||
|
||||
self.bus.post(PlaylistChangeEvent(changes=tracklist))
|
||||
self.bus.post(PlaylistChangeEvent(changes=tracklist, plugin_name='music.mpd'))
|
||||
elif event == 'options_changed':
|
||||
new_status = self._get_tracklist_status()
|
||||
if new_status['random'] != self._latest_status.get('random'):
|
||||
self.bus.post(PlaybackRandomModeChangeEvent(state=new_status['random']))
|
||||
self.bus.post(PlaybackRandomModeChangeEvent(state=new_status['random'], plugin_name='music.mpd'))
|
||||
if new_status['repeat'] != self._latest_status['repeat']:
|
||||
self.bus.post(PlaybackRepeatModeChangeEvent(state=new_status['repeat']))
|
||||
self.bus.post(PlaybackRepeatModeChangeEvent(state=new_status['repeat'], plugin_name='music.mpd'))
|
||||
if new_status['single'] != self._latest_status['single']:
|
||||
self.bus.post(PlaybackSingleModeChangeEvent(state=new_status['single']))
|
||||
self.bus.post(PlaybackSingleModeChangeEvent(state=new_status['single'], plugin_name='music.mpd'))
|
||||
if new_status['consume'] != self._latest_status['consume']:
|
||||
self.bus.post(PlaybackConsumeModeChangeEvent(state=new_status['consume']))
|
||||
self.bus.post(PlaybackConsumeModeChangeEvent(state=new_status['consume'], plugin_name='music.mpd'))
|
||||
|
||||
self._latest_status = new_status
|
||||
|
||||
|
|
|
@ -81,11 +81,11 @@ class MusicMpdBackend(Backend):
|
|||
|
||||
if state != last_state:
|
||||
if state == 'stop':
|
||||
self.bus.post(MusicStopEvent(status=status, track=track))
|
||||
self.bus.post(MusicStopEvent(status=status, track=track, plugin_name='music.mpd'))
|
||||
elif state == 'pause':
|
||||
self.bus.post(MusicPauseEvent(status=status, track=track))
|
||||
self.bus.post(MusicPauseEvent(status=status, track=track, plugin_name='music.mpd'))
|
||||
elif state == 'play':
|
||||
self.bus.post(MusicPlayEvent(status=status, track=track))
|
||||
self.bus.post(MusicPlayEvent(status=status, track=track, plugin_name='music.mpd'))
|
||||
|
||||
if playlist != last_playlist:
|
||||
if last_playlist:
|
||||
|
@ -93,31 +93,31 @@ class MusicMpdBackend(Backend):
|
|||
# PlaylistChangeEvent temporarily disabled
|
||||
# changes = plugin.plchanges(last_playlist).output
|
||||
# self.bus.post(PlaylistChangeEvent(changes=changes))
|
||||
self.bus.post(PlaylistChangeEvent())
|
||||
self.bus.post(PlaylistChangeEvent(plugin_name='music.mpd'))
|
||||
last_playlist = playlist
|
||||
|
||||
if state == 'play' and track != last_track:
|
||||
self.bus.post(NewPlayingTrackEvent(status=status, track=track))
|
||||
self.bus.post(NewPlayingTrackEvent(status=status, track=track, plugin_name='music.mpd'))
|
||||
|
||||
if last_status.get('volume', None) != status['volume']:
|
||||
self.bus.post(VolumeChangeEvent(
|
||||
volume=int(status['volume']), status=status, track=track))
|
||||
self.bus.post(VolumeChangeEvent(volume=int(status['volume']), status=status, track=track,
|
||||
plugin_name='music.mpd'))
|
||||
|
||||
if last_status.get('random', None) != status['random']:
|
||||
self.bus.post(PlaybackRandomModeChangeEvent(
|
||||
state=bool(int(status['random'])), status=status, track=track))
|
||||
self.bus.post(PlaybackRandomModeChangeEvent(state=bool(int(status['random'])), status=status,
|
||||
track=track, plugin_name='music.mpd'))
|
||||
|
||||
if last_status.get('repeat', None) != status['repeat']:
|
||||
self.bus.post(PlaybackRepeatModeChangeEvent(
|
||||
state=bool(int(status['repeat'])), status=status, track=track))
|
||||
self.bus.post(PlaybackRepeatModeChangeEvent(state=bool(int(status['repeat'])), status=status,
|
||||
track=track, plugin_name='music.mpd'))
|
||||
|
||||
if last_status.get('consume', None) != status['consume']:
|
||||
self.bus.post(PlaybackConsumeModeChangeEvent(
|
||||
state=bool(int(status['consume'])), status=status, track=track))
|
||||
self.bus.post(PlaybackConsumeModeChangeEvent(state=bool(int(status['consume'])), status=status,
|
||||
track=track, plugin_name='music.mpd'))
|
||||
|
||||
if last_status.get('single', None) != status['single']:
|
||||
self.bus.post(PlaybackSingleModeChangeEvent(
|
||||
state=bool(int(status['single'])), status=status, track=track))
|
||||
self.bus.post(PlaybackSingleModeChangeEvent(state=bool(int(status['single'])), status=status,
|
||||
track=track, plugin_name='music.mpd'))
|
||||
|
||||
last_status = status
|
||||
last_state = state
|
||||
|
|
|
@ -4,8 +4,8 @@ from platypush.message.event import Event
|
|||
class MusicEvent(Event):
|
||||
""" Base class for music events """
|
||||
|
||||
def __init__(self, status, track, *args, **kwargs):
|
||||
super().__init__(*args, status=status, track=track, **kwargs)
|
||||
def __init__(self, status, track, plugin_name=None, *args, **kwargs):
|
||||
super().__init__(*args, status=status, track=track, plugin_name=plugin_name, **kwargs)
|
||||
|
||||
|
||||
class MusicPlayEvent(MusicEvent):
|
||||
|
|
|
@ -3,7 +3,7 @@ from platypush.plugins import Plugin, action
|
|||
|
||||
class MusicPlugin(Plugin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@action
|
||||
def play(self):
|
||||
|
@ -26,7 +26,11 @@ class MusicPlugin(Plugin):
|
|||
raise NotImplementedError()
|
||||
|
||||
@action
|
||||
def setvol(self, vol):
|
||||
def set_volume(self, volume):
|
||||
raise NotImplementedError()
|
||||
|
||||
@action
|
||||
def seek(self, position):
|
||||
raise NotImplementedError()
|
||||
|
||||
@action
|
||||
|
|
|
@ -181,12 +181,22 @@ class MusicMpdPlugin(MusicPlugin):
|
|||
@action
|
||||
def setvol(self, vol):
|
||||
"""
|
||||
Set the volume
|
||||
Set the volume (DEPRECATED, use :meth:`.set_volume` instead).
|
||||
|
||||
:param vol: Volume value (range: 0-100)
|
||||
:type vol: int
|
||||
"""
|
||||
return self._exec('setvol', str(vol))
|
||||
return self.set_volume(vol)
|
||||
|
||||
@action
|
||||
def set_volume(self, volume):
|
||||
"""
|
||||
Set the volume.
|
||||
|
||||
:param volume: Volume value (range: 0-100)
|
||||
:type volume: int
|
||||
"""
|
||||
return self._exec('setvol', str(volume))
|
||||
|
||||
@action
|
||||
def volup(self, delta=10):
|
||||
|
@ -407,13 +417,24 @@ class MusicMpdPlugin(MusicPlugin):
|
|||
@action
|
||||
def seekcur(self, value):
|
||||
"""
|
||||
Seek to the specified position
|
||||
Seek to the specified position (DEPRECATED, use :meth:`.seek` instead).
|
||||
|
||||
:param value: Seek position in seconds, or delta string (e.g. '+15' or '-15') to indicate a seek relative to the current position
|
||||
:type value: int
|
||||
"""
|
||||
|
||||
return self._exec('seekcur', value)
|
||||
return self.seek(value)
|
||||
|
||||
@action
|
||||
def seek(self, position):
|
||||
"""
|
||||
Seek to the specified position
|
||||
|
||||
:param position: Seek position in seconds, or delta string (e.g. '+15' or '-15') to indicate a seek relative to the current position
|
||||
:type position: int
|
||||
"""
|
||||
|
||||
return self._exec('seekcur', position)
|
||||
|
||||
@action
|
||||
def forward(self):
|
||||
|
|
Loading…
Reference in a new issue