Migrating light.hue panel WIP
This commit is contained in:
parent
0cd120f492
commit
fc718c907a
36 changed files with 810 additions and 81 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.c68a1871.css" rel="prefetch"><link href="/static/css/chunk-2dcde994.79277e60.css" rel="prefetch"><link href="/static/css/chunk-4bbbb9a3.c0cffcb7.css" rel="prefetch"><link href="/static/css/chunk-5710a9bc.b05a2ff9.css" rel="prefetch"><link href="/static/css/chunk-62a3d08e.698b2d60.css" rel="prefetch"><link href="/static/css/chunk-7c2209ed.a322204e.css" rel="prefetch"><link href="/static/css/chunk-e8078048.67cca65c.css" rel="prefetch"><link href="/static/js/chunk-24ff873d.0f916e0f.js" rel="prefetch"><link href="/static/js/chunk-2dcde994.c90cdf08.js" rel="prefetch"><link href="/static/js/chunk-4bbbb9a3.c1ba820e.js" rel="prefetch"><link href="/static/js/chunk-5710a9bc.5aba1b9a.js" rel="prefetch"><link href="/static/js/chunk-62a3d08e.cd0ca5eb.js" rel="prefetch"><link href="/static/js/chunk-7c2209ed.3981671f.js" rel="prefetch"><link href="/static/js/chunk-e8078048.bc52467d.js" rel="prefetch"><link href="/static/css/app.54d8460c.css" rel="preload" as="style"><link href="/static/js/app.aaafc657.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.d4962a4a.js" rel="preload" as="script"><link href="/static/css/app.54d8460c.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.d4962a4a.js"></script><script src="/static/js/app.aaafc657.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-0a24466a.09555bc4.css" rel="prefetch"><link href="/static/css/chunk-16a3f845.79277e60.css" rel="prefetch"><link href="/static/css/chunk-24ff873d.c68a1871.css" rel="prefetch"><link href="/static/css/chunk-4bbbb9a3.c0cffcb7.css" rel="prefetch"><link href="/static/css/chunk-5710a9bc.b05a2ff9.css" rel="prefetch"><link href="/static/css/chunk-62a3d08e.698b2d60.css" rel="prefetch"><link href="/static/css/chunk-ac6aae98.a322204e.css" rel="prefetch"><link href="/static/css/chunk-e8078048.67cca65c.css" rel="prefetch"><link href="/static/js/chunk-0a24466a.ebe2c04f.js" rel="prefetch"><link href="/static/js/chunk-16a3f845.3bdbbdb5.js" rel="prefetch"><link href="/static/js/chunk-24ff873d.0f916e0f.js" rel="prefetch"><link href="/static/js/chunk-2d2091df.377ea7b0.js" rel="prefetch"><link href="/static/js/chunk-4bbbb9a3.c1ba820e.js" rel="prefetch"><link href="/static/js/chunk-5710a9bc.5aba1b9a.js" rel="prefetch"><link href="/static/js/chunk-62a3d08e.cd0ca5eb.js" rel="prefetch"><link href="/static/js/chunk-ac6aae98.dda16597.js" rel="prefetch"><link href="/static/js/chunk-e8078048.bc52467d.js" rel="prefetch"><link href="/static/css/app.ac911816.css" rel="preload" as="style"><link href="/static/js/app.b4cc8001.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.e8b1896c.js" rel="preload" as="script"><link href="/static/css/app.ac911816.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.e8b1896c.js"></script><script src="/static/js/app.b4cc8001.js"></script></body></html>
|
File diff suppressed because one or more lines are too long
11
platypush/backend/http/dist/static/css/chunk-0a24466a.09555bc4.css
vendored
Normal file
11
platypush/backend/http/dist/static/css/chunk-0a24466a.09555bc4.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
2
platypush/backend/http/dist/static/js/app.b4cc8001.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/app.b4cc8001.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/app.b4cc8001.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/app.b4cc8001.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-0a24466a.ebe2c04f.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-0a24466a.ebe2c04f.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-0a24466a.ebe2c04f.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-0a24466a.ebe2c04f.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-16a3f845.3bdbbdb5.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-16a3f845.3bdbbdb5.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-16a3f845.3bdbbdb5.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-16a3f845.3bdbbdb5.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-2d2091df.377ea7b0.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-2d2091df.377ea7b0.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d2091df"],{a84f:function(e,t,n){"use strict";n.r(t);var r=n("7a23");function i(e,t,n,i,u,s){var a=Object(r["z"])("Loading"),o=Object(r["z"])("LightPlugin");return Object(r["r"])(),Object(r["e"])(r["a"],null,[u.loading?(Object(r["r"])(),Object(r["e"])(a,{key:0})):Object(r["f"])("",!0),Object(r["h"])(o,{"plugin-name":"light.hue",config:n.config,lights:u.lights,groups:u.groups,scenes:u.scenes,animations:u.animations,"initial-group":s.initialGroup,"loading-groups":u.loadingGroups,onGroupToggle:s.toggleGroup},null,8,["config","lights","groups","scenes","animations","initial-group","loading-groups","onGroupToggle"])],64)}n("4de4"),n("13d5"),n("b0c0"),n("4fad"),n("b64b"),n("d3b7"),n("3ca3"),n("ddb0");var u=n("b85c"),s=(n("96cf"),n("1da1")),a=n("5530"),o=n("3835"),c=n("cf99"),g=n("3e54"),p=n("3a5e"),l={name:"LightHue",components:{Loading:p["a"],LightPlugin:c["default"]},mixins:[g["a"]],props:{config:{type:Object,default:function(){}}},data:function(){return{lights:{},groups:{},scenes:{},animations:{},loading:!1,loadingLights:{},loadingGroups:{}}},computed:{groupsByName:function(){return this.groups?Object.entries(this.groups).reduce((function(e,t){var n=Object(o["a"])(t,2),r=n[0],i=n[1];return e[i.name||r]=Object(a["a"])(Object(a["a"])({},i),{},{id:r}),e}),{}):{}},initialGroup:function(){if(!this.config.groups||!Object.keys(this.config.groups).length)return null;var e=this.config.groups[0];return e in this.groups?this.groups[e].id:e in this.groupsByName?this.groupsByName[e].id:null}},methods:{getLights:function(){var e=this;return Object(s["a"])(regeneratorRuntime.mark((function t(){return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:return t.next=2,e.request("light.hue.get_lights");case 2:return t.abrupt("return",t.sent);case 3:case"end":return t.stop()}}),t)})))()},getGroups:function(){var e=this;return Object(s["a"])(regeneratorRuntime.mark((function t(){return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:return t.t0=Object,t.next=3,e.request("light.hue.get_groups");case 3:return t.t1=t.sent,t.abrupt("return",t.t0.entries.call(t.t0,t.t1).filter((function(e){return!e[1].recycle&&"room"===e[1].type.toLowerCase()})).reduce((function(e,t){var n=Object(o["a"])(t,2),r=n[0],i=n[1];return e[r]=i,e}),{}));case 5:case"end":return t.stop()}}),t)})))()},getScenes:function(){var e=this;return Object(s["a"])(regeneratorRuntime.mark((function t(){return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:return t.next=2,e.request("light.hue.get_scenes");case 2:return t.abrupt("return",t.sent);case 3:case"end":return t.stop()}}),t)})))()},toggleGroup:function(e){var t=this;return Object(s["a"])(regeneratorRuntime.mark((function n(){var r;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return r=[],null!=e&&r.push(e.name),t.setGroupsLoading(e),n.prev=3,n.next=6,t.request("light.hue.toggle",{groups:r});case 6:return n.next=8,t.refresh();case 8:return n.prev=8,t.unsetGroupsLoading(e),n.finish(8);case 11:case"end":return n.stop()}}),n,null,[[3,,8,11]])})))()},refresh:function(){var e=this;return Object(s["a"])(regeneratorRuntime.mark((function t(){var n,r;return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:return e.loading=!0,t.prev=1,t.next=4,Promise.all([e.getLights(),e.getGroups(),e.getScenes()]);case 4:n=t.sent,r=Object(o["a"])(n,3),e.lights=r[0],e.groups=r[1],e.scenes=r[2];case 9:return t.prev=9,e.loading=!1,t.finish(9);case 12:case"end":return t.stop()}}),t,null,[[1,,9,12]])})))()},setGroupsLoading:function(){for(var e={},t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];if(n.length&&n[0]){var i,s=Object(u["a"])(n);try{for(s.s();!(i=s.n()).done;){var o=i.value;e[o.id]=!0}}catch(c){s.e(c)}finally{s.f()}}else e=Object.keys(this.groups);this.loadingGroups=Object(a["a"])(Object(a["a"])({},this.loadingGroups),e)},unsetGroupsLoading:function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];for(var r=0,i=t;r<i.length;r++){var u=i[r];u.id in this.loadingGroups&&delete this.loadingGroups[u.id]}}},mounted:function(){this.refresh()}};l.render=i;t["default"]=l}}]);
|
||||
//# sourceMappingURL=chunk-2d2091df.377ea7b0.js.map
|
1
platypush/backend/http/dist/static/js/chunk-2d2091df.377ea7b0.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-2d2091df.377ea7b0.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
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-ac6aae98.dda16597.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-ac6aae98.dda16597.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-ac6aae98.dda16597.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-ac6aae98.dda16597.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
7
platypush/backend/http/dist/static/js/chunk-vendors.e8b1896c.js
vendored
Normal file
7
platypush/backend/http/dist/static/js/chunk-vendors.e8b1896c.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-vendors.e8b1896c.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-vendors.e8b1896c.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
87
platypush/backend/http/webapp/src/components/Light/Group.vue
Normal file
87
platypush/backend/http/webapp/src/components/Light/Group.vue
Normal file
|
@ -0,0 +1,87 @@
|
|||
<template>
|
||||
<div class="light-group-container">
|
||||
<MenuPanel>
|
||||
<li class="header">
|
||||
<button class="back-btn" title="Back" @click="close" v-if="group">
|
||||
<i class="fas fa-chevron-left" />
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<div class="no-lights" v-if="!lights || !Object.keys(lights).length">
|
||||
No lights found
|
||||
</div>
|
||||
|
||||
<li v-for="(light, id) in lightsSorted" :key="id" v-else>
|
||||
<Light :light="light" />
|
||||
</li>
|
||||
</MenuPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Light from "@/components/Light/Light";
|
||||
import MenuPanel from "@/components/MenuPanel";
|
||||
|
||||
export default {
|
||||
name: "Group",
|
||||
emits: ['close'],
|
||||
components: {MenuPanel, Light},
|
||||
props: {
|
||||
lights: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
group: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
lightsSorted() {
|
||||
if (!this.lights)
|
||||
return []
|
||||
|
||||
return Object.entries(this.lights)
|
||||
.sort((a, b) => a[1].name.localeCompare(b[1].name))
|
||||
.map(([id, light]) => {
|
||||
return {
|
||||
...light,
|
||||
id: id,
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
close(event) {
|
||||
event.stopPropagation()
|
||||
this.$emit('close')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.light-group-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.header {
|
||||
.back-btn {
|
||||
border: 0;
|
||||
|
||||
&:hover {
|
||||
border: 0;
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li.header {
|
||||
.back-btn {
|
||||
background: none;
|
||||
margin-left: -0.75em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<MenuPanel>
|
||||
<li class="header">
|
||||
<i class="icon fas fa-home" />
|
||||
<span class="name">Rooms</span>
|
||||
</li>
|
||||
<li class="row group" v-for="group in groupsSorted" :key="group.id" @click="$emit('select', group.id)">
|
||||
<span class="name col-9">
|
||||
{{ group.name || `[Group #${group.id}]` }}
|
||||
</span>
|
||||
<span class="controls col-3 pull-right">
|
||||
<ToggleSwitch :value="group.state.any_on" :disabled="group.id in (loadingGroups || {})" @input="toggleGroup(group)" />
|
||||
</span>
|
||||
</li>
|
||||
</MenuPanel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuPanel from "@/components/MenuPanel";
|
||||
import ToggleSwitch from "@/components/elements/ToggleSwitch";
|
||||
|
||||
export default {
|
||||
name: "Groups",
|
||||
components: {ToggleSwitch, MenuPanel},
|
||||
emits: ['select', 'toggle'],
|
||||
props: {
|
||||
groups: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
loadingGroups: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
groupsSorted() {
|
||||
return Object.entries(this.groups)
|
||||
.sort((a, b) => a[1].name.localeCompare(b[1].name))
|
||||
.map(([id, group]) => {
|
||||
return {
|
||||
...group,
|
||||
id: id,
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleGroup(group) {
|
||||
this.$emit('toggle', group)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
.icon {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
21
platypush/backend/http/webapp/src/components/Light/Light.vue
Normal file
21
platypush/backend/http/webapp/src/components/Light/Light.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<div class="light">
|
||||
{{ light.name || light.id }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Light",
|
||||
props: {
|
||||
light: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
122
platypush/backend/http/webapp/src/components/MenuPanel.vue
Normal file
122
platypush/backend/http/webapp/src/components/MenuPanel.vue
Normal file
|
@ -0,0 +1,122 @@
|
|||
<template>
|
||||
<div class="menu-panel">
|
||||
<ul :style="style">
|
||||
<slot />
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MenuPanel",
|
||||
props: {
|
||||
style: {
|
||||
type: [String, Object, Array],
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.menu-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: $menu-panel-bg;
|
||||
padding-top: 2em;
|
||||
|
||||
ul {
|
||||
background: $menu-panel-content-bg;
|
||||
border-radius: 15px;
|
||||
box-shadow: $plugin-panel-shadow;
|
||||
border: 0;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
padding: 0.5em;
|
||||
box-shadow: $plugin-panel-entry-shadow;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
letter-spacing: 0.05em;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: 15px 15px 0 0;
|
||||
box-shadow: $plugin-panel-first-entry-shadow;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 15px 15px;
|
||||
box-shadow: $plugin-panel-last-entry-shadow;
|
||||
}
|
||||
|
||||
&.header {
|
||||
background: $menu-header-bg;
|
||||
font-weight: bold;
|
||||
box-shadow: $menu-header-shadow;
|
||||
|
||||
&:hover {
|
||||
background: $menu-header-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
.menu-panel {
|
||||
padding-top: 0;
|
||||
ul {
|
||||
min-width: 100%;
|
||||
border-radius: 0;
|
||||
|
||||
li {
|
||||
&:first-child {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
.menu-panel {
|
||||
ul {
|
||||
min-width: 65%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: $desktop) {
|
||||
.menu-panel {
|
||||
ul {
|
||||
min-width: 40%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: $widescreen) {
|
||||
.menu-panel {
|
||||
ul {
|
||||
min-width: 30%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: $fullhd) {
|
||||
.menu-panel {
|
||||
ul {
|
||||
min-width: 25%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -12,7 +12,7 @@
|
|||
<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">{{ name }}</span>
|
||||
<span class="name" v-if="!collapsed">{{ displayName(name) }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</nav>
|
||||
|
@ -39,6 +39,12 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
displayName(name) {
|
||||
return name.split('.').map((token) => token[0].toUpperCase() + token.slice(1)).join(' ')
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
collapsed: false,
|
||||
|
@ -46,9 +52,6 @@ export default {
|
|||
host: null,
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -65,18 +68,19 @@ nav {
|
|||
box-shadow: $nav-box-shadow-main;
|
||||
margin-right: 4px;
|
||||
|
||||
li {
|
||||
box-shadow: $nav-box-shadow-entry;
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
color: $nav-fg;
|
||||
padding: 1em 0.25em;
|
||||
&:hover {
|
||||
color: $nav-fg;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 1em 0.25em;
|
||||
box-shadow: $nav-box-shadow-entry;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $nav-entry-hover-bg;
|
||||
}
|
||||
|
@ -117,6 +121,7 @@ nav {
|
|||
|
||||
a {
|
||||
color: $nav-collapsed-fg;
|
||||
padding: 0.25em 0;
|
||||
&:hover {
|
||||
color: $nav-collapsed-fg;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
<template>
|
||||
<div class="power-switch" :class="{disabled: disabled}" @click="onInput">
|
||||
<!--suppress HtmlFormInputWithoutLabel -->
|
||||
<input type="checkbox" :checked="value">
|
||||
<label>
|
||||
<!--suppress HtmlUnknownTag -->
|
||||
<div class="switch">
|
||||
<div class="dot" />
|
||||
</div>
|
||||
<span class="label">
|
||||
<slot />
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ToggleSwitch",
|
||||
emits: ['input'],
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onInput(event) {
|
||||
event.stopPropagation()
|
||||
if (this.disabled)
|
||||
return false
|
||||
|
||||
this.$emit('input', event)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.power-switch {
|
||||
position: relative;
|
||||
transition: transform .3s;
|
||||
transform: scale(var(--scale, 1)) translateZ(0);
|
||||
|
||||
&:active {
|
||||
--scale: .96;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
& + label {
|
||||
border-radius: 1em;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: box-shadow .4s;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: inherit;
|
||||
background: none;
|
||||
opacity: var(--gradient, 0);
|
||||
transition: opacity .4s;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
height: 1.4em;
|
||||
width: 2.5em;
|
||||
border-radius: 1em;
|
||||
background: $toggle-bg;
|
||||
box-shadow: $toggle-shadow;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: inherit;
|
||||
background: $toggle-selected-bg;
|
||||
opacity: var(--gradient, 0);
|
||||
transition: opacity .4s;
|
||||
}
|
||||
|
||||
.dot {
|
||||
background: $toggle-dot-bg;
|
||||
position: absolute;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
border-radius: 50%;
|
||||
box-shadow: $toggle-dot-shadow;
|
||||
left: -0.25em;
|
||||
top: -1px;
|
||||
transform: translateX(var(--offset, 0));
|
||||
transition: transform .4s, box-shadow .4s;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: inherit;
|
||||
background: $toggle-selected-dot-bg;
|
||||
box-shadow: $toggle-dot-shadow;
|
||||
opacity: var(--gradient, 0);
|
||||
transition: opacity .4s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
line-height: 2em;
|
||||
font-size: 1.2em;
|
||||
color: var(--text, #646B8C);
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
margin-left: 0.5em;
|
||||
transition: color .4s;
|
||||
}
|
||||
|
||||
& + span {
|
||||
text-align: center;
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
opacity: 0;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
color: #A6ACCD;
|
||||
transform: translateY(4px);
|
||||
transition: opacity .4s, transform .4s;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:checked) {
|
||||
& + label {
|
||||
pointer-events: none;
|
||||
& + span {
|
||||
opacity: 1;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
& + label {
|
||||
--offset: 1.5em;
|
||||
--text: #406046;
|
||||
--gradient: 1;
|
||||
--shadow: rgba(0, 39, 6, .1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,78 +1,137 @@
|
|||
<template>
|
||||
<div class="light-plugin">
|
||||
I'm in the content!
|
||||
{{ pluginName }}
|
||||
<div class="plugin lights-plugin">
|
||||
<div class="panel" v-if="selectedGroup == null && groups && Object.keys(groups).length">
|
||||
<Groups :groups="groups" :loading-groups="loadingGroups" @select="selectedGroup = $event" @toggle="toggleGroup" />
|
||||
</div>
|
||||
<div class="panel" v-else>
|
||||
<Group :group="groups[selectedGroup]" :lights="displayedLights" @close="closeGroup" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Utils from "@/Utils";
|
||||
import Panel from "@/components/panels/Panel";
|
||||
import Groups from "@/components/Light/Groups";
|
||||
import Group from "@/components/Light/Group";
|
||||
|
||||
/**
|
||||
* Generic component for light plugins panels.
|
||||
*/
|
||||
export default {
|
||||
name: "Light",
|
||||
components: {Group, Groups},
|
||||
mixins: [Utils, Panel],
|
||||
emits: ['group-toggle'],
|
||||
|
||||
props: {
|
||||
// Set to false if the light plugin doesn't support groups.
|
||||
hasGroups: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
lights: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
// Set to false if the light plugin doesn't support scenes.
|
||||
hasScenes: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
groups: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
// Set to false if the light plugin doesn't support animations.
|
||||
hasAnimations: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
scenes: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
animations: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
loadingLights: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
loadingGroups: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
pluginName: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
initialGroup: {
|
||||
type: [Number, String],
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
lights: {},
|
||||
groups: {},
|
||||
scenes: {},
|
||||
selectedGroup: null,
|
||||
initialized: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
displayedLights() {
|
||||
const selectedGroup = this.selectedGroup || this.initialGroup
|
||||
if (selectedGroup == null)
|
||||
return this.lights
|
||||
|
||||
return this.groups[selectedGroup].lights.reduce((lights, lightId) => {
|
||||
lights[lightId] = this.lights[lightId]
|
||||
return lights
|
||||
}, {})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getLights() {
|
||||
throw "getLights should be implemented by a derived component"
|
||||
initSelectedGroup() {
|
||||
const self = this
|
||||
const unwatch = this.$watch(() => self.initialGroup, (newVal) => {
|
||||
if (!self.initialized) {
|
||||
self.initialized = true
|
||||
unwatch()
|
||||
if (self.selectedGroup == null && newVal != null) {
|
||||
self.selectedGroup = self.initialGroup
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async getGroups() {
|
||||
if (!this.hasGroups)
|
||||
return {}
|
||||
|
||||
throw "getGroups should be implemented by a derived component"
|
||||
closeGroup() {
|
||||
this.selectedGroup = null
|
||||
},
|
||||
|
||||
async getScenes() {
|
||||
if (!this.hasScenes)
|
||||
return {}
|
||||
|
||||
throw "getScenes should be implemented by a derived component"
|
||||
toggleGroup(group) {
|
||||
this.$emit('group-toggle', group)
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
[this.lights, this.groups, this.scenes] = await Promise.all([
|
||||
this.getLights(),
|
||||
this.getGroups(),
|
||||
this.getScenes(),
|
||||
])
|
||||
mounted() {
|
||||
this.initSelectedGroup()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.plugin {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: none;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.lights-plugin {
|
||||
.menu-panel {
|
||||
ul {
|
||||
li:not(.header) {
|
||||
padding: 1.5em 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,13 +1,136 @@
|
|||
<template>
|
||||
<Light plugin-name="light.hue" />
|
||||
<Loading v-if="loading" />
|
||||
<LightPlugin plugin-name="light.hue" :config="config" :lights="lights" :groups="groups" :scenes="scenes"
|
||||
:animations="animations" :initial-group="initialGroup" :loading-groups="loadingGroups"
|
||||
@group-toggle="toggleGroup" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Light from "@/components/panels/Light/Index";
|
||||
import LightPlugin from "@/components/panels/Light/Index";
|
||||
import Utils from "@/Utils";
|
||||
import Loading from "@/components/Loading";
|
||||
|
||||
export default {
|
||||
name: "LightHue",
|
||||
mixins: [Light],
|
||||
components: {Light},
|
||||
components: {Loading, LightPlugin},
|
||||
mixins: [Utils],
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
lights: {},
|
||||
groups: {},
|
||||
scenes: {},
|
||||
animations: {},
|
||||
loading: false,
|
||||
loadingLights: {},
|
||||
loadingGroups: {},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
groupsByName() {
|
||||
if (!this.groups)
|
||||
return {}
|
||||
|
||||
return Object.entries(this.groups).reduce((groups, [id, group]) => {
|
||||
groups[group.name || id] = {
|
||||
...group,
|
||||
id: id,
|
||||
}
|
||||
|
||||
return groups
|
||||
}, {})
|
||||
},
|
||||
|
||||
initialGroup() {
|
||||
if (!this.config.groups || !Object.keys(this.config.groups).length)
|
||||
return null
|
||||
|
||||
const group = this.config.groups[0]
|
||||
if (group in this.groups)
|
||||
return this.groups[group].id
|
||||
else if (group in this.groupsByName)
|
||||
return this.groupsByName[group].id
|
||||
return null
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getLights() {
|
||||
return await this.request('light.hue.get_lights')
|
||||
},
|
||||
|
||||
async getGroups() {
|
||||
return Object.entries(await this.request('light.hue.get_groups'))
|
||||
.filter((group) => !group[1].recycle && group[1].type.toLowerCase() === 'room')
|
||||
.reduce((obj, [id, group]) => {
|
||||
obj[id] = group
|
||||
return obj
|
||||
}, {})
|
||||
},
|
||||
|
||||
async getScenes() {
|
||||
return await this.request('light.hue.get_scenes')
|
||||
},
|
||||
|
||||
async toggleGroup(group) {
|
||||
const groups = []
|
||||
if (group != null)
|
||||
groups.push(group.name)
|
||||
|
||||
this.setGroupsLoading(group)
|
||||
try {
|
||||
await this.request('light.hue.toggle', {
|
||||
groups: groups,
|
||||
})
|
||||
|
||||
await this.refresh()
|
||||
} finally {
|
||||
this.unsetGroupsLoading(group)
|
||||
}
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
this.loading = true
|
||||
try {
|
||||
[this.lights, this.groups, this.scenes] = await Promise.all([
|
||||
this.getLights(),
|
||||
this.getGroups(),
|
||||
this.getScenes(),
|
||||
])
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
setGroupsLoading(...groups) {
|
||||
let loadingGroups = {}
|
||||
if (groups.length && groups[0]) {
|
||||
for (const group of groups)
|
||||
loadingGroups[group.id] = true
|
||||
} else {
|
||||
loadingGroups = Object.keys(this.groups)
|
||||
}
|
||||
|
||||
this.loadingGroups = {...this.loadingGroups, ...loadingGroups}
|
||||
},
|
||||
|
||||
unsetGroupsLoading(...groups) {
|
||||
for (const group of groups) {
|
||||
if (group.id in this.loadingGroups)
|
||||
delete this.loadingGroups[group.id]
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.refresh()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -54,7 +54,7 @@ $active-glow-bg-2: #9cdfb0 !default;
|
|||
/// Hover
|
||||
$default-hover-fg: #35b870 !default;
|
||||
$default-hover-fg-2: #38cf80 !default;
|
||||
$hover-bg: #def6ea !default;
|
||||
$hover-bg: #bef6da !default;
|
||||
|
||||
/// Navigator
|
||||
$nav-bg: #002626 !default;
|
||||
|
@ -67,3 +67,21 @@ $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-fg: #5e5e5e;
|
||||
|
||||
/// Panel/menu components
|
||||
$menu-panel-bg: #e0eae8;
|
||||
$menu-panel-content-bg: white;
|
||||
$plugin-panel-shadow: 0 0 2px 2px #ccc !default;
|
||||
$plugin-panel-entry-shadow: 1px 0 1px 1px #ddd !default;
|
||||
$plugin-panel-first-entry-shadow: 2px 0 1px -2px #ddd !default;
|
||||
$plugin-panel-last-entry-shadow: -1px 0 1px 0 #ddd !default;
|
||||
$menu-header-bg: #dde5e1 !default;
|
||||
$menu-header-shadow: 0 0 1px 1px #c0c0c0 !default;
|
||||
|
||||
/// Toggle switch
|
||||
$toggle-bg: #e0e8e0 !default;
|
||||
$toggle-selected-bg: linear-gradient(90deg, #4fef97, #27ee5e) !default;
|
||||
$toggle-dot-bg: #d4d8d6 !default;
|
||||
$toggle-shadow: inset 0 0 2px 1px #c8c8c8 !default;
|
||||
$toggle-dot-shadow: inset 0 0 2px 1px #d0d0d0 !default;
|
||||
$toggle-selected-dot-bg: linear-gradient(160deg, #ecfff0, #e4fff8);
|
||||
|
|
|
@ -23,10 +23,10 @@ export default {
|
|||
axios.post('/execute', request, opts)
|
||||
.then((response) => {
|
||||
response = response.data.response
|
||||
if (!response.errors.length) {
|
||||
if (!response.errors?.length) {
|
||||
resolve(response.output);
|
||||
} else {
|
||||
const error = response.errors[0]
|
||||
const error = response.errors?.[0] || response
|
||||
this.notify({
|
||||
text: error,
|
||||
error: true,
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<main>
|
||||
<Loading v-if="loading" />
|
||||
<Nav :panels="components" :selected-panel="selectedPanel" :hostname="hostname" @select="selectedPanel = $event" />
|
||||
<Nav :panels="components" :selected-panel="selectedPanel" :hostname="hostname"
|
||||
@select="selectedPanel = $event" v-else />
|
||||
|
||||
<div class="panel-container">
|
||||
<div class="panel" v-for="(panel, name) in components" :key="name">
|
||||
|
@ -35,6 +36,16 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
initSelectedPanel() {
|
||||
const match = this.$route.hash.match('#?([a-zA-Z0-9.]+)[?]?(.*)')
|
||||
if (!match)
|
||||
return
|
||||
|
||||
const plugin = match[1]
|
||||
if (plugin?.length)
|
||||
this.selectedPanel = plugin
|
||||
},
|
||||
|
||||
initPanels() {
|
||||
const self = this
|
||||
this.components = {}
|
||||
|
@ -75,6 +86,7 @@ export default {
|
|||
try {
|
||||
await this.parseConfig()
|
||||
this.initPanels()
|
||||
this.initSelectedPanel()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
@ -87,9 +99,18 @@ main {
|
|||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
.panel-container {
|
||||
display: flex;
|
||||
flex-grow: 100;
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
margin: 0 !important;
|
||||
box-shadow: none !important;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in a new issue