forked from platypush/platypush
Webpanel refactoring in progress
This commit is contained in:
parent
897338399f
commit
01b111f436
28 changed files with 660 additions and 44 deletions
|
@ -2,7 +2,6 @@ import os
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from platypush.backend.http.utils import HttpUtils
|
|
||||||
from platypush.backend.http.app.utils import get_routes
|
from platypush.backend.http.app.utils import get_routes
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,5 +15,6 @@
|
||||||
|
|
||||||
@import 'common/elements/button';
|
@import 'common/elements/button';
|
||||||
@import 'common/elements/switch';
|
@import 'common/elements/switch';
|
||||||
|
@import 'common/elements/range-slider';
|
||||||
@import 'common/elements/slider';
|
@import 'common/elements/slider';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
@supports (--css: variables) {
|
||||||
|
.input-range-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"].multirange {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
opacity: 1 !important;
|
||||||
|
|
||||||
|
&.original {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
transform: scale(1); /* FF doesn't apply position it seems */
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-track {
|
||||||
|
border-color: transparent; /* needed to switch FF to "styleable" control */
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ghost {
|
||||||
|
position: relative;
|
||||||
|
background: var(--track-background);
|
||||||
|
--track-background: linear-gradient(to right,
|
||||||
|
transparent var(--low), var(--range-color) 0,
|
||||||
|
var(--range-color) var(--high), transparent 0
|
||||||
|
) no-repeat 0 45% / 100% 40%;
|
||||||
|
--range-color: $slider-thumb-bg;
|
||||||
|
|
||||||
|
&::-webkit-slider-runnable-track {
|
||||||
|
background: var(--track-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-track {
|
||||||
|
background: var(--track-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled]::-webkit-slider-thumb {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,11 +6,6 @@
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: $slider-bg;
|
background: $slider-bg;
|
||||||
outline: none;
|
outline: none;
|
||||||
opacity: 0.7;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-slider-thumb {
|
&::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
@ -26,6 +21,10 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
&::-moz-range-thumb {
|
&::-moz-range-thumb {
|
||||||
width: 25px;
|
width: 25px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
|
|
|
@ -29,8 +29,11 @@ $widths: (
|
||||||
}
|
}
|
||||||
|
|
||||||
@if $i < 12 {
|
@if $i < 12 {
|
||||||
.col-offset-#{$i} {
|
.col-offset-#{$i}:first-child {
|
||||||
margin-left: (8.66666666667%*$i);
|
margin-left: (8.66666666667%*$i) !important;
|
||||||
|
}
|
||||||
|
.col-offset-#{$i}:not(first-child) {
|
||||||
|
margin-left: 4% + (8.66666666667%*$i) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,11 @@ $default-link-fg: #5f7869 !default;
|
||||||
$selected-bg: #c8ffd0 !default;
|
$selected-bg: #c8ffd0 !default;
|
||||||
$hover-bg: #def6ea !default;
|
$hover-bg: #def6ea !default;
|
||||||
$header-bg: $default_bg !default;
|
$header-bg: $default_bg !default;
|
||||||
|
|
||||||
$nav-bg: #e8e8e8 !default;
|
$nav-bg: #e8e8e8 !default;
|
||||||
$nav-fg: $default-link-fg;
|
$nav-fg: $default-link-fg;
|
||||||
|
$nav-date-time-shadow: 2px 2px 2px #ccc !default;
|
||||||
|
|
||||||
$modal-bg: #f0f0f0 !default;
|
$modal-bg: #f0f0f0 !default;
|
||||||
|
|
||||||
//// Switch element
|
//// Switch element
|
||||||
|
@ -42,7 +45,9 @@ $switch-shadow-glow-checked-2: inset 0 0 0 5px #00e094, inset 0 0 0 14px #fff !d
|
||||||
|
|
||||||
//// Slier element
|
//// Slier element
|
||||||
$slider-bg: #e4e4e4 !default;
|
$slider-bg: #e4e4e4 !default;
|
||||||
$slider-thumb-bg: #4caf50 !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;
|
||||||
|
|
||||||
//// Header style
|
//// Header style
|
||||||
$header-bottom: $default-bottom;
|
$header-bottom: $default-bottom;
|
||||||
|
|
|
@ -2,6 +2,7 @@ nav {
|
||||||
margin-bottom: 1.2rem;
|
margin-bottom: 1.2rem;
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
position: relative;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
|
@ -35,6 +36,14 @@ nav {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-time {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
margin-right: .7rem;
|
||||||
|
font-size: 14pt;
|
||||||
|
text-shadow: $nav-date-time-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
.decorator {
|
.decorator {
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
@ -55,6 +64,10 @@ nav {
|
||||||
li.selected {
|
li.selected {
|
||||||
border-radius: 2rem;
|
border-radius: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-time {
|
||||||
|
@extend .hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,16 @@
|
||||||
line-height: 3.8rem;
|
line-height: 3.8rem;
|
||||||
letter-spacing: .1rem;
|
letter-spacing: .1rem;
|
||||||
|
|
||||||
|
%panel {
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
padding: 1.5rem;
|
||||||
|
font-weight: 100;
|
||||||
|
border: $default-border-2;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
background: $light-hue-properties-bg;
|
||||||
|
box-shadow: $light-hue-properties-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
.groups,
|
.groups,
|
||||||
.scenes,
|
.scenes,
|
||||||
.units {
|
.units {
|
||||||
|
@ -29,6 +39,7 @@
|
||||||
.group,
|
.group,
|
||||||
.scene,
|
.scene,
|
||||||
.unit,
|
.unit,
|
||||||
|
.animations,
|
||||||
.group-controller {
|
.group-controller {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -48,13 +59,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
* > .properties {
|
* > .properties {
|
||||||
margin: 1.5rem auto;
|
@extend %panel;
|
||||||
padding: 1.5rem;
|
|
||||||
font-weight: 100;
|
|
||||||
border: $default-border-2;
|
|
||||||
border-radius: 1.5rem;
|
|
||||||
background: $light-hue-properties-bg;
|
|
||||||
box-shadow: $light-hue-properties-shadow;
|
|
||||||
|
|
||||||
.slider-container {
|
.slider-container {
|
||||||
@extend .vertical-center;
|
@extend .vertical-center;
|
||||||
|
@ -90,6 +95,54 @@
|
||||||
.group-controller {
|
.group-controller {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.animations {
|
||||||
|
.row {
|
||||||
|
.caption {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-container {
|
||||||
|
@extend %panel;
|
||||||
|
cursor: auto;
|
||||||
|
|
||||||
|
.animation {
|
||||||
|
.row {
|
||||||
|
padding: 1rem .3333rem;
|
||||||
|
&:hover {
|
||||||
|
background: $hover-bg;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
|
||||||
|
* > input[type=range] {
|
||||||
|
background: $slider-hover-on-hover-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select[name=animation-type] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* > .input-range-container {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: -1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
* > input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.row {
|
||||||
|
.animation-container {
|
||||||
|
background: $light-hue-properties-hover-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.groups {
|
.groups {
|
||||||
|
|
|
@ -35,11 +35,17 @@ var app = new Vue({
|
||||||
return {
|
return {
|
||||||
config: window.config,
|
config: window.config,
|
||||||
selectedPlugin: undefined,
|
selectedPlugin: undefined,
|
||||||
|
now: new Date(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted: function() {},
|
mounted: function() {},
|
||||||
created: function() {},
|
created: function() {
|
||||||
|
const self = this;
|
||||||
|
setInterval(() => {
|
||||||
|
self.now = new Date();
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
updated: function() {},
|
updated: function() {},
|
||||||
destroyed: function() {},
|
destroyed: function() {},
|
||||||
});
|
});
|
||||||
|
|
89
platypush/backend/http/static/js/elements/range-slider.js
Normal file
89
platypush/backend/http/static/js/elements/range-slider.js
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
Vue.component('range-slider', {
|
||||||
|
template: '#tmpl-range-slider',
|
||||||
|
props: ['min','max','value'],
|
||||||
|
|
||||||
|
mounted: function() {
|
||||||
|
var input = this.$el.querySelector('input[type=range]');
|
||||||
|
var supportsMultiple = self.HTMLInputElement && "valueLow" in HTMLInputElement.prototype;
|
||||||
|
var descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
|
||||||
|
|
||||||
|
if (supportsMultiple || input.classList.contains("multirange")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = [];
|
||||||
|
if (Array.isArray(this.value)) {
|
||||||
|
values = this.value;
|
||||||
|
} else if (this.value !== null) {
|
||||||
|
values = this.value.split(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
var min = +(this.min || 0);
|
||||||
|
var max = +(this.max || 100);
|
||||||
|
var ghost = input.cloneNode();
|
||||||
|
|
||||||
|
input.classList.add("multirange", "original");
|
||||||
|
ghost.classList.add("multirange", "ghost");
|
||||||
|
|
||||||
|
input.value = values[0] !== undefined ? values[0] : min + (max - min) / 2;
|
||||||
|
ghost.value = values[1] !== undefined ? values[1] : min + (max - min) / 2;
|
||||||
|
|
||||||
|
input.parentNode.insertBefore(ghost, input.nextSibling);
|
||||||
|
|
||||||
|
Object.defineProperty(input, "originalValue", descriptor.get ? descriptor : {
|
||||||
|
// Fuck you Safari >:(
|
||||||
|
get: function() { return this.value; },
|
||||||
|
set: function(v) { this.value = v; }
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperties(input, {
|
||||||
|
valueLow: {
|
||||||
|
get: function() { return Math.min(this.originalValue, ghost.value); },
|
||||||
|
set: function(v) { this.originalValue = v; },
|
||||||
|
enumerable: true
|
||||||
|
},
|
||||||
|
valueHigh: {
|
||||||
|
get: function() { return Math.max(this.originalValue, ghost.value); },
|
||||||
|
set: function(v) { ghost.value = v; },
|
||||||
|
enumerable: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (descriptor.get) {
|
||||||
|
// Again, fuck you Safari
|
||||||
|
Object.defineProperty(input, "value", {
|
||||||
|
get: function() { return this.valueLow + "," + this.valueHigh; },
|
||||||
|
set: function(v) {
|
||||||
|
var values = v.split(",");
|
||||||
|
this.valueLow = values[0];
|
||||||
|
this.valueHigh = values[1];
|
||||||
|
update();
|
||||||
|
},
|
||||||
|
enumerable: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
input.oninput = this.changed;
|
||||||
|
ghost.oninput = this.changed;
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
ghost.style.setProperty("--low", 100 * ((input.valueLow - min) / (max - min)) + 1 + "%");
|
||||||
|
ghost.style.setProperty("--high", 100 * ((input.valueHigh - min) / (max - min)) - 1 + "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener("input", update);
|
||||||
|
ghost.addEventListener("input", update);
|
||||||
|
|
||||||
|
update();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
changed: function(event) {
|
||||||
|
const value = this.$el.querySelectorAll('input[type=range]')[0].value
|
||||||
|
.split(',').map(_ => parseFloat(_));
|
||||||
|
|
||||||
|
this.$emit('changed', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
Vue.component('light-hue-animations-container', {
|
||||||
|
template: '#tmpl-light-hue-animations-container',
|
||||||
|
props: ['groupId','animation','collapsed'],
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
selectedAnimation: 'color_transition',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
animationsCollapsedToggled: function() {
|
||||||
|
this.$emit('animations-collapsed-toggled', {
|
||||||
|
type: 'animation',
|
||||||
|
id: this.groupId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
toggled: async function(event) {
|
||||||
|
if (event.value) {
|
||||||
|
var args = {
|
||||||
|
...this.$refs[this.selectedAnimation].value,
|
||||||
|
animation: this.selectedAnimation,
|
||||||
|
groups: [this.groupId],
|
||||||
|
}
|
||||||
|
|
||||||
|
await request('light.hue.on', {groups: [this.groupId]});
|
||||||
|
await request('light.hue.animate', args);
|
||||||
|
|
||||||
|
this.$emit('animation-started', {
|
||||||
|
...this.$refs[this.selectedAnimation].value,
|
||||||
|
type: this.selectedAnimation,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await request('light.hue.stop_animation');
|
||||||
|
this.$emit('animation-stopped', {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
Vue.component('light-hue-animation-blink', {
|
||||||
|
template: '#tmpl-light-hue-animation-blink',
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
value: {
|
||||||
|
transition_seconds: 1,
|
||||||
|
duration: undefined,
|
||||||
|
},
|
||||||
|
transitionSecondsRange: [0.1, 60],
|
||||||
|
durationRange: [0, 600],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onTransitionSecondsChange: function(event) {
|
||||||
|
this.value.transition_seconds = event.target.value;
|
||||||
|
},
|
||||||
|
onDurationChanged: function(event) {
|
||||||
|
var value = event.target.value;
|
||||||
|
if (value == null || value.length === 0 || parseFloat(value) == 0) {
|
||||||
|
value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.duration = value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
Vue.component('light-hue-animation-color_transition', {
|
||||||
|
template: '#tmpl-light-hue-animation-color_transition',
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
value: {
|
||||||
|
hue_range: [0,65535],
|
||||||
|
sat_range: [150,255],
|
||||||
|
bri_range: [190,255],
|
||||||
|
hue_step: 150,
|
||||||
|
sat_step: 5,
|
||||||
|
bri_step: 2,
|
||||||
|
transition_seconds: 1,
|
||||||
|
duration: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
hueStepRange: function() {
|
||||||
|
return [1, parseInt((this.value.hue_range[1]-this.value.hue_range[0])/2)-1];
|
||||||
|
},
|
||||||
|
satStepRange: function() {
|
||||||
|
return [1, parseInt((this.value.sat_range[1]-this.value.sat_range[0])/2)-1];
|
||||||
|
},
|
||||||
|
briStepRange: function() {
|
||||||
|
return [1, parseInt((this.value.bri_range[1]-this.value.bri_range[0])/2)-1];
|
||||||
|
},
|
||||||
|
transitionSecondsRange: function() {
|
||||||
|
return [0.1, 60];
|
||||||
|
},
|
||||||
|
durationRange: function() {
|
||||||
|
return [0, 600];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
hueRangeChanged: function(value) {
|
||||||
|
this.value.hue_range = value;
|
||||||
|
},
|
||||||
|
satRangeChanged: function(value) {
|
||||||
|
this.value.sat_range = value;
|
||||||
|
},
|
||||||
|
briRangeChanged: function(value) {
|
||||||
|
this.value.bri_range = value;
|
||||||
|
},
|
||||||
|
onTransitionSecondsChange: function(event) {
|
||||||
|
this.value.transition_seconds = event.target.value;
|
||||||
|
},
|
||||||
|
onDurationChanged: function(event) {
|
||||||
|
var value = event.target.value;
|
||||||
|
if (value == null || value.length === 0 || parseFloat(value) == 0) {
|
||||||
|
value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.duration = value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@ Vue.component('light-hue', {
|
||||||
groups: {},
|
groups: {},
|
||||||
lights: {},
|
lights: {},
|
||||||
scenes: {},
|
scenes: {},
|
||||||
|
animations: {},
|
||||||
selectedGroup: undefined,
|
selectedGroup: undefined,
|
||||||
selectedScene: undefined,
|
selectedScene: undefined,
|
||||||
selectedProperties: {
|
selectedProperties: {
|
||||||
|
@ -89,8 +90,10 @@ Vue.component('light-hue', {
|
||||||
const getLights = request('light.hue.get_lights');
|
const getLights = request('light.hue.get_lights');
|
||||||
const getGroups = request('light.hue.get_groups');
|
const getGroups = request('light.hue.get_groups');
|
||||||
const getScenes = request('light.hue.get_scenes');
|
const getScenes = request('light.hue.get_scenes');
|
||||||
|
const getAnimations = request('light.hue.get_animations');
|
||||||
|
|
||||||
[this.lights, this.groups, this.scenes] = await Promise.all([getLights, getGroups, getScenes]);
|
[this.lights, this.groups, this.scenes, this.animations] = await Promise.all(
|
||||||
|
[getLights, getGroups, getScenes, getAnimations]);
|
||||||
|
|
||||||
this._prepareGroups();
|
this._prepareGroups();
|
||||||
this._prepareScenes();
|
this._prepareScenes();
|
||||||
|
@ -112,6 +115,14 @@ Vue.component('light-hue', {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
startedAnimation: function(value) {
|
||||||
|
this.animations.groups[this.selectedGroup] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
stoppedAnimation: function() {
|
||||||
|
this.animations.groups[this.selectedGroup] = undefined;
|
||||||
|
},
|
||||||
|
|
||||||
selectScene: async function(event) {
|
selectScene: async function(event) {
|
||||||
await request(
|
await request(
|
||||||
'light.hue.scene', {
|
'light.hue.scene', {
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
{% include 'elements/switch.html' %}
|
{% include 'elements/switch.html' %}
|
||||||
|
{% include 'elements/range-slider.html' %}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<script type="text/x-template" id="tmpl-range-slider">
|
||||||
|
<div class="input-range-container">
|
||||||
|
<input type="range" class="slider" multiple="multiple" :value="value" :min="min" :max="max" @input="changed">
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="application/javascript" src="{{ url_for('static', filename='js/elements/range-slider.js') }}"></script>
|
||||||
|
|
|
@ -5,3 +5,5 @@
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script type="application/javascript" src="{{ url_for('static', filename='js/elements/switch.js') }}"></script>
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
{% include 'header.html' %}
|
{# include 'header.html' #}
|
||||||
|
|
||||||
{% with plugins=templates.keys() %}
|
{% with plugins=templates.keys() %}
|
||||||
{% include 'nav.html' %}
|
{% include 'nav.html' %}
|
||||||
|
@ -67,7 +67,6 @@
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename=script['_script_file']) }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename=script['_script_file']) }}"></script>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/elements.js') }}"></script>
|
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/application.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename='js/application.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<nav>
|
<nav>
|
||||||
<ul class="row">
|
<ul>
|
||||||
{% for plugin in plugins|sort %}
|
{% for plugin in plugins|sort %}
|
||||||
<li :class="{selected: '{{ plugin }}' == selectedPlugin}">
|
<li :class="{selected: '{{ plugin }}' == selectedPlugin}">
|
||||||
<a href="#{{ plugin }}" @click="selectedPlugin = '{{ plugin }}'">
|
<a href="#{{ plugin }}" @click="selectedPlugin = '{{ plugin }}'">
|
||||||
|
@ -15,6 +15,11 @@
|
||||||
<li>
|
<li>
|
||||||
<a href="#{{ plugin }}">Test tab 3</a>
|
<a href="#{{ plugin }}">Test tab 3</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<div class="date-time pull-right">
|
||||||
|
<!--<div class="date" v-text="now.toDateString().substring(0,10)"></div>-->
|
||||||
|
<div class="time" v-text="now.toTimeString().substring(0,8)"></div>
|
||||||
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
{% with templates = utils.find_templates_in_dir('plugins/light.hue/animations') %}
|
||||||
|
{% for template in templates %}
|
||||||
|
{% include template %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<script type="text/x-template" id="tmpl-light-hue-animations-container">
|
||||||
|
<div class="animations">
|
||||||
|
<div class="row vertical-center">
|
||||||
|
<div class="col-10 caption" @click="animationsCollapsedToggled">Animate</div>
|
||||||
|
<div class="col-2 pull-right">
|
||||||
|
<toggle-switch :glow="true" :value="animation != null" @toggled="toggled"></toggle-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row" :class="{hidden: collapsed}">
|
||||||
|
<div class="row">
|
||||||
|
{% with templates = utils.find_templates_in_dir('plugins/light.hue/animations') %}
|
||||||
|
<select name="animation-type" v-model="selectedAnimation"
|
||||||
|
v-if="{{ templates|length }} > 0">
|
||||||
|
{% for template_file in templates %}
|
||||||
|
{% with name = template_file.split('/')[-1].split('.')[0] %}
|
||||||
|
<option value="{{ name }}">{{ name }}</option>
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for template_file in templates %}
|
||||||
|
{% with name = template_file.split('/')[-1].split('.')[0] %}
|
||||||
|
<div class="animation-container" :class="{hidden: selectedAnimation != '{{name}}'}">
|
||||||
|
<component :is="'light-hue-animation-{{ name }}'" ref="{{ name }}"></component>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/light.hue/animations.js') }}"></script>
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script type="text/x-template" id="tmpl-light-hue-animation-blink">
|
||||||
|
<div class="animation">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">Transition</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<input type="text" v-model="value.transition_seconds">
|
||||||
|
</div>
|
||||||
|
<div class="col-6 slider-container">
|
||||||
|
<input class="slider" type="range" :min="transitionSecondsRange[0]" :max="transitionSecondsRange[1]"
|
||||||
|
v-model="value.transition_seconds" @input="onTransitionSecondsChange" placeholder="secs">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">Duration</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<input type="text" v-model="value.duration" @input="onDurationChanged">
|
||||||
|
</div>
|
||||||
|
<div class="col-6 slider-container">
|
||||||
|
<input class="slider" type="range" step="1" :min="durationRange[0]" :max="durationRange[1]" placeholder="secs"
|
||||||
|
:class="{disabled: value.duration == undefined}" v-model="value.duration" @input="onDurationChanged">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/light.hue/animations/blink.js') }}"></script>
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script type="text/x-template" id="tmpl-light-hue-animation-color_transition">
|
||||||
|
<div class="animation">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">Hue range</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<range-slider min="0" max="65535" v-model="value.hue_range" @changed="hueRangeChanged"></range-slider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">Sat range</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<range-slider min="0" max="255" v-model="value.sat_range" @changed="satRangeChanged"></range-slider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">Bri range</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<range-slider min="0" max="255" v-model="value.bri_range" @changed="briRangeChanged"></range-slider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">Hue step</div>
|
||||||
|
<div class="col-8 slider-container">
|
||||||
|
<input class="slider" type="range" :min="hueStepRange[0]" :max="hueStepRange[1]" step="1"
|
||||||
|
v-model="value.hue_step">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">Sat step</div>
|
||||||
|
<div class="col-8 slider-container">
|
||||||
|
<input class="slider" type="range" :min="satStepRange[0]" :max="satStepRange[1]" step="1"
|
||||||
|
v-model="value.sat_step">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">Bri step</div>
|
||||||
|
<div class="col-8 slider-container">
|
||||||
|
<input class="slider" type="range" step="1" :min="briStepRange[0]" :max="briStepRange[1]"
|
||||||
|
v-model="value.bri_step">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">Transition</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<input type="text" v-model="value.transition_seconds">
|
||||||
|
</div>
|
||||||
|
<div class="col-6 slider-container">
|
||||||
|
<input class="slider" type="range" :min="transitionSecondsRange[0]" :max="transitionSecondsRange[1]"
|
||||||
|
v-model="value.transition_seconds" @input="onTransitionSecondsChange" placeholder="secs">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">Duration</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<input type="text" v-model="value.duration" @input="onDurationChanged">
|
||||||
|
</div>
|
||||||
|
<div class="col-6 slider-container">
|
||||||
|
<input class="slider" type="range" step="1" :min="durationRange[0]" :max="durationRange[1]" placeholder="secs"
|
||||||
|
:class="{disabled: value.duration == undefined}" v-model="value.duration" @input="onDurationChanged">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/light.hue/animations/color_transition.js') }}"></script>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
{% include 'plugins/light.hue/groups.html' %}
|
{% include 'plugins/light.hue/groups.html' %}
|
||||||
{% include 'plugins/light.hue/scenes.html' %}
|
{% include 'plugins/light.hue/scenes.html' %}
|
||||||
{% include 'plugins/light.hue/units.html' %}
|
{% include 'plugins/light.hue/units.html' %}
|
||||||
|
{% include 'plugins/light.hue/animations.html' %}
|
||||||
|
|
||||||
<script type="text/x-template" id="tmpl-light-hue">
|
<script type="text/x-template" id="tmpl-light-hue">
|
||||||
<div class="row light-hue-container">
|
<div class="row light-hue-container">
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
|
|
||||||
<div class="units col-no-margin-6 col-s-12">
|
<div class="units col-no-margin-6 col-s-12">
|
||||||
<div class="title">Lights</div>
|
<div class="title">Lights</div>
|
||||||
|
|
||||||
<light-hue-group-controller
|
<light-hue-group-controller
|
||||||
v-if="selectedGroup"
|
v-if="selectedGroup"
|
||||||
v-model="groups[selectedGroup]"
|
v-model="groups[selectedGroup]"
|
||||||
|
@ -43,6 +45,16 @@
|
||||||
@input="updatedGroup">
|
@input="updatedGroup">
|
||||||
</light-hue-group-controller>
|
</light-hue-group-controller>
|
||||||
|
|
||||||
|
<light-hue-animations-container
|
||||||
|
v-if="selectedGroup"
|
||||||
|
:group-id="selectedGroup"
|
||||||
|
:animation="animations.groups[selectedGroup]"
|
||||||
|
:collapsed="!(selectedProperties.type == 'animation' && selectedProperties.id == selectedGroup)"
|
||||||
|
@animations-collapsed-toggled="collapsedToggled"
|
||||||
|
@animation-started="startedAnimation"
|
||||||
|
@animation-stopped="stoppedAnimation">
|
||||||
|
</light-hue-animations-container>
|
||||||
|
|
||||||
<light-hue-unit
|
<light-hue-unit
|
||||||
v-for="(light, id) in lights"
|
v-for="(light, id) in lights"
|
||||||
v-model="light.state"
|
v-model="light.state"
|
||||||
|
|
|
@ -3,6 +3,8 @@ import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
|
from platypush.backend.http.app import template_folder
|
||||||
|
|
||||||
|
|
||||||
class HttpUtils(object):
|
class HttpUtils(object):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -100,5 +102,19 @@ class HttpUtils(object):
|
||||||
def plugin_name_to_tag(cls, module_name):
|
def plugin_name_to_tag(cls, module_name):
|
||||||
return module_name.replace('.','-')
|
return module_name.replace('.','-')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_templates_in_dir(cls, directory):
|
||||||
|
return [
|
||||||
|
os.path.join(directory, file)
|
||||||
|
for root, path, files in os.walk(os.path.abspath(os.path.join(template_folder, directory)))
|
||||||
|
for file in files
|
||||||
|
if file.endswith('.html') or file.endswith('.htm')
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def readfile(cls, file):
|
||||||
|
with open(file) as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -70,12 +70,26 @@ class LightHuePlugin(LightPlugin):
|
||||||
|
|
||||||
self.redis = None
|
self.redis = None
|
||||||
self.animation_thread = None
|
self.animation_thread = None
|
||||||
|
self.animations = {}
|
||||||
|
self._init_animations()
|
||||||
self.logger.info('Configured lights: "{}"'. format(self.lights))
|
self.logger.info('Configured lights: "{}"'. format(self.lights))
|
||||||
|
|
||||||
def _expand_groups(self):
|
def _expand_groups(self):
|
||||||
groups = [g for g in self.bridge.groups if g.name in self.groups]
|
groups = [g for g in self.bridge.groups if g.name in self.groups]
|
||||||
for g in groups:
|
for g in groups:
|
||||||
self.lights.extend([l.name for l in g.lights])
|
for l in g.lights:
|
||||||
|
self.lights += [l.name]
|
||||||
|
|
||||||
|
def _init_animations(self):
|
||||||
|
self.animations = {
|
||||||
|
'groups': {},
|
||||||
|
'lights': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for g in self.bridge.groups:
|
||||||
|
self.animations['groups'][g.group_id] = None
|
||||||
|
for l in self.bridge.lights:
|
||||||
|
self.animations['lights'][l.light_id] = None
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def connect(self):
|
def connect(self):
|
||||||
|
@ -239,6 +253,36 @@ class LightHuePlugin(LightPlugin):
|
||||||
|
|
||||||
return self.bridge.get_group()
|
return self.bridge.get_group()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_animations(self):
|
||||||
|
"""
|
||||||
|
Get the list of running light animations.
|
||||||
|
|
||||||
|
:returns: A dictionary with the following structure:
|
||||||
|
|
||||||
|
{
|
||||||
|
"groups": {
|
||||||
|
"id_1": {
|
||||||
|
"type": "color_transition",
|
||||||
|
"hue_range": [0,65535],
|
||||||
|
"sat_range": [0,255],
|
||||||
|
"bri_range": [0,255],
|
||||||
|
"hue_step": 10,
|
||||||
|
"sat_step": 10,
|
||||||
|
"bri_step": 2,
|
||||||
|
"transition_seconds": 2
|
||||||
|
},
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"lights": {
|
||||||
|
"id_1": { ... },
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.animations
|
||||||
|
|
||||||
def _exec(self, attr, *args, **kwargs):
|
def _exec(self, attr, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
self.connect()
|
self.connect()
|
||||||
|
@ -654,6 +698,7 @@ class LightHuePlugin(LightPlugin):
|
||||||
if self.animation_thread and self.animation_thread.is_alive():
|
if self.animation_thread and self.animation_thread.is_alive():
|
||||||
redis = self._get_redis()
|
redis = self._get_redis()
|
||||||
redis.rpush(self.ANIMATION_CTRL_QUEUE_NAME, 'STOP')
|
redis.rpush(self.ANIMATION_CTRL_QUEUE_NAME, 'STOP')
|
||||||
|
self._init_animations()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def animate(self, animation, duration=None,
|
def animate(self, animation, duration=None,
|
||||||
|
@ -669,27 +714,27 @@ class LightHuePlugin(LightPlugin):
|
||||||
:param duration: Animation duration in seconds (default: None, i.e. continue until stop)
|
:param duration: Animation duration in seconds (default: None, i.e. continue until stop)
|
||||||
:type duration: float
|
:type duration: float
|
||||||
|
|
||||||
:param hue_range: If you selected a color transition, this will specify the hue range of your color transition.
|
:param hue_range: If you selected a color color_transition.html, this will specify the hue range of your color color_transition.html.
|
||||||
Default: [0, 65535]
|
Default: [0, 65535]
|
||||||
:type hue_range: list[int]
|
:type hue_range: list[int]
|
||||||
|
|
||||||
:param sat_range: If you selected a color transition, this will specify the saturation range of your color
|
:param sat_range: If you selected a color color_transition.html, this will specify the saturation range of your color
|
||||||
transition. Default: [0, 255]
|
color_transition.html. Default: [0, 255]
|
||||||
:type sat_range: list[int]
|
:type sat_range: list[int]
|
||||||
|
|
||||||
:param bri_range: If you selected a color transition, this will specify the brightness range of your color
|
:param bri_range: If you selected a color color_transition.html, this will specify the brightness range of your color
|
||||||
transition. Default: [254, 255] :type bri_range: list[int]
|
color_transition.html. Default: [254, 255] :type bri_range: list[int]
|
||||||
|
|
||||||
:param lights: Lights to control (names or light objects). Default: plugin default lights
|
:param lights: Lights to control (names, IDs or light objects). Default: plugin default lights
|
||||||
:param groups: Groups to control (names or group objects). Default: plugin default groups
|
:param groups: Groups to control (names, IDs or group objects). Default: plugin default groups
|
||||||
|
|
||||||
:param hue_step: If you selected a color transition, this will specify by how much the color hue will change
|
:param hue_step: If you selected a color color_transition.html, this will specify by how much the color hue will change
|
||||||
between iterations. Default: 1000 :type hue_step: int
|
between iterations. Default: 1000 :type hue_step: int
|
||||||
|
|
||||||
:param sat_step: If you selected a color transition, this will specify by how much the saturation will change
|
:param sat_step: If you selected a color color_transition.html, this will specify by how much the saturation will change
|
||||||
between iterations. Default: 2 :type sat_step: int
|
between iterations. Default: 2 :type sat_step: int
|
||||||
|
|
||||||
:param bri_step: If you selected a color transition, this will specify by how much the brightness will change
|
:param bri_step: If you selected a color color_transition.html, this will specify by how much the brightness will change
|
||||||
between iterations. Default: 1 :type bri_step: int
|
between iterations. Default: 1 :type bri_step: int
|
||||||
|
|
||||||
:param transition_seconds: Time between two transitions or blinks in seconds. Default: 1.0
|
:param transition_seconds: Time between two transitions or blinks in seconds. Default: 1.0
|
||||||
|
@ -703,13 +748,34 @@ class LightHuePlugin(LightPlugin):
|
||||||
if hue_range is None:
|
if hue_range is None:
|
||||||
hue_range = [0, self.MAX_HUE]
|
hue_range = [0, self.MAX_HUE]
|
||||||
if groups:
|
if groups:
|
||||||
groups = [g for g in self.bridge.groups if g.name in groups]
|
groups = [g for g in self.bridge.groups if g.name in groups or g.group_id in groups]
|
||||||
lights = lights or []
|
lights = lights or []
|
||||||
for g in groups:
|
for g in groups:
|
||||||
lights.extend([l.name for l in g.lights])
|
lights.extend([l.name for l in g.lights])
|
||||||
elif not lights:
|
elif lights:
|
||||||
|
lights = [l.name for l in self.bridge.lights if l.name in lights or l.light_id in lights]
|
||||||
|
else:
|
||||||
lights = self.lights
|
lights = self.lights
|
||||||
|
|
||||||
|
info = {
|
||||||
|
'type': animation,
|
||||||
|
'duration': duration,
|
||||||
|
'hue_range': hue_range,
|
||||||
|
'sat_range': sat_range,
|
||||||
|
'bri_range': bri_range,
|
||||||
|
'hue_step': hue_step,
|
||||||
|
'sat_step': sat_step,
|
||||||
|
'bri_step': bri_step,
|
||||||
|
'transition_seconds': transition_seconds,
|
||||||
|
}
|
||||||
|
|
||||||
|
for g in groups:
|
||||||
|
self.animations['groups'][g.group_id] = info
|
||||||
|
|
||||||
|
for l in self.bridge.lights:
|
||||||
|
if l.name in lights:
|
||||||
|
self.animations['lights'][l.light_id] = info
|
||||||
|
|
||||||
def _initialize_light_attrs(lights):
|
def _initialize_light_attrs(lights):
|
||||||
if animation == self.Animation.COLOR_TRANSITION:
|
if animation == self.Animation.COLOR_TRANSITION:
|
||||||
return { l: {
|
return { l: {
|
||||||
|
|
10
setup.py
10
setup.py
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import distutils.cmd
|
import distutils.cmd
|
||||||
from distutils.command.build import build
|
from distutils.command.build import build
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
@ -15,8 +16,8 @@ class WebBuildCommand(distutils.cmd.Command):
|
||||||
description = 'Build components and styles for the web pages'
|
description = 'Build components and styles for the web pages'
|
||||||
user_options = []
|
user_options = []
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def generate_css_files():
|
def generate_css_files(cls):
|
||||||
from scss import Compiler
|
from scss import Compiler
|
||||||
|
|
||||||
print('Building CSS files')
|
print('Building CSS files')
|
||||||
|
@ -36,8 +37,13 @@ class WebBuildCommand(distutils.cmd.Command):
|
||||||
|
|
||||||
with open(css_file, 'w') as f:
|
with open(css_file, 'w') as f:
|
||||||
css_content = Compiler(output_style='compressed', search_path=[root, input_path]).compile(scss_file)
|
css_content = Compiler(output_style='compressed', search_path=[root, input_path]).compile(scss_file)
|
||||||
|
css_content = cls._fix_css4_vars(css_content)
|
||||||
f.write(css_content)
|
f.write(css_content)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fix_css4_vars(css):
|
||||||
|
return re.sub(r'var\("--([^"]+)"\)', r'var(--\1)', css)
|
||||||
|
|
||||||
def initialize_options(self):
|
def initialize_options(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue