Lights panel migration - WIP
This commit is contained in:
parent
79179746a7
commit
2de1e3ebe6
22 changed files with 1193 additions and 152 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-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>
|
||||
<!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.2793b349.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.9f4771bd.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.9f4771bd.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
192
platypush/backend/http/webapp/src/components/Light/Controls.vue
Normal file
192
platypush/backend/http/webapp/src/components/Light/Controls.vue
Normal file
|
@ -0,0 +1,192 @@
|
|||
<template>
|
||||
<div class="controls light-controls" @click="$event.stopPropagation()">
|
||||
<Loading v-if="loading" />
|
||||
|
||||
<div class="row" v-if="state.bri != null">
|
||||
<div class="col-1 icon">
|
||||
<i class="fas fa-sun" />
|
||||
</div>
|
||||
<div class="col-11 control">
|
||||
<Slider :range="colorConverter.ranges.bri" :disabled="loading" :value="state.bri"
|
||||
@mouseup.stop="$emit(light ? 'set-light' : 'set-group', {brightness: parseInt($event.target.value)})" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="state.ct != null">
|
||||
<div class="col-1 icon">
|
||||
<i class="fas fa-thermometer-half" />
|
||||
</div>
|
||||
<div class="col-11 control">
|
||||
<Slider :range="colorConverter.ranges.ct" :disabled="loading" :value="state.ct"
|
||||
@change.stop="$emit(light ? 'set-light' : 'set-group', {temperature: parseInt($event.target.value)})" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="row" v-if="rgbColor">
|
||||
<span class="col-1 icon">
|
||||
<i class="fas fa-palette" />
|
||||
</span>
|
||||
<span class="col-11 control">
|
||||
<input type="color" :value="rgbColor" @change.stop="onColorSelect" />
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Slider from "@/components/elements/Slider";
|
||||
import Loading from "@/components/Loading";
|
||||
import {ColorConverter} from "@/components/panels/Light/color";
|
||||
|
||||
export default {
|
||||
name: "Controls",
|
||||
components: {Loading, Slider},
|
||||
emits: ['set-light', 'set-group'],
|
||||
props: {
|
||||
light: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
lights: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
group: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
colorConverter: {
|
||||
type: Object,
|
||||
default: () => new ColorConverter(),
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
state() {
|
||||
if (this.light?.state)
|
||||
return this.light.state
|
||||
|
||||
const state = this.group?.state || {}
|
||||
if (!this.lights)
|
||||
return state
|
||||
|
||||
const avg = (values) => {
|
||||
if (!(values && values.length))
|
||||
return 0
|
||||
|
||||
if (values[0] instanceof Array)
|
||||
return [...values[0].keys()].map((i) => {
|
||||
return avg(values.map((value) => value[i]))
|
||||
})
|
||||
|
||||
return values.reduce((sum, value) => sum+value, 0) / values.length
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
...Object.entries(
|
||||
Object.values(this.lights).reduce((obj, light) => {
|
||||
['bri', 'hue', 'sat', 'rgb', 'xy', 'red', 'green', 'blue', 'ct'].forEach((attr) => {
|
||||
if (light.state?.[attr] != null) {
|
||||
obj[attr] = [...(obj[attr] || []), light.state[attr]]
|
||||
}
|
||||
})
|
||||
|
||||
return obj
|
||||
}, {})
|
||||
).reduce((obj, [attr, values]) => {
|
||||
obj[attr] = avg(values)
|
||||
return obj
|
||||
}, {})
|
||||
}
|
||||
},
|
||||
|
||||
color() {
|
||||
return this.getColor(this.state)
|
||||
},
|
||||
|
||||
rgbColor() {
|
||||
const rgb = this.colorConverter.toRGB(this.state)
|
||||
if (rgb)
|
||||
return '#' + rgb.map((x) => {
|
||||
let hex = x.toString(16)
|
||||
if (hex.length < 2)
|
||||
hex = '0' + hex
|
||||
return hex
|
||||
}).join('')
|
||||
|
||||
return null
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onColorSelect(event) {
|
||||
const rgb = event.target.value.slice(1).split(/(?=(?:..)*$)/).map((t) => parseInt(`0x${t}`))
|
||||
this.$emit(this.light ? 'set-light' : 'set-group', {
|
||||
rgb: rgb,
|
||||
xy: this.colorConverter.rgbToXY(...rgb),
|
||||
hsl: this.colorConverter.rgbToHsl(...rgb),
|
||||
brightness: this.colorConverter.rgbToBri(...rgb),
|
||||
})
|
||||
},
|
||||
|
||||
getColor(state) {
|
||||
return {
|
||||
rgb: this.colorConverter.toRGB(state),
|
||||
xy: this.colorConverter.toXY(state),
|
||||
hsl: this.colorConverter.toHSL(state),
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$light-controls-bg: white;
|
||||
|
||||
.controls {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 2.25em;
|
||||
background: $light-controls-bg;
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 1em;
|
||||
box-shadow: $plugin-panel-shadow;
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control {
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
|
||||
.icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
input[type=color] {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.light-controls {
|
||||
.row {
|
||||
.slider {
|
||||
margin-top: 0.4em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,31 +1,79 @@
|
|||
<template>
|
||||
<div class="light-group-container">
|
||||
<MenuPanel>
|
||||
<li class="header">
|
||||
<button class="back-btn" title="Back" @click="close" v-if="group">
|
||||
<div class="panel-row header">
|
||||
<div class="col-3" v-if="group">
|
||||
<button class="back-btn" title="Back" @click="close">
|
||||
<i class="fas fa-chevron-left" />
|
||||
</button>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<div class="col-6 name" :class="{selected: selectedView === 'group'}"
|
||||
v-text="groupName" @click="selectedView = selectedView === 'group' ? null : 'group'" />
|
||||
|
||||
<div class="col-3 pull-right" v-if="group">
|
||||
<ToggleSwitch :value="group.state.any_on" @input="$emit('group-toggle', group)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div class="lights-view" v-else>
|
||||
<div class="row view-selector">
|
||||
<button :class="{selected: selectedView === 'lights'}" title="Lights" @click="selectedView = 'lights'">
|
||||
<i class="fas fa-lightbulb" /> Lights
|
||||
</button>
|
||||
<button :class="{selected: selectedView === 'scenes'}" title="Scenes" @click="selectedView = 'scenes'">
|
||||
<i class="far fa-image" /> Scenes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="view" v-if="selectedView === 'lights'">
|
||||
<keep-alive>
|
||||
<div class="panel-row row" :class="{expanded: light.id === selectedLight}"
|
||||
v-for="(light, id) in lightsSorted" :key="id"
|
||||
@click="selectedLight = selectedLight === light.id ? null : light.id">
|
||||
<Light :light="light" :group="group" :collapsed="light.id !== selectedLight"
|
||||
:color-converter="colorConverter" @toggle="$emit('light-toggle', light)"
|
||||
@set-light="$emit('set-light', {light: light, value: $event})" />
|
||||
</div>
|
||||
</keep-alive>
|
||||
</div>
|
||||
|
||||
<div class="view" v-else-if="selectedView === 'scenes'">
|
||||
<keep-alive>
|
||||
<div class="panel-row row" :class="{selected: scene.id === selectedScene}"
|
||||
v-for="(scene, id) in scenesSorted" :key="id" @click="onSceneSelected(scene.id)">
|
||||
<Scene :scene="scene" :group="group" />
|
||||
</div>
|
||||
</keep-alive>
|
||||
</div>
|
||||
|
||||
<div class="view group-controls" v-else-if="selectedView === 'group'">
|
||||
<keep-alive>
|
||||
<Controls :group="group" :lights="lights" :color-converter="colorConverter"
|
||||
@set-group="$emit('set-group', $event)" />
|
||||
</keep-alive>
|
||||
</div>
|
||||
</div>
|
||||
</MenuPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Light from "@/components/Light/Light";
|
||||
import Scene from "@/components/Light/Scene";
|
||||
import Controls from "@/components/Light/Controls";
|
||||
import MenuPanel from "@/components/MenuPanel";
|
||||
import ToggleSwitch from "@/components/elements/ToggleSwitch";
|
||||
import {ColorConverter} from "@/components/panels/Light/color";
|
||||
|
||||
export default {
|
||||
name: "Group",
|
||||
emits: ['close'],
|
||||
components: {MenuPanel, Light},
|
||||
emits: ['close', 'group-toggle', 'light-toggle', 'set-light', 'select-scene'],
|
||||
components: {ToggleSwitch, MenuPanel, Light, Scene, Controls},
|
||||
props: {
|
||||
lights: {
|
||||
type: Object,
|
||||
|
@ -34,6 +82,23 @@ export default {
|
|||
group: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
scenes: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
colorConverter: {
|
||||
type: Object,
|
||||
default: () => new ColorConverter(),
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedLight: null,
|
||||
selectedScene: null,
|
||||
selectedView: 'lights',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -50,6 +115,28 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
|
||||
scenesSorted() {
|
||||
if (!this.scenes)
|
||||
return []
|
||||
|
||||
return Object.entries(this.scenes)
|
||||
.sort((a, b) => a[1].name.localeCompare(b[1].name))
|
||||
.map(([id, scene]) => {
|
||||
return {
|
||||
...scene,
|
||||
id: id,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
groupName() {
|
||||
if (this.group?.name)
|
||||
return this.group.name
|
||||
if (this.group?.id != null)
|
||||
return `[Group ${this.group.id}]`
|
||||
return 'Lights'
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -57,6 +144,11 @@ export default {
|
|||
event.stopPropagation()
|
||||
this.$emit('close')
|
||||
},
|
||||
|
||||
onSceneSelected(sceneId) {
|
||||
this.selectedScene = sceneId
|
||||
this.$emit('select-scene', sceneId)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -64,23 +156,80 @@ export default {
|
|||
<style lang="scss">
|
||||
.light-group-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
.row.panel-row {
|
||||
flex-direction: column;
|
||||
|
||||
&.expanded,
|
||||
&.selected {
|
||||
background: $selected-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 0.5em !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.back-btn {
|
||||
border: 0;
|
||||
background: none;
|
||||
|
||||
&:hover {
|
||||
border: 0;
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
|
||||
&.selected {
|
||||
color: $selected-fg;
|
||||
}
|
||||
|
||||
li.header {
|
||||
.back-btn {
|
||||
background: none;
|
||||
margin-left: -0.75em;
|
||||
&:hover {
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.view-selector {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
|
||||
button {
|
||||
width: 50%;
|
||||
padding: 1.5em;
|
||||
text-align: left;
|
||||
opacity: 0.8;
|
||||
box-shadow: $plugin-panel-entry-shadow;
|
||||
|
||||
&:first-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: $selected-bg;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.light-group-container {
|
||||
.group-controls {
|
||||
margin: 1em;
|
||||
|
||||
.controls {
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,33 @@
|
|||
<template>
|
||||
<MenuPanel>
|
||||
<li class="header">
|
||||
<div class="panel-row header">
|
||||
<div class="col-3">
|
||||
<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)">
|
||||
</div>
|
||||
<div class="col-6 name">
|
||||
Rooms
|
||||
</div>
|
||||
<div class="col-3 pull-right">
|
||||
<ToggleSwitch :value="anyLightsOn" @input="$emit('toggle')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-row 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}]` }}
|
||||
{{ 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)" />
|
||||
<ToggleSwitch :value="group.state.any_on" :disabled="group.id in (loadingGroups || {})"
|
||||
@input="$emit('toggle', group)" />
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
</MenuPanel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuPanel from "@/components/MenuPanel";
|
||||
import ToggleSwitch from "@/components/elements/ToggleSwitch";
|
||||
import {ColorConverter} from "@/components/panels/Light/color";
|
||||
|
||||
export default {
|
||||
name: "Groups",
|
||||
|
@ -32,7 +42,12 @@ export default {
|
|||
loadingGroups: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
|
||||
colorConverter: {
|
||||
type: Object,
|
||||
default: () => new ColorConverter(),
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -46,22 +61,31 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleGroup(group) {
|
||||
this.$emit('toggle', group)
|
||||
}
|
||||
}
|
||||
anyLightsOn() {
|
||||
for (const group of Object.values(this.groups))
|
||||
if (group?.state?.any_on)
|
||||
return true
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 0.75em !important;
|
||||
padding-bottom: 0.75em !important;
|
||||
|
||||
.icon {
|
||||
margin-right: 1em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +1,69 @@
|
|||
<template>
|
||||
<div class="light">
|
||||
{{ light.name || light.id }}
|
||||
<div class="light" :class="{expanded: !collapsed}" ref="element">
|
||||
<div class="row">
|
||||
<span class="name col-9" @click="expandToggle">
|
||||
{{ light.name || `[Light ${light.id}]` }}
|
||||
</span>
|
||||
<span class="toggle col-3 pull-right">
|
||||
<ToggleSwitch :value="light.state.on" :disabled="loading" @input="$emit('toggle', light)" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="row fade-in" v-if="!collapsed">
|
||||
<Controls :light="light" :loading="loading" :color-converter="colorConverter"
|
||||
@set-light="$emit('set-light', $event)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ToggleSwitch from "@/components/elements/ToggleSwitch";
|
||||
import Controls from "@/components/Light/Controls";
|
||||
import {ColorConverter} from "@/components/panels/Light/color";
|
||||
|
||||
export default {
|
||||
name: "Light",
|
||||
components: {Controls, ToggleSwitch},
|
||||
emits: ['toggle', 'set-light', 'collapsed', 'expanded'],
|
||||
props: {
|
||||
light: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
group: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
colorConverter: {
|
||||
type: Object,
|
||||
default: () => new ColorConverter(),
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
expandToggle() {
|
||||
this.$emit(this.collapsed ? 'expanded' : 'collapsed')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.expanded {
|
||||
.name {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
}
|
||||
</style>
|
38
platypush/backend/http/webapp/src/components/Light/Scene.vue
Normal file
38
platypush/backend/http/webapp/src/components/Light/Scene.vue
Normal file
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<Loading v-if="loading" />
|
||||
<div class="scene row name" @click="onSelect">
|
||||
{{ scene.name || `[Scene ${scene.id}]` }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Light",
|
||||
emits: ['select'],
|
||||
props: {
|
||||
scene: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
group: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSelect() {
|
||||
if (this.loading)
|
||||
return false
|
||||
|
||||
this.$emit('select')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,19 +1,14 @@
|
|||
<template>
|
||||
<div class="menu-panel">
|
||||
<ul :style="style">
|
||||
<div class="content">
|
||||
<slot />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MenuPanel",
|
||||
props: {
|
||||
style: {
|
||||
type: [String, Object, Array],
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -27,15 +22,16 @@ export default {
|
|||
background: $menu-panel-bg;
|
||||
padding-top: 2em;
|
||||
|
||||
ul {
|
||||
.content {
|
||||
background: $menu-panel-content-bg;
|
||||
border-radius: 15px;
|
||||
box-shadow: $plugin-panel-shadow;
|
||||
border: 0;
|
||||
|
||||
li {
|
||||
.panel-row {
|
||||
display: flex;
|
||||
padding: 0.5em;
|
||||
margin: 0 !important;
|
||||
padding: 1em;
|
||||
box-shadow: $plugin-panel-entry-shadow;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
|
@ -54,8 +50,9 @@ export default {
|
|||
border-radius: 0 0 15px 15px;
|
||||
box-shadow: $plugin-panel-last-entry-shadow;
|
||||
}
|
||||
}
|
||||
|
||||
&.header {
|
||||
.header {
|
||||
background: $menu-header-bg;
|
||||
font-weight: bold;
|
||||
box-shadow: $menu-header-shadow;
|
||||
|
@ -65,17 +62,17 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
.menu-panel {
|
||||
padding-top: 0;
|
||||
ul {
|
||||
|
||||
.content {
|
||||
min-width: 100%;
|
||||
border-radius: 0;
|
||||
|
||||
li {
|
||||
.row {
|
||||
&:first-child {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
@ -90,32 +87,32 @@ export default {
|
|||
|
||||
@media screen and (min-width: $tablet) {
|
||||
.menu-panel {
|
||||
ul {
|
||||
min-width: 65%;
|
||||
.content {
|
||||
min-width: 75%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: $desktop) {
|
||||
.menu-panel {
|
||||
ul {
|
||||
min-width: 40%;
|
||||
.content {
|
||||
min-width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: $widescreen) {
|
||||
.menu-panel {
|
||||
ul {
|
||||
min-width: 30%;
|
||||
.content {
|
||||
min-width: 40%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: $fullhd) {
|
||||
.menu-panel {
|
||||
ul {
|
||||
min-width: 25%;
|
||||
.content {
|
||||
min-width: 35%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
collapsed: false,
|
||||
collapsed: true,
|
||||
icons: icons,
|
||||
host: null,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<label>
|
||||
<input class="slider" type="range" :min="range[0]" :max="range[1]" :value="value"
|
||||
@change="$emit('input', $event)" @mouseup="$emit('mouseup', $event)"
|
||||
@mousedown="$emit('mousedown', $event)">
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Slider",
|
||||
emits: ['input', 'mouseup', 'mousedown'],
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
range: {
|
||||
type: Array,
|
||||
default: () => [0, 100],
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.slider {
|
||||
@include appearance(none);
|
||||
@include transition(opacity .2s);
|
||||
width: 100%;
|
||||
height: 1em;
|
||||
border-radius: 0.33em;
|
||||
background: $slider-bg;
|
||||
outline: none;
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
@include appearance(none);
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
border-radius: 50%;
|
||||
border: 0;
|
||||
background: $slider-thumb-bg;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
@include appearance(none);
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
border-radius: 50%;
|
||||
border: 0;
|
||||
background: $slider-thumb-bg;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[disabled]::-webkit-slider-thumb {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&[disabled]::-moz-range-thumb {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&.disabled { opacity: 0.3; }
|
||||
|
||||
&::-moz-range-track {
|
||||
@include appearance(none);
|
||||
}
|
||||
|
||||
&::-moz-range-progress {
|
||||
background: $slider-progress-bg;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
&[disabled]::-webkit-progress-value {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&[disabled]::-moz-range-progress {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -109,7 +109,7 @@ export default {
|
|||
border-radius: 50%;
|
||||
box-shadow: $toggle-dot-shadow;
|
||||
left: -0.25em;
|
||||
top: -1px;
|
||||
top: -0.05em;
|
||||
transform: translateX(var(--offset, 0));
|
||||
transition: transform .4s, box-shadow .4s;
|
||||
|
||||
|
@ -152,7 +152,7 @@ export default {
|
|||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
color: #A6ACCD;
|
||||
transform: translateY(4px);
|
||||
transform: translateY(0.2em);
|
||||
transition: opacity .4s, transform .4s;
|
||||
}
|
||||
}
|
||||
|
@ -162,7 +162,7 @@ export default {
|
|||
pointer-events: none;
|
||||
& + span {
|
||||
opacity: 1;
|
||||
transform: translateY(12px);
|
||||
transform: translateY(0.6em);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
<template>
|
||||
<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" />
|
||||
<Groups :groups="groups" :loading-groups="loadingGroups" :color-converter="colorConverter"
|
||||
@select="selectedGroup = $event" @toggle="$emit('group-toggle', $event)" />
|
||||
</div>
|
||||
<div class="panel" v-else>
|
||||
<Group :group="groups[selectedGroup]" :lights="displayedLights" @close="closeGroup" />
|
||||
<Group :group="groups[selectedGroup]" :lights="displayedLights" :scenes="scenesByGroup[selectedGroup]"
|
||||
:color-converter="colorConverter" @close="closeGroup" @light-toggle="$emit('light-toggle', $event)"
|
||||
@group-toggle="$emit('group-toggle', $event)" @set-light="$emit('set-light', $event)"
|
||||
@set-group="$emit('set-group', {groupId: selectedGroup, value: $event})"
|
||||
@select-scene="$emit('select-scene', {groupId: selectedGroup, sceneId: $event})" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -14,6 +19,7 @@ import Utils from "@/Utils";
|
|||
import Panel from "@/components/panels/Panel";
|
||||
import Groups from "@/components/Light/Groups";
|
||||
import Group from "@/components/Light/Group";
|
||||
import {ColorConverter} from "@/components/panels/Light/color";
|
||||
|
||||
/**
|
||||
* Generic component for light plugins panels.
|
||||
|
@ -22,7 +28,7 @@ export default {
|
|||
name: "Light",
|
||||
components: {Group, Groups},
|
||||
mixins: [Utils, Panel],
|
||||
emits: ['group-toggle'],
|
||||
emits: ['group-toggle', 'light-toggle', 'set-light', 'set-group', 'select-scene'],
|
||||
|
||||
props: {
|
||||
lights: {
|
||||
|
@ -41,6 +47,11 @@ export default {
|
|||
type: Object,
|
||||
},
|
||||
|
||||
colorConverter: {
|
||||
type: Object,
|
||||
default: () => new ColorConverter(),
|
||||
},
|
||||
|
||||
loadingLights: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
|
@ -78,6 +89,51 @@ export default {
|
|||
return lights
|
||||
}, {})
|
||||
},
|
||||
|
||||
lightsByGroup() {
|
||||
if (!this.groups)
|
||||
return {}
|
||||
|
||||
const self = this
|
||||
return Object.entries(this.groups).reduce((obj, [groupId, group]) => {
|
||||
obj[groupId] = group.lights.map((lightId) => self.lights[lightId])
|
||||
return obj
|
||||
}, {})
|
||||
},
|
||||
|
||||
groupsByLight() {
|
||||
if (!this.groups)
|
||||
return {}
|
||||
|
||||
return Object.entries(this.groups).reduce((obj, [groupId, group]) => {
|
||||
group.lights.forEach((lightId) => {
|
||||
if (!obj[lightId])
|
||||
obj[lightId] = {}
|
||||
obj[lightId][groupId] = group
|
||||
})
|
||||
|
||||
return obj
|
||||
}, {})
|
||||
},
|
||||
|
||||
scenesByGroup() {
|
||||
if (!this.scenes)
|
||||
return {}
|
||||
|
||||
const self = this
|
||||
return Object.entries(this.scenes).reduce((obj, [sceneId, scene]) => {
|
||||
scene.lights.forEach((lightId) => {
|
||||
Object.keys(self.groupsByLight[lightId]).forEach((groupId) => {
|
||||
if (!obj[groupId])
|
||||
obj[groupId] = {}
|
||||
|
||||
obj[groupId][sceneId] = scene
|
||||
})
|
||||
})
|
||||
|
||||
return obj
|
||||
}, {})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -97,10 +153,6 @@ export default {
|
|||
closeGroup() {
|
||||
this.selectedGroup = null
|
||||
},
|
||||
|
||||
toggleGroup(group) {
|
||||
this.$emit('group-toggle', group)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
<script>
|
||||
export default {
|
||||
name: "Utils",
|
||||
data() {
|
||||
return {
|
||||
lights: {},
|
||||
groups: {},
|
||||
scenes: {},
|
||||
animations: {},
|
||||
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
|
||||
}, {})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
_getGroups(groupIds) {
|
||||
const groups = groupIds.filter((id) => id != null)
|
||||
if (!groups.length)
|
||||
return Object.values(this.groups)
|
||||
|
||||
const self = this
|
||||
return groups.map((id) => id instanceof Object ? id : self.groups[id])
|
||||
},
|
||||
|
||||
_getLights(lightIds) {
|
||||
const lights = lightIds.filter((id) => id != null)
|
||||
if (!lights.length)
|
||||
return Object.values(this.lights)
|
||||
|
||||
const self = this
|
||||
return lights.map((id) => id instanceof Object ? id : self.lights[id])
|
||||
},
|
||||
|
||||
setGroupsLoading(groupsIds) {
|
||||
const self = this
|
||||
this._getGroups(groupsIds).forEach((group) => {
|
||||
self.loadingGroups[group.id] = true
|
||||
if (group.lights)
|
||||
self.setLightsLoading(group.lights)
|
||||
})
|
||||
},
|
||||
|
||||
unsetGroupsLoading(groupsIds) {
|
||||
const self = this
|
||||
this._getGroups(groupsIds).forEach((group) => {
|
||||
if (group.id in self.loadingGroups)
|
||||
delete self.loadingGroups[group.id]
|
||||
if (group.lights)
|
||||
self.setLightsLoading(group.lights)
|
||||
})
|
||||
},
|
||||
|
||||
setLightsLoading(lightIds) {
|
||||
const self = this
|
||||
this._getLights(lightIds).forEach((light) => {
|
||||
self.loadingLights[light.id] = true
|
||||
})
|
||||
},
|
||||
|
||||
unsetLightsLoading(lightIds) {
|
||||
const self = this
|
||||
this._getLights(lightIds).forEach((light) => {
|
||||
if (light.id in self.loadingLights)
|
||||
delete self.loadingLights[light.id]
|
||||
})
|
||||
},
|
||||
|
||||
async groupAction(action, args, ...groups) {
|
||||
this.setGroupsLoading(groups)
|
||||
try {
|
||||
return await this.request(action, args)
|
||||
} finally {
|
||||
this.unsetGroupsLoading(groups)
|
||||
}
|
||||
},
|
||||
|
||||
async lightAction(action, args, ...lights) {
|
||||
this.setLightsLoading(lights)
|
||||
try {
|
||||
return await this.request(action, args)
|
||||
} finally {
|
||||
this.unsetLightsLoading(lights)
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,213 @@
|
|||
export class ColorConverter {
|
||||
constructor(ranges) {
|
||||
this.ranges = {
|
||||
hue: [0, 360],
|
||||
sat: [0, 100],
|
||||
bri: [0, 100],
|
||||
ct: [154, 500],
|
||||
}
|
||||
|
||||
if (ranges)
|
||||
for (const attr of Object.keys(this.ranges))
|
||||
if (ranges[attr])
|
||||
this.ranges[attr] = ranges[attr]
|
||||
}
|
||||
|
||||
normalize(x, xRange, yRange) {
|
||||
return yRange[0] + (((x-xRange[0]) * (yRange[1]-yRange[0])) / (xRange[1]-xRange[0]))
|
||||
}
|
||||
|
||||
hslToRgb(h, s, l) {
|
||||
[h, s, l] = [
|
||||
this.normalize(h, this.ranges.hue, [0, 360]),
|
||||
this.normalize(s, this.ranges.sat, [0, 100]),
|
||||
this.normalize(l, this.ranges.bri, [0, 100]),
|
||||
]
|
||||
|
||||
l /= 100
|
||||
const a = s * Math.min(l, 1 - l) / 100
|
||||
const f = n => {
|
||||
const k = (n + h / 30) % 12
|
||||
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
|
||||
return Math.round(255 * color)
|
||||
}
|
||||
|
||||
return [f(0), f(8), f(4)]
|
||||
}
|
||||
|
||||
rgbToHsl(r, g, b){
|
||||
r /= 255
|
||||
g /= 255
|
||||
b /= 255;
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let h, s, l = (max + min) / 2;
|
||||
|
||||
if(max === min){
|
||||
h = s = 0; // achromatic
|
||||
} else {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
|
||||
switch(max){
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
case b: h = (r - g) / d + 4; break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return [
|
||||
parseInt(this.normalize(h, [0, 1], this.ranges.hue)),
|
||||
parseInt(this.normalize(s, [0, 1], this.ranges.sat)),
|
||||
parseInt(this.normalize(l, [0, 1], this.ranges.bri)),
|
||||
]
|
||||
}
|
||||
|
||||
xyToRgb(x, y, brightness) {
|
||||
// Set to maximum brightness if no custom value was given (Not the slick ECMAScript 6 way for compatibility reasons)
|
||||
if (brightness == null)
|
||||
brightness = this.ranges.bri[1];
|
||||
|
||||
const z = 1.0 - x - y;
|
||||
const Y = (brightness / (this.ranges.bri[1]-1)).toFixed(2);
|
||||
const X = (Y / y) * x;
|
||||
const Z = (Y / y) * z;
|
||||
|
||||
//Convert to RGB using Wide RGB D65 conversion
|
||||
let red = X * 1.656492 - Y * 0.354851 - Z * 0.255038;
|
||||
let green = -X * 0.707196 + Y * 1.655397 + Z * 0.036152;
|
||||
let blue = X * 0.051713 - Y * 0.121364 + Z * 1.011530;
|
||||
|
||||
//If red, green or blue is larger than 1.0 set it back to the maximum of 1.0
|
||||
if (red > blue && red > green && red > 1.0) {
|
||||
green = green / red;
|
||||
blue = blue / red;
|
||||
red = 1.0;
|
||||
} else if (green > blue && green > red && green > 1.0) {
|
||||
red = red / green;
|
||||
blue = blue / green;
|
||||
green = 1.0;
|
||||
} else if (blue > red && blue > green && blue > 1.0) {
|
||||
red = red / blue;
|
||||
green = green / blue;
|
||||
blue = 1.0;
|
||||
}
|
||||
|
||||
//Reverse gamma correction
|
||||
red = red <= 0.0031308 ? 12.92 * red : (1.0 + 0.055) * Math.pow(red, (1.0 / 2.4)) - 0.055;
|
||||
green = green <= 0.0031308 ? 12.92 * green : (1.0 + 0.055) * Math.pow(green, (1.0 / 2.4)) - 0.055;
|
||||
blue = blue <= 0.0031308 ? 12.92 * blue : (1.0 + 0.055) * Math.pow(blue, (1.0 / 2.4)) - 0.055;
|
||||
|
||||
//Convert normalized decimal to decimal
|
||||
red = Math.round(red * 255);
|
||||
green = Math.round(green * 255);
|
||||
blue = Math.round(blue * 255);
|
||||
|
||||
if (isNaN(red))
|
||||
red = 0;
|
||||
if (isNaN(green))
|
||||
green = 0;
|
||||
if (isNaN(blue))
|
||||
blue = 0;
|
||||
|
||||
return [red, green, blue].map((c) => Math.min(Math.max(0, c), 255))
|
||||
}
|
||||
|
||||
rgbToXY(red, green, blue) {
|
||||
if (red > 1) { red /= 255; }
|
||||
if (green > 1) { green /= 255; }
|
||||
if (blue > 1) { blue /= 255; }
|
||||
|
||||
//Apply a gamma correction to the RGB values, which makes the color more vivid and more the like the color displayed on the screen of your device
|
||||
red = (red > 0.04045) ? Math.pow((red + 0.055) / (1.0 + 0.055), 2.4) : (red / 12.92);
|
||||
green = (green > 0.04045) ? Math.pow((green + 0.055) / (1.0 + 0.055), 2.4) : (green / 12.92);
|
||||
blue = (blue > 0.04045) ? Math.pow((blue + 0.055) / (1.0 + 0.055), 2.4) : (blue / 12.92);
|
||||
|
||||
//RGB values to XYZ using the Wide RGB D65 conversion formula
|
||||
const X = red * 0.664511 + green * 0.154324 + blue * 0.162028;
|
||||
const Y = red * 0.283881 + green * 0.668433 + blue * 0.047685;
|
||||
const Z = red * 0.000088 + green * 0.072310 + blue * 0.986039;
|
||||
|
||||
//Calculate the xy values from the XYZ values
|
||||
let x = parseFloat((X / (X + Y + Z)).toFixed(4));
|
||||
let y = parseFloat((Y / (X + Y + Z)).toFixed(4));
|
||||
|
||||
if (isNaN(x))
|
||||
x = 0;
|
||||
if (isNaN(y))
|
||||
y = 0;
|
||||
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
rgbToBri(red, green, blue) {
|
||||
return Math.min(2 * this.rgbToHsl(red, green, blue)[2], this.ranges.bri[1])
|
||||
}
|
||||
|
||||
getRGB(color) {
|
||||
if (color.red != null && color.green != null && color.blue != null)
|
||||
return [color.red, color.green, color.blue]
|
||||
if (color.r != null && color.g != null && color.b != null)
|
||||
return [color.r, color.g, color.b]
|
||||
if (color.rgb)
|
||||
return color.rgb
|
||||
}
|
||||
|
||||
getXY(color) {
|
||||
if (color.x != null && color.y != null)
|
||||
return [color.x, color.y]
|
||||
if (color.xy)
|
||||
return color.xy
|
||||
}
|
||||
|
||||
toRGB(color) {
|
||||
const rgb = this.getRGB(color)
|
||||
if (rgb)
|
||||
return rgb
|
||||
|
||||
const xy = this.getXY(color)
|
||||
if (xy && color.bri)
|
||||
return this.xyToRgb(...xy, color.bri)
|
||||
if (color.hue && color.sat && color.bri)
|
||||
return this.hslToRgb(color.hue, color.sat, color.bri)
|
||||
|
||||
console.debug('Could not determine color space')
|
||||
console.debug(color)
|
||||
}
|
||||
|
||||
toXY(color) {
|
||||
const xy = this.getXY(color)
|
||||
if (xy && color.bri)
|
||||
return [xy[0], xy[1], color.bri]
|
||||
|
||||
const rgb = this.getRGB(color)
|
||||
if (rgb)
|
||||
return this.rgbToXY(...rgb)
|
||||
|
||||
if (color.hue && color.sat && color.bri) {
|
||||
const rgb = this.hslToRgb(color.hue, color.sat, color.bri)
|
||||
return this.rgbToXY(...rgb)
|
||||
}
|
||||
|
||||
console.debug('Could not determine color space')
|
||||
console.debug(color)
|
||||
}
|
||||
|
||||
toHSL(color) {
|
||||
if (color.hue && color.sat && color.bri)
|
||||
return [color.hue, color.sat, color.bri]
|
||||
|
||||
const rgb = this.getRGB(color)
|
||||
if (rgb)
|
||||
return this.rgbToHsl(...rgb)
|
||||
|
||||
const xy = this.getXY(color)
|
||||
if (xy && color.bri) {
|
||||
const rgb = this.xyToRgb(...xy, color.bri)
|
||||
return this.rgbToHsl(...rgb)
|
||||
}
|
||||
|
||||
console.debug('Could not determine color space')
|
||||
console.debug(color)
|
||||
}
|
||||
}
|
|
@ -2,18 +2,21 @@
|
|||
<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" />
|
||||
:color-converter="colorConverter" @group-toggle="toggleGroup"
|
||||
@light-toggle="toggleLight" @set-light="setLight" @set-group="setGroup" @select-scene="setScene" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LightPlugin from "@/components/panels/Light/Index";
|
||||
import LightUtils from "@/components/panels/Light/Utils";
|
||||
import { ColorConverter } from "@/components/panels/Light/color"
|
||||
import Utils from "@/Utils";
|
||||
import Loading from "@/components/Loading";
|
||||
|
||||
export default {
|
||||
name: "LightHue",
|
||||
components: {Loading, LightPlugin},
|
||||
mixins: [Utils],
|
||||
mixins: [Utils, LightUtils],
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
|
@ -23,31 +26,17 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
lights: {},
|
||||
groups: {},
|
||||
scenes: {},
|
||||
animations: {},
|
||||
loading: false,
|
||||
loadingLights: {},
|
||||
loadingGroups: {},
|
||||
colorConverter: new ColorConverter({
|
||||
hue: [0, 65535],
|
||||
sat: [0, 255],
|
||||
bri: [0, 255],
|
||||
ct: [150, 500],
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
|
@ -76,28 +65,128 @@ export default {
|
|||
},
|
||||
|
||||
async getScenes() {
|
||||
return await this.request('light.hue.get_scenes')
|
||||
// return await this.request('light.hue.get_scenes')
|
||||
return Object.entries(await this.request('light.hue.get_scenes'))
|
||||
.filter((scene) => !scene[1].recycle && scene[1].type.toLowerCase() === 'lightscene')
|
||||
.reduce((obj, [id, scene]) => {
|
||||
obj[id] = scene
|
||||
return obj
|
||||
}, {})
|
||||
},
|
||||
|
||||
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)
|
||||
let groups = Object.values(this.groups)
|
||||
let args = {
|
||||
groups: groups.map((group) => group.name)
|
||||
}
|
||||
|
||||
if (group != null) {
|
||||
groups = [group]
|
||||
args = {
|
||||
groups: [group.name],
|
||||
}
|
||||
}
|
||||
|
||||
await this.groupAction('light.hue.toggle', args, ...groups)
|
||||
await this.refresh(true)
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
async toggleLight(light) {
|
||||
const lights = [light]
|
||||
const args = light != null ? {
|
||||
lights: [light.name],
|
||||
} : {}
|
||||
|
||||
await this.lightAction('light.hue.toggle', args, ...lights)
|
||||
await this.refresh(true)
|
||||
},
|
||||
|
||||
async setLight(event) {
|
||||
let lights = Object.keys(this.lights)
|
||||
const light = event.light
|
||||
const args = {}
|
||||
|
||||
if (light) {
|
||||
args.lights = [light.name]
|
||||
lights = [light]
|
||||
}
|
||||
|
||||
const self = this
|
||||
const requests = Object.entries(event.value).map(([attr, value]) => {
|
||||
let method = null;
|
||||
args.value = value
|
||||
|
||||
switch (attr) {
|
||||
case 'brightness':
|
||||
method = 'light.hue.bri'
|
||||
break
|
||||
|
||||
case 'temperature':
|
||||
method = 'light.hue.ct'
|
||||
break
|
||||
|
||||
case 'xy':
|
||||
method = 'light.hue.xy'
|
||||
break
|
||||
}
|
||||
|
||||
if (method)
|
||||
return self.lightAction(method, args, ...lights)
|
||||
}).filter((req) => req != null)
|
||||
|
||||
await Promise.all(requests)
|
||||
await this.refresh(true)
|
||||
},
|
||||
|
||||
async setGroup(event) {
|
||||
if (!event.groupId)
|
||||
return this.setLight(event)
|
||||
|
||||
const group = this.groups[event.groupId]
|
||||
const args = {
|
||||
groups: [group.name],
|
||||
}
|
||||
|
||||
const self = this
|
||||
const requests = Object.entries(event.value).map(([attr, value]) => {
|
||||
let method = null;
|
||||
args.value = value
|
||||
|
||||
switch (attr) {
|
||||
case 'brightness':
|
||||
method = 'light.hue.bri'
|
||||
break
|
||||
|
||||
case 'temperature':
|
||||
method = 'light.hue.ct'
|
||||
break
|
||||
|
||||
case 'xy':
|
||||
method = 'light.hue.xy'
|
||||
break
|
||||
}
|
||||
|
||||
if (method)
|
||||
return self.lightAction(method, args, group)
|
||||
}).filter((req) => req != null)
|
||||
|
||||
await Promise.all(requests)
|
||||
await this.refresh(true)
|
||||
},
|
||||
|
||||
async setScene(event) {
|
||||
await this.groupAction('light.hue.scene', {
|
||||
name: this.scenes[event.sceneId].name,
|
||||
groups: [this.groups[event.groupId].name],
|
||||
}, this.groups[event.groupId])
|
||||
|
||||
await this.refresh(true)
|
||||
},
|
||||
|
||||
async refresh(background) {
|
||||
if (!background)
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
[this.lights, this.groups, this.scenes] = await Promise.all([
|
||||
this.getLights(),
|
||||
|
@ -105,28 +194,10 @@ export default {
|
|||
this.getScenes(),
|
||||
])
|
||||
} finally {
|
||||
if (!background)
|
||||
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() {
|
||||
|
|
36
platypush/backend/http/webapp/src/style/mixins.scss
Normal file
36
platypush/backend/http/webapp/src/style/mixins.scss
Normal file
|
@ -0,0 +1,36 @@
|
|||
@mixin appearance($value) {
|
||||
-webkit-appearance: $value;
|
||||
-ms-appearance: $value;
|
||||
-o-appearance: $value;
|
||||
-ms-appearance: $value;
|
||||
appearance: $value;
|
||||
}
|
||||
|
||||
@mixin transition($value) {
|
||||
-webkit-transition: $value;
|
||||
-ms-transition: $value;
|
||||
-o-transition: $value;
|
||||
-ms-transition: $value;
|
||||
transition: $value;
|
||||
}
|
||||
|
||||
@mixin animation($value) {
|
||||
-webkit-animation: $value;
|
||||
-ms-animation: $value;
|
||||
-o-animation: $value;
|
||||
-ms-animation: $value;
|
||||
animation: $value;
|
||||
}
|
||||
|
||||
@mixin box-shadow($value) {
|
||||
-webkit-box-shadow: $value;
|
||||
-o-box-shadow: $value;
|
||||
-ms-box-shadow: $value;
|
||||
box-shadow: $value;
|
||||
}
|
||||
|
||||
@mixin calc($property, $expression) {
|
||||
#{$property}: -webkit-calc( #{$expression} );
|
||||
#{$property}: -moz-calc( #{$expression} );
|
||||
#{$property}: calc( #{$expression} );
|
||||
}
|
|
@ -41,7 +41,7 @@ $modal-body-bg: white !default;
|
|||
|
||||
/// Selected
|
||||
$selected-bg: #c8ffd0 !default;
|
||||
$selected-fg: #426448;
|
||||
$selected-fg: #32b646;
|
||||
$selected-border: 1px solid #98cfa0 !default;
|
||||
|
||||
/// Links
|
||||
|
@ -84,4 +84,11 @@ $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);
|
||||
$toggle-selected-dot-bg: white;
|
||||
|
||||
//// Slider element
|
||||
$slider-bg: #e4e4e4 !default;
|
||||
$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;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<Nav :panels="components" :selected-panel="selectedPanel" :hostname="hostname"
|
||||
@select="selectedPanel = $event" v-else />
|
||||
|
||||
<div class="panel-container">
|
||||
<div class="canvas">
|
||||
<div class="panel" v-for="(panel, name) in components" :key="name">
|
||||
<component :is="panel.component" :config="panel.config" :plugin-name="name" v-if="name === selectedPanel" />
|
||||
</div>
|
||||
|
@ -99,10 +99,10 @@ main {
|
|||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
.panel-container {
|
||||
.canvas {
|
||||
display: flex;
|
||||
flex-grow: 100;
|
||||
}
|
||||
background: $menu-panel-bg;
|
||||
|
||||
.panel {
|
||||
width: 100%;
|
||||
|
@ -112,6 +112,7 @@ main {
|
|||
box-shadow: none !important;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ module.exports = {
|
|||
sass: {
|
||||
additionalData: `
|
||||
@import '~bulma';
|
||||
@import "@/style/mixins.scss";
|
||||
@import "@/style/themes/light.scss";
|
||||
@import "@/style/layout.scss";
|
||||
@import "@/style/components.scss";
|
||||
|
|
|
@ -154,7 +154,13 @@ class LightHuePlugin(LightPlugin):
|
|||
|
||||
"""
|
||||
|
||||
return self.bridge.get_scene()
|
||||
return {
|
||||
id: {
|
||||
'id': id,
|
||||
**scene,
|
||||
}
|
||||
for id, scene in self.bridge.get_scene().items()
|
||||
}
|
||||
|
||||
@action
|
||||
def get_lights(self):
|
||||
|
@ -195,7 +201,13 @@ class LightHuePlugin(LightPlugin):
|
|||
|
||||
"""
|
||||
|
||||
return self.bridge.get_light()
|
||||
return {
|
||||
id: {
|
||||
'id': id,
|
||||
**light,
|
||||
}
|
||||
for id, light in self.bridge.get_light().items()
|
||||
}
|
||||
|
||||
@action
|
||||
def get_groups(self):
|
||||
|
@ -247,7 +259,13 @@ class LightHuePlugin(LightPlugin):
|
|||
|
||||
"""
|
||||
|
||||
return self.bridge.get_group()
|
||||
return {
|
||||
id: {
|
||||
'id': id,
|
||||
**group,
|
||||
}
|
||||
for id, group in self.bridge.get_group().items()
|
||||
}
|
||||
|
||||
@action
|
||||
def get_animations(self):
|
||||
|
|
Loading…
Reference in a new issue