diff --git a/platypush/backend/http/app/__init__.py b/platypush/backend/http/app/__init__.py index e0e331eb..2f8d8cfe 100644 --- a/platypush/backend/http/app/__init__.py +++ b/platypush/backend/http/app/__init__.py @@ -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 diff --git a/platypush/backend/http/static/css/source/common/elements.scss b/platypush/backend/http/static/css/source/common/elements.scss index 40a7d299..9c597551 100644 --- a/platypush/backend/http/static/css/source/common/elements.scss +++ b/platypush/backend/http/static/css/source/common/elements.scss @@ -15,5 +15,6 @@ @import 'common/elements/button'; @import 'common/elements/switch'; +@import 'common/elements/range-slider'; @import 'common/elements/slider'; diff --git a/platypush/backend/http/static/css/source/common/elements/range-slider.scss b/platypush/backend/http/static/css/source/common/elements/range-slider.scss new file mode 100644 index 00000000..973b5fc8 --- /dev/null +++ b/platypush/backend/http/static/css/source/common/elements/range-slider.scss @@ -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; + } + } +} + diff --git a/platypush/backend/http/static/css/source/common/elements/slider.scss b/platypush/backend/http/static/css/source/common/elements/slider.scss index e75e9387..31df3137 100644 --- a/platypush/backend/http/static/css/source/common/elements/slider.scss +++ b/platypush/backend/http/static/css/source/common/elements/slider.scss @@ -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; diff --git a/platypush/backend/http/static/css/source/common/layout.scss b/platypush/backend/http/static/css/source/common/layout.scss index a1fad35b..f69d0e4c 100644 --- a/platypush/backend/http/static/css/source/common/layout.scss +++ b/platypush/backend/http/static/css/source/common/layout.scss @@ -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; } } } diff --git a/platypush/backend/http/static/css/source/common/vars.scss b/platypush/backend/http/static/css/source/common/vars.scss index 32253447..c87a4d2d 100644 --- a/platypush/backend/http/static/css/source/common/vars.scss +++ b/platypush/backend/http/static/css/source/common/vars.scss @@ -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; diff --git a/platypush/backend/http/static/css/source/webpanel/nav.scss b/platypush/backend/http/static/css/source/webpanel/nav.scss index e8dba707..b82f6d53 100644 --- a/platypush/backend/http/static/css/source/webpanel/nav.scss +++ b/platypush/backend/http/static/css/source/webpanel/nav.scss @@ -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; + } } } } diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/light.hue/index.scss b/platypush/backend/http/static/css/source/webpanel/plugins/light.hue/index.scss index bb7e02d6..da02f79f 100644 --- a/platypush/backend/http/static/css/source/webpanel/plugins/light.hue/index.scss +++ b/platypush/backend/http/static/css/source/webpanel/plugins/light.hue/index.scss @@ -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 { diff --git a/platypush/backend/http/static/js/application.js b/platypush/backend/http/static/js/application.js index e7a3afa2..da478e43 100644 --- a/platypush/backend/http/static/js/application.js +++ b/platypush/backend/http/static/js/application.js @@ -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() {}, }); diff --git a/platypush/backend/http/static/js/elements/range-slider.js b/platypush/backend/http/static/js/elements/range-slider.js new file mode 100644 index 00000000..d5273412 --- /dev/null +++ b/platypush/backend/http/static/js/elements/range-slider.js @@ -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); + }, + }, +}); + diff --git a/platypush/backend/http/static/js/elements.js b/platypush/backend/http/static/js/elements/switch.js similarity index 100% rename from platypush/backend/http/static/js/elements.js rename to platypush/backend/http/static/js/elements/switch.js diff --git a/platypush/backend/http/static/js/plugins/light.hue/animations.js b/platypush/backend/http/static/js/plugins/light.hue/animations.js new file mode 100644 index 00000000..ed345b34 --- /dev/null +++ b/platypush/backend/http/static/js/plugins/light.hue/animations.js @@ -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', {}); + } + }, + }, +}); + diff --git a/platypush/backend/http/static/js/plugins/light.hue/animations/blink.js b/platypush/backend/http/static/js/plugins/light.hue/animations/blink.js new file mode 100644 index 00000000..51e09f5f --- /dev/null +++ b/platypush/backend/http/static/js/plugins/light.hue/animations/blink.js @@ -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; + }, + }, +}); + diff --git a/platypush/backend/http/static/js/plugins/light.hue/animations/color_transition.js b/platypush/backend/http/static/js/plugins/light.hue/animations/color_transition.js new file mode 100644 index 00000000..f36c7413 --- /dev/null +++ b/platypush/backend/http/static/js/plugins/light.hue/animations/color_transition.js @@ -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; + }, + }, +}); + diff --git a/platypush/backend/http/static/js/plugins/light.hue/index.js b/platypush/backend/http/static/js/plugins/light.hue/index.js index 047d2952..9f105131 100644 --- a/platypush/backend/http/static/js/plugins/light.hue/index.js +++ b/platypush/backend/http/static/js/plugins/light.hue/index.js @@ -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', { diff --git a/platypush/backend/http/templates/elements.html b/platypush/backend/http/templates/elements.html index dc4f6771..2614c228 100644 --- a/platypush/backend/http/templates/elements.html +++ b/platypush/backend/http/templates/elements.html @@ -1,2 +1,3 @@ {% include 'elements/switch.html' %} +{% include 'elements/range-slider.html' %} diff --git a/platypush/backend/http/templates/elements/range-slider.html b/platypush/backend/http/templates/elements/range-slider.html new file mode 100644 index 00000000..e394f0d3 --- /dev/null +++ b/platypush/backend/http/templates/elements/range-slider.html @@ -0,0 +1,8 @@ + + + + diff --git a/platypush/backend/http/templates/elements/switch.html b/platypush/backend/http/templates/elements/switch.html index aa4d3560..a8e8ed10 100644 --- a/platypush/backend/http/templates/elements/switch.html +++ b/platypush/backend/http/templates/elements/switch.html @@ -5,3 +5,5 @@ + + diff --git a/platypush/backend/http/templates/header.html b/platypush/backend/http/templates/header.html index d9868a2a..24e7af4b 100644 --- a/platypush/backend/http/templates/header.html +++ b/platypush/backend/http/templates/header.html @@ -1,15 +1,15 @@ diff --git a/platypush/backend/http/templates/index.html b/platypush/backend/http/templates/index.html index b8fce9d0..70063977 100644 --- a/platypush/backend/http/templates/index.html +++ b/platypush/backend/http/templates/index.html @@ -42,7 +42,7 @@