Webpanel refactoring in progress
This commit is contained in:
parent
439548de1b
commit
fc08ac7794
28 changed files with 660 additions and 44 deletions
|
@ -2,7 +2,6 @@ import os
|
|||
|
||||
from flask import Flask
|
||||
|
||||
from platypush.backend.http.utils import HttpUtils
|
||||
from platypush.backend.http.app.utils import get_routes
|
||||
|
||||
|
||||
|
|
|
@ -15,5 +15,6 @@
|
|||
|
||||
@import 'common/elements/button';
|
||||
@import 'common/elements/switch';
|
||||
@import 'common/elements/range-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;
|
||||
background: $slider-bg;
|
||||
outline: none;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
|
@ -26,6 +21,10 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
|
|
|
@ -29,8 +29,11 @@ $widths: (
|
|||
}
|
||||
|
||||
@if $i < 12 {
|
||||
.col-offset-#{$i} {
|
||||
margin-left: (8.66666666667%*$i);
|
||||
.col-offset-#{$i}:first-child {
|
||||
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;
|
||||
$hover-bg: #def6ea !default;
|
||||
$header-bg: $default_bg !default;
|
||||
|
||||
$nav-bg: #e8e8e8 !default;
|
||||
$nav-fg: $default-link-fg;
|
||||
$nav-date-time-shadow: 2px 2px 2px #ccc !default;
|
||||
|
||||
$modal-bg: #f0f0f0 !default;
|
||||
|
||||
//// 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
|
||||
$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-bottom: $default-bottom;
|
||||
|
|
|
@ -2,6 +2,7 @@ nav {
|
|||
margin-bottom: 1.2rem;
|
||||
|
||||
ul {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
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 {
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
@ -55,6 +64,10 @@ nav {
|
|||
li.selected {
|
||||
border-radius: 2rem;
|
||||
}
|
||||
|
||||
.date-time {
|
||||
@extend .hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,16 @@
|
|||
line-height: 3.8rem;
|
||||
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,
|
||||
.scenes,
|
||||
.units {
|
||||
|
@ -29,6 +39,7 @@
|
|||
.group,
|
||||
.scene,
|
||||
.unit,
|
||||
.animations,
|
||||
.group-controller {
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
|
@ -48,13 +59,7 @@
|
|||
}
|
||||
|
||||
* > .properties {
|
||||
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;
|
||||
@extend %panel;
|
||||
|
||||
.slider-container {
|
||||
@extend .vertical-center;
|
||||
|
@ -90,6 +95,54 @@
|
|||
.group-controller {
|
||||
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 {
|
||||
|
|
|
@ -35,11 +35,17 @@ var app = new Vue({
|
|||
return {
|
||||
config: window.config,
|
||||
selectedPlugin: undefined,
|
||||
now: new Date(),
|
||||
};
|
||||
},
|
||||
|
||||
mounted: function() {},
|
||||
created: function() {},
|
||||
created: function() {
|
||||
const self = this;
|
||||
setInterval(() => {
|
||||
self.now = new Date();
|
||||
}, 1000)
|
||||
},
|
||||
updated: 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: {},
|
||||
lights: {},
|
||||
scenes: {},
|
||||
animations: {},
|
||||
selectedGroup: undefined,
|
||||
selectedScene: undefined,
|
||||
selectedProperties: {
|
||||
|
@ -89,8 +90,10 @@ Vue.component('light-hue', {
|
|||
const getLights = request('light.hue.get_lights');
|
||||
const getGroups = request('light.hue.get_groups');
|
||||
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._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) {
|
||||
await request(
|
||||
'light.hue.scene', {
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
{% 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>
|
||||
</script>
|
||||
|
||||
<script type="application/javascript" src="{{ url_for('static', filename='js/elements/switch.js') }}"></script>
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<script type="text/x-template" id="tmpl-app-header">
|
||||
<header class="s-hidden m-hidden">
|
||||
<div class="row">
|
||||
<div class="logo col-9">
|
||||
<span class="logo-1">Platypush</span>
|
||||
<span class="logo-2">Web Panel</span>
|
||||
</div>
|
||||
<div class="logo col-9">
|
||||
<span class="logo-1">Platypush</span>
|
||||
<span class="logo-2">Web Panel</span>
|
||||
</div>
|
||||
|
||||
<div class="date-time col-3">
|
||||
<div class="date" v-text="now.toDateString().substring(0,10)"></div>
|
||||
<div class="time" v-text="now.toTimeString().substring(0,8)"></div>
|
||||
</div>
|
||||
<div class="date-time col-3">
|
||||
<div class="date" v-text="now.toDateString().substring(0,10)"></div>
|
||||
<div class="time" v-text="now.toTimeString().substring(0,8)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</script>
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
<body>
|
||||
<div id="app">
|
||||
{% include 'header.html' %}
|
||||
{# include 'header.html' #}
|
||||
|
||||
{% with plugins=templates.keys() %}
|
||||
{% include 'nav.html' %}
|
||||
|
@ -67,7 +67,6 @@
|
|||
<script type="text/javascript" src="{{ url_for('static', filename=script['_script_file']) }}"></script>
|
||||
{% 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>
|
||||
</body>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<nav>
|
||||
<ul class="row">
|
||||
<ul>
|
||||
{% for plugin in plugins|sort %}
|
||||
<li :class="{selected: '{{ plugin }}' == selectedPlugin}">
|
||||
<a href="#{{ plugin }}" @click="selectedPlugin = '{{ plugin }}'">
|
||||
|
@ -15,6 +15,11 @@
|
|||
<li>
|
||||
<a href="#{{ plugin }}">Test tab 3</a>
|
||||
</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>
|
||||
</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/scenes.html' %}
|
||||
{% include 'plugins/light.hue/units.html' %}
|
||||
{% include 'plugins/light.hue/animations.html' %}
|
||||
|
||||
<script type="text/x-template" id="tmpl-light-hue">
|
||||
<div class="row light-hue-container">
|
||||
|
@ -33,6 +34,7 @@
|
|||
|
||||
<div class="units col-no-margin-6 col-s-12">
|
||||
<div class="title">Lights</div>
|
||||
|
||||
<light-hue-group-controller
|
||||
v-if="selectedGroup"
|
||||
v-model="groups[selectedGroup]"
|
||||
|
@ -43,6 +45,16 @@
|
|||
@input="updatedGroup">
|
||||
</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
|
||||
v-for="(light, id) in lights"
|
||||
v-model="light.state"
|
||||
|
|
|
@ -3,6 +3,8 @@ import os
|
|||
import re
|
||||
|
||||
from platypush.config import Config
|
||||
from platypush.backend.http.app import template_folder
|
||||
|
||||
|
||||
class HttpUtils(object):
|
||||
@staticmethod
|
||||
|
@ -100,5 +102,19 @@ class HttpUtils(object):
|
|||
def plugin_name_to_tag(cls, module_name):
|
||||
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:
|
||||
|
|
|
@ -70,12 +70,26 @@ class LightHuePlugin(LightPlugin):
|
|||
|
||||
self.redis = None
|
||||
self.animation_thread = None
|
||||
self.animations = {}
|
||||
self._init_animations()
|
||||
self.logger.info('Configured lights: "{}"'. format(self.lights))
|
||||
|
||||
def _expand_groups(self):
|
||||
groups = [g for g in self.bridge.groups if g.name in self.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
|
||||
def connect(self):
|
||||
|
@ -239,6 +253,36 @@ class LightHuePlugin(LightPlugin):
|
|||
|
||||
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):
|
||||
try:
|
||||
self.connect()
|
||||
|
@ -654,6 +698,7 @@ class LightHuePlugin(LightPlugin):
|
|||
if self.animation_thread and self.animation_thread.is_alive():
|
||||
redis = self._get_redis()
|
||||
redis.rpush(self.ANIMATION_CTRL_QUEUE_NAME, 'STOP')
|
||||
self._init_animations()
|
||||
|
||||
@action
|
||||
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)
|
||||
: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]
|
||||
:type hue_range: list[int]
|
||||
|
||||
:param sat_range: If you selected a color transition, this will specify the saturation range of your color
|
||||
transition. Default: [0, 255]
|
||||
:param sat_range: If you selected a color color_transition.html, this will specify the saturation range of your color
|
||||
color_transition.html. Default: [0, 255]
|
||||
:type sat_range: list[int]
|
||||
|
||||
:param bri_range: If you selected a color transition, this will specify the brightness range of your color
|
||||
transition. Default: [254, 255] :type bri_range: list[int]
|
||||
:param bri_range: If you selected a color color_transition.html, this will specify the brightness range of your color
|
||||
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 groups: Groups to control (names or group objects). Default: plugin default groups
|
||||
:param lights: Lights to control (names, IDs or light objects). Default: plugin default lights
|
||||
: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
|
||||
|
||||
: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
|
||||
|
||||
: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
|
||||
|
||||
: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:
|
||||
hue_range = [0, self.MAX_HUE]
|
||||
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 []
|
||||
for g in groups:
|
||||
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
|
||||
|
||||
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):
|
||||
if animation == self.Animation.COLOR_TRANSITION:
|
||||
return { l: {
|
||||
|
|
10
setup.py
10
setup.py
|
@ -2,6 +2,7 @@
|
|||
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import distutils.cmd
|
||||
from distutils.command.build import build
|
||||
from setuptools import setup, find_packages
|
||||
|
@ -15,8 +16,8 @@ class WebBuildCommand(distutils.cmd.Command):
|
|||
description = 'Build components and styles for the web pages'
|
||||
user_options = []
|
||||
|
||||
@staticmethod
|
||||
def generate_css_files():
|
||||
@classmethod
|
||||
def generate_css_files(cls):
|
||||
from scss import Compiler
|
||||
|
||||
print('Building CSS files')
|
||||
|
@ -36,8 +37,13 @@ class WebBuildCommand(distutils.cmd.Command):
|
|||
|
||||
with open(css_file, 'w') as f:
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def _fix_css4_vars(css):
|
||||
return re.sub(r'var\("--([^"]+)"\)', r'var(--\1)', css)
|
||||
|
||||
def initialize_options(self):
|
||||
pass
|
||||
|
||||
|
|
Loading…
Reference in a new issue