vue.js refactor part 1 - prepared webpanel logic and migrated light.hue plugin

This commit is contained in:
Fabio Manganiello 2019-05-23 18:51:28 +02:00
parent f053aa455d
commit e72d4de581
49 changed files with 1666 additions and 393 deletions

1
.gitignore vendored
View file

@ -12,3 +12,4 @@ platypush/backend/http/static/resources/*
docs/build docs/build
.idea/ .idea/
config config
platypush/backend/http/static/css/*/.sass-cache/

View file

@ -24,29 +24,39 @@ def index():
if not authentication_ok(request): if not authentication_ok(request):
return authenticate() return authenticate()
# These plugins have their own template file but won't be shown as a tab in
# the web panel. This is usually the case for plugins that only include JS
# code but no template content.
_hidden_plugins = {
'assistant.google'
}
configured_plugins = Config.get_plugins() configured_plugins = Config.get_plugins()
enabled_plugins = {} enabled_templates = {}
hidden_plugins = {} enabled_scripts = {}
enabled_styles = {}
js_folder = os.path.abspath(
os.path.join(template_folder, '..', 'static', 'js'))
style_folder = os.path.abspath(
os.path.join(template_folder, '..', 'static', 'css', 'dist'))
for plugin, conf in configured_plugins.items(): for plugin, conf in configured_plugins.items():
template_file = os.path.join('panel', plugin, 'index.html') template_file = os.path.join(
if os.path.isfile(os.path.join(template_folder, template_file)): template_folder, 'plugins', plugin, 'index.html')
if plugin in _hidden_plugins:
hidden_plugins[plugin] = conf script_file = os.path.join(js_folder, 'plugins', plugin, 'index.js')
else: style_file = os.path.join(style_folder, 'webpanel', 'plugins', plugin+'.css')
enabled_plugins[plugin] = conf
if os.path.isfile(template_file):
conf['_template_file'] = '/' + '/'.join(template_file.split(os.sep)[-3:])
enabled_templates[plugin] = conf
if os.path.isfile(script_file):
conf['_script_file'] = '/'.join(script_file.split(os.sep)[-4:])
enabled_scripts[plugin] = conf
if os.path.isfile(style_file):
conf['_style_file'] = 'css/dist/' + style_file[len(style_folder)+1:]
enabled_styles[plugin] = conf
http_conf = Config.get('backend.http') http_conf = Config.get('backend.http')
return render_template('index.html', plugins=enabled_plugins, return render_template('index.html', templates=enabled_templates,
hidden_plugins=hidden_plugins, utils=HttpUtils, scripts=enabled_scripts, styles=enabled_styles,
token=Config.get('token'), utils=HttpUtils, token=Config.get('token'),
websocket_port=get_websocket_port(), websocket_port=get_websocket_port(),
has_ssl=http_conf.get('ssl_cert') is not None) has_ssl=http_conf.get('ssl_cert') is not None)

View file

@ -1,29 +0,0 @@
ul.tab-nav {
list-style: none;
border-bottom: 1px solid #bbb;
padding-left: 5px;
}
ul.tab-nav li {
display: inline;
}
ul.tab-nav li a.button {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
margin-bottom: -1px;
border-bottom: none;
}
ul.tab-nav li a.active.button {
border-bottom: 1px solid #fff;
}
.tab-content .tab-pane {
display: none;
}
.tab-content .tab-pane.active {
display: block;
}

View file

@ -0,0 +1,19 @@
//// General purpose classes /////
.hidden {
display: none !important;
}
.selected {
background: $selected-bg;
}
.pull-right {
text-align: right !important;
}
//// UI elements definitions /////
@import 'common/elements/button';
@import 'common/elements/switch';
@import 'common/elements/slider';

View file

@ -0,0 +1,12 @@
button[disabled],
.button[disabled] {
color: #bbb;
background: rgba(240,240,240,1);
border: 1px solid;
}
.btn-primary {
background-color: #d8ffe0 !important;
border: 1px solid #98efb0 !important;
}

View file

@ -0,0 +1,36 @@
.slider {
@include appearance(none);
@include transition(opacity .2s);
width: 100%;
height: 15px;
border-radius: 5px;
background: $slider-bg;
outline: none;
opacity: 0.7;
&:hover {
opacity: 1;
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 25px;
height: 25px;
border-radius: 50%;
background: $slider-thumb-bg;
cursor: pointer;
}
&[disabled]::-webkit-slider-thumb {
display: none;
}
&::-moz-range-thumb {
width: 25px;
height: 25px;
background: $slider-thumb-bg;
cursor: pointer;
}
}

View file

@ -0,0 +1,109 @@
@import url(https://fonts.googleapis.com/css?family=Francois+One);
@import url(https://fonts.googleapis.com/css?family=PT+Sans);
@font-face {
font-family: 'Audiowide';
font-style: normal;
font-weight: 400;
src: local("Audiowide"), local("Audiowide-Regular"), url(http://themes.googleusercontent.com/static/fonts/audiowide/v2/8XtYtNKEyyZh481XVWfVOj8E0i7KZn-EPnyo3HZu7kw.woff) format("woff");
}
.switch {
display: inline-block;
text-align: center;
user-select: none;
padding-top: 1rem;
input[type=checkbox] {
display: none !important;
}
label, label:before, label:after,
input[type=checkbox], input[type=checkbox]:before, input[type=checkbox]:after,
.feature, .feature:before, .feature:after {
transition: all 250ms ease-in;
}
label:before, label:after,
input[type=checkbox]:before, input[type=checkbox]:after,
.feature:before, .feature:after {
content: '';
display: block;
}
label {
position: relative;
width: 50px;
height: 50px;
background-color: $switch-bg-1;
border-radius: 50%;
box-shadow: $switch-shadow-1;
display: block;
margin: 0 auto;
font-size: 1.4em;
transition: all 350ms ease-in;
&:before {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 22.7272727273px;
height: 22.7272727273px;
border-radius: 50%;
background-color: $switch-bg-2;
box-shadow: $switch-shadow-2;
}
&:after {
position: absolute;
left: 50%;
top: 35%;
transform: translate(-50%, -50%);
width: 4px;
height: 12px;
background-color: $switch-bg-3;
box-shadow: $switch-shadow-3;
}
&:before, &:after {
transition-duration: 150ms;
}
&:hover {
cursor: pointer;
&:before { box-shadow: $switch-shadow-hover; }
&:after { background-color: $switch-bg-hover; }
}
}
input[type=checkbox]:checked + label {
box-shadow: $switch-shadow-checked-1;
&:before { box-shadow: $switch-shadow-checked-2; }
&:after { background-color: $switch-bg-checked; }
}
&.glow {
label {
background-color: $switch-bg-glow-2;
box-shadow: $switch-shadow-glow-1;
&:before { box-shadow: $switch-shadow-glow-2; }
&:after {
background-color: $switch-bg-glow-3;
}
label:hover {
&:before { box-shadow: $switch-shadow-glow-hover; }
&:after { background-color: $switch-bg-glow-hover; }
}
}
input[type=checkbox]:checked + label {
box-shadow: $switch-shadow-glow-checked-1;
&:before { box-shadow: $switch-shadow-glow-checked-2; }
&:after { background-color: $switch-bg-glow-checked; }
}
}
}

View file

@ -0,0 +1,85 @@
$widths: (
s: '(max-width: 720px)',
m: '(max-width: 1024px) and (min-width: 720px)',
l: '(min-width: 1024px)',
);
@for $i from 1 through 12 {
.col-#{$i} {
float: left;
box-sizing: border-box;
@if $i < 12 {
width: (4.66666666667%*$i) + (4% * if($i > 1, $i - 1, 0));
margin-left: 4%;
} @else {
width: 100%;
margin-left: 0;
}
&:first-child {
margin-left: 0;
}
}
.col-no-margin-#{$i} {
float: left;
box-sizing: border-box;
width: ((100%/12)*$i);
}
@if $i < 12 {
.col-offset-#{$i} {
margin-left: (8.66666666667%*$i);
}
}
}
@each $size, $width in $widths {
@media #{$width} {
@for $i from 1 through 12 {
.col-#{$size}-#{$i} {
float: left;
box-sizing: border-box;
@if $i < 12 {
width: (4.66666666667%*$i) + (4% * if($i > 1, $i - 1, 0));
margin-left: 4%;
} @else {
width: 100%;
margin-left: 0;
}
&:first-child {
margin-left: 0;
}
}
@if $i < 12 {
.col-offset-#{$size}-#{$i} {
margin-left: (8.66666666667%*$i);
}
}
.col-no-margin-#{$size}-#{$i} {
float: left;
box-sizing: border-box;
width: ((100%/12)*$i);
}
}
.#{$size}-hidden {
display: none !important;
}
.#{$size}-visible {
display: block !important;
}
}
}
.vertical-center {
display: flex;
align-items: center;
}

View file

@ -0,0 +1,23 @@
@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 box-shadow($value) {
-webkit-box-shadow: $value;
-o-box-shadow: $value;
-ms-box-shadow: $value;
box-shadow: $value;
}

View file

@ -0,0 +1,36 @@
.modal {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 999;
background-color: rgba(10,10,10,0.85);
}
.modal-container {
margin: 5% auto auto auto;
width: 70%;
background: white;
border-radius: 10px;
}
.modal-header {
border-bottom: 1px solid #ccc;
margin: 0.5rem auto;
padding: 0.5rem;
text-align: center;
background-color: $modal-bg;
border-radius: 10px 10px 0 0;
text-transform: uppercase;
letter-spacing: .1rem;
line-height: 38px;
}
.modal-body {
padding: 2.5rem 2rem 1.5rem 2rem;
}

View file

@ -0,0 +1,65 @@
#notification-container {
position: fixed;
bottom: 0;
right: 0;
width: 25em;
.notification {
background: rgba(180,245,188,0.85);
border: 1px solid #bbb;
border-radius: 5px;
margin-bottom: 1rem;
margin-right: 1rem;
cursor: pointer;
display: none;
}
.notification:hover {
background: rgba(160,235,168,0.9);
}
.notification-title {
padding: 4px 10px;
font-weight: bold;
text-transform: capitalize;
line-height: 30px;
letter-spacing: .1rem;
}
.notification-body {
height: 6em;
overflow: hidden;
padding-bottom: 1rem;
letter-spacing: .05rem;
}
.notification-text {
margin-left: 0 !important;
}
.notification-image {
height: 100%;
text-align: center;
padding-top: .6em;
padding-bottom: .6em;
.notification-image-item {
width: 100%;
height: 100%;
margin: auto;
}
.fa.notification-image-item {
margin-top: .8em;
margin-left: .2em;
font-size: 25px;
}
img {
width: 80%;
height: 80%;
}
}
}

View file

@ -0,0 +1,49 @@
//// Common defaults
$default-bg: #f4f5f6 !default;
$default-fg: black !default;
$default-fg-2: #333333 !default;
$default-font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif !default;
$default-border: 1px solid #e1e4e8 !default;
$default-border-2: 1px solid #dddddd !default;
$default-bottom: $default-border !default;
$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;
$modal-bg: #f0f0f0 !default;
//// Switch element
$switch-bg-1: #f9f8f6 !default;
$switch-bg-2: #38ffa0 !default;
$switch-bg-3: #cccccc !default;
$switch-bg-hover: #b3b3b3 !default;
$switch-bg-checked: #38ffa0 !default;
$switch-bg-glow-1: #111111 !default;
$switch-bg-glow-2: #ffffff !default;
$switch-bg-glow-3: #aaaaaa !default;
$switch-bg-glow-hover: #ffffff !default;
$switch-bg-glow-checked: #00e094 !default;
$switch-shadow-1: 0 5px 10px 0px #333, 0 15px 20px 0px #cccccc !default;
$switch-shadow-2: inset 0 0 0 5px #ccc, inset 0 0 0 14px #f9f8f6 !default;
$switch-shadow-3: 0 0 0 2.5px #f9f8f6 !default;
$switch-shadow-hover: inset 0 0 0 5px #b3b3b3, inset 0 0 0 14px #f9f8f6 !default;
$switch-shadow-checked-1: 0 2px 5px 0px gray, 0 15px 20px 0px transparent !default;
$switch-shadow-checked-2: inset 0 0 0 5px #38ffa0, inset 0 0 0 14px #f9f8f6 !default;
$switch-shadow-glow-1: 0 5px 10px 0 #aaa, 0 0 0 3px #bbb, 0 0 8px 2px transparent, 0 0 0 6px #eee !default;
$switch-shadow-glow-2: inset 0 0 0 5px #aaa, inset 0 0 0 14px #fff !default;
$switch-shadow-glow-hover: inset 0 0 0 5px #fff, inset 0 0 0 14px #fff !default;
$switch-shadow-glow-checked-1: 0 0px 8px 0 #00ad72, 0 0 0 3px #00e094, 0 0 30px 0 #00e094, 0 0 0 6px #fff !default;
$switch-shadow-glow-checked-2: inset 0 0 0 5px #00e094, inset 0 0 0 14px #fff !default;
//// Slier element
$slider-bg: #e4e4e4 !default;
$slider-thumb-bg: #4caf50 !default;
//// Header style
$header-bottom: $default-bottom;

View file

@ -0,0 +1,34 @@
header {
.row {
width: 100%;
background: $header-bg;
padding: 1rem 2.5rem;
// margin: 0 1rem 3.5rem -1rem;
border-bottom: $header-bottom;
display: flex;
align-items: center;
.logo {
font-size: 25px;
.logo-1 {
font-weight: bold;
}
}
.date-time {
text-align: right;
padding-right: 3rem;
.date {
color: #666;
}
.time {
font-weight: bold;
font-size: 25px;
}
}
}
}

View file

@ -0,0 +1,32 @@
@import 'common/vars';
@import 'common/mixins';
@import 'common/layout';
@import 'common/elements';
@import 'common/modal';
@import 'common/notification';
@import 'header';
@import 'nav';
body {
width: 100%;
overflow-x: hidden;
font-family: $default-font-family;
}
main {
margin: auto;
}
a {
color: $default-link-fg;
}
.plugin-container {
border: $default-border-2;
border-radius: 1rem;
margin: 1.5rem;
box-shadow: 8px 8px 6px -1px rgba(187,187,187,0.75);
}

View file

@ -0,0 +1,61 @@
nav {
margin-bottom: 1.2rem;
ul {
margin: 0;
padding: 0;
list-style-type: none;
background: $nav-bg;
display: flex;
align-items: center;
border-bottom: $default-bottom;
box-shadow: 0 2.5px 4px 0 #bbb;
li {
padding: 1rem 1.5rem;
display: inline-block;
margin: 0;
text-transform: uppercase;
letter-spacing: .1rem;
&:hover {
border-radius: 2rem;
background: $hover-bg;
letter-spacing: .4rem;
}
a {
text-decoration: none;
display: block;
color: $nav-fg;
&:hover {
color: $nav-fg;
}
}
}
.decorator {
width: 0;
height: 0;
border-top: 20px solid transparent;
border-bottom: 20px solid transparent;
border-left: 17px solid $selected-bg;
&:hover {
display: none;
}
}
&:hover {
.decorator {
display: none;
}
li.selected {
border-radius: 2rem;
}
}
}
}

View file

@ -0,0 +1,107 @@
@import 'common/vars';
@import 'common/layout';
@import 'webpanel/plugins/light.hue/vars';
.light-hue-container {
display: flex;
color: $default-fg-2;
font-weight: 400;
line-height: 3.8rem;
letter-spacing: .1rem;
.groups,
.scenes,
.units {
&:not(:last-child) {
border-right: $default-border-2;
}
.title {
padding: .75rem;
background: $default-bg;
border-bottom: $default-border-2;
&:last-child {
border-radius: 0 1rem 0 0;
}
}
.group,
.scene,
.unit,
.group-controller {
padding: 1rem;
cursor: pointer;
&:hover {
background: $hover-bg;
}
&:not(:last-child) {
border-bottom: $default-border-2;
}
&:not(.hidden) {
.row {
width: 100%;
}
}
* > .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;
.slider-container {
@extend .vertical-center;
margin: 1rem auto;
}
* > .fa {
font-size: 3rem;
}
* > .color-logo {
width: 2rem;
height: 2rem;
border-radius: 1rem;
}
* > .color-logo-red { background-color: red; }
* > .color-logo-green { background-color: green; }
* > .color-logo-blue { background-color: blue; }
}
&:hover {
* > .properties {
background: $light-hue-properties-hover-bg;
}
}
}
.group {
text-transform: uppercase;
}
.group-controller {
font-weight: 600;
}
}
.groups {
.title {
border-radius: 1rem 0 0 0;
}
}
.units {
.title {
border-radius: 0 1rem 0 0;
}
}
}

View file

@ -0,0 +1,4 @@
$light-hue-properties-bg: rgba(239,239,240,0.5);
$light-hue-properties-hover-bg: white;
$light-hue-properties-shadow: 0 0 4px 2px rgba(187,187,187,0.75);

View file

@ -1,159 +0,0 @@
@import url(https://fonts.googleapis.com/css?family=Francois+One);
@import url(https://fonts.googleapis.com/css?family=PT+Sans);
@font-face {
font-family: 'Audiowide';
font-style: normal;
font-weight: 400;
src: local("Audiowide"), local("Audiowide-Regular"), url(http://themes.googleusercontent.com/static/fonts/audiowide/v2/8XtYtNKEyyZh481XVWfVOj8E0i7KZn-EPnyo3HZu7kw.woff) format("woff"); }
.toggle {
display: inline-block;
text-align: center;
user-select: none; }
.toggle--checkbox {
display: none !important; }
.toggle--btn {
display: block;
margin: 0 auto;
font-size: 1.4em;
transition: all 350ms ease-in; }
.toggle--btn:hover {
cursor: pointer; }
.toggle--btn, .toggle--btn:before, .toggle--btn:after,
.toggle--checkbox,
.toggle--checkbox:before,
.toggle--checkbox:after,
.toggle--feature,
.toggle--feature:before,
.toggle--feature:after {
transition: all 250ms ease-in; }
.toggle--btn:before, .toggle--btn:after,
.toggle--checkbox:before,
.toggle--checkbox:after,
.toggle--feature:before,
.toggle--feature:after {
content: '';
display: block; }
/* =====================================================
Toggle - switch stylee
===================================================== */
.toggle--switch .toggle--btn {
position: relative;
width: 120px;
height: 44px;
font-family: 'PT Sans', Sans Serif;
text-transform: uppercase;
color: #fff;
background: linear-gradient(90deg, #a4bf4d 0%, #a4bf4d 50%, #ca5046 50%, #ca5046 200%);
background-position: -80px 0;
background-size: 200% 100%;
box-shadow: inset 0 0px 22px -8px #111; }
.toggle--switch .toggle--btn, .toggle--switch .toggle--btn:before {
border-radius: 4px; }
.toggle--switch .toggle--btn:before {
display: block;
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
width: 52px;
height: 44px;
border: 2px solid #202027;
background-image: linear-gradient(90deg, transparent 50%, rgba(255, 255, 255, 0.15) 100%);
background-color: #2b2e3a;
background-size: 5px 5px;
text-indent: -100%; }
.toggle--switch .toggle--feature {
position: relative;
display: block;
overflow: hidden;
height: 44px;
text-shadow: 0 1px 2px #666; }
.toggle--switch .toggle--feature:before, .toggle--switch .toggle--feature:after {
position: absolute;
top: 50%;
transform: translateY(-50%); }
.toggle--switch .toggle--feature:before {
content: attr(data-label-on);
left: -60%; }
.toggle--switch .toggle--feature:after {
content: attr(data-label-off);
right: 16%; }
.toggle--switch .toggle--checkbox:checked + .toggle--btn {
background-position: 0 0; }
.toggle--switch .toggle--checkbox:checked + .toggle--btn:before {
left: calc(100% - 52px); }
.toggle--switch .toggle--checkbox:checked + .toggle--btn .toggle--feature:before {
left: 20%; }
.toggle--switch .toggle--checkbox:checked + .toggle--btn .toggle--feature:after {
right: -60%; }
/* ======================================================
Push button toggle
====================================================== */
.toggle--push .toggle--btn {
position: relative;
width: 50px;
height: 50px;
background-color: #f9f8f6;
border-radius: 50%;
box-shadow: 0 5px 10px 0px #333, 0 15px 20px 0px #cccccc; }
.toggle--push .toggle--btn, .toggle--push .toggle--btn:before, .toggle--push .toggle--btn:after {
transition-duration: 150ms; }
.toggle--push .toggle--btn:before {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 22.7272727273px;
height: 22.7272727273px;
border-radius: 50%;
background-color: #38ffa0;
box-shadow: inset 0 0 0 5px #ccc, inset 0 0 0 14px #f9f8f6; }
.toggle--push .toggle--btn:after {
position: absolute;
left: 50%;
top: 35%;
transform: translate(-50%, -50%);
width: 4px;
height: 12px;
background-color: #ccc;
box-shadow: 0 0 0 2.5px #f9f8f6; }
.toggle--push .toggle--btn:hover:before {
box-shadow: inset 0 0 0 5px #b3b3b3, inset 0 0 0 14px #f9f8f6; }
.toggle--push .toggle--btn:hover:after {
background-color: #b3b3b3; }
.toggle--push .toggle--checkbox:checked + .toggle--btn {
box-shadow: 0 2px 5px 0px gray, 0 15px 20px 0px transparent; }
.toggle--push .toggle--checkbox:checked + .toggle--btn:before {
box-shadow: inset 0 0 0 5px #38ffa0, inset 0 0 0 14px #f9f8f6; }
.toggle--push .toggle--checkbox:checked + .toggle--btn:after {
background-color: #38ffa0; }
.toggle--push--glow {
background: #111;
padding: 50px 0;
margin-bottom: -50px; }
.toggle--push--glow .toggle--btn {
background-color: #dfdfdf;
box-shadow: 0 5px 10px 0px #333, 0 0 0 3px #444444, 0 0 8px 2px transparent, 0 0 0 6px #919191; }
.toggle--push--glow .toggle--btn:before {
box-shadow: inset 0 0 0 5px #aaa, inset 0 0 0 14px #dfdfdf; }
.toggle--push--glow .toggle--btn:after {
background-color: #aaa;
box-shadow: 0 0 0 2.5px #dfdfdf; }
.toggle--push--glow .toggle--btn:hover:before {
box-shadow: inset 0 0 0 5px #777777, inset 0 0 0 14px #dfdfdf; }
.toggle--push--glow .toggle--btn:hover:after {
background-color: #777777; }
.toggle--push--glow .toggle--checkbox:checked + .toggle--btn {
box-shadow: 0 0px 8px 0 #0072ad, 0 0 0 3px #0094e0, 0 0 30px 0 #0094e0, 0 0 0 6px #777777; }
.toggle--push--glow .toggle--checkbox:checked + .toggle--btn:before {
box-shadow: inset 0 0 0 5px #0094e0, inset 0 0 0 14px #dfdfdf; }
.toggle--push--glow .toggle--checkbox:checked + .toggle--btn:after {
background-color: #0094e0; }

View file

@ -0,0 +1,43 @@
function execute(request) {
var additionalPayload = {};
if (!('target' in request) || !request['target']) {
request['target'] = 'localhost';
}
if (!('type' in request) || !request['type']) {
request['type'] = 'request';
}
if (window.config.token) {
additionalPayload.headers = {
'X-Token': window.config.token
};
}
return new Promise((resolve, reject) => {
axios.post('/execute', request, additionalPayload)
.then((response) => {
response = response.data.response;
if (!response.errors.length) {
resolve(response.output);
} else {
// TODO Handle error
reject(response.errors[0]);
}
})
.catch((error) => {
// TODO Handle error
reject(error);
});
});
}
function request(action, args={}) {
return execute({
type: 'request',
action: action,
args: args,
});
}

View file

@ -1,32 +1,46 @@
// Declaration of the main vue app Vue.component('app-header', {
var app; template: '#tmpl-app-header',
data: function() {
function ready(callback){ return {
if (document.readyState!='loading') callback(); now: new Date(),
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', callback); };
else document.attachEvent('onreadystatechange', function(){
if (document.readyState=='complete') callback();
});
}
ready(function() {
app = new Vue({
el: '#app',
delimiters: ['[[',']]'],
data: {
config: {foo:"bar"}
}, },
created: function() { created: function() {
}, const self = this;
setInterval(() => {
mounted: function() { self.now = new Date();
}, }, 1000)
updated: function() {
},
destroyed: function() {
}, },
}); });
Vue.component('plugin', {
template: '#tmpl-plugin',
props: ['config','tag'],
data: function() {
return {
selected: false,
};
},
}); });
// Declaration of the main vue app
var app = new Vue({
el: '#app',
// Override {{ }} delimiters to prevent clash with Flask templates
delimiters: ['[[',']]'],
data: function() {
return {
config: window.config,
selectedPlugin: undefined,
};
},
mounted: function() {},
created: function() {},
updated: function() {},
destroyed: function() {},
});

View file

@ -0,0 +1,14 @@
Vue.component('toggle-switch', {
template: '#tmpl-switch',
props: ['id','value','glow'],
methods: {
toggled: function(event) {
this.$emit('toggled', {
id: this.id,
value: !this.value
});
},
},
});

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

View file

@ -0,0 +1,106 @@
// Source: https://gist.github.com/uredkar/bd305f2dda9abf5b393d417424777c87#file-cie_rgb_converter-js
/**
* Converts CIE color space to RGB color space
* @param {Number} x
* @param {Number} y
* @param {Number} brightness - Ranges from 1 to 254
* @return {Array} Array that contains the color values for red, green and blue
*/
function toRGB(x, y, brightness) {
//Set to maximum brightness if no custom value was given (Not the slick ECMAScript 6 way for compatibility reasons)
if (brightness === undefined) {
brightness = 254;
}
var z = 1.0 - x - y;
var Y = (brightness / 254).toFixed(2);
var X = (Y / y) * x;
var Z = (Y / y) * z;
//Convert to RGB using Wide RGB D65 conversion
var red = X * 1.656492 - Y * 0.354851 - Z * 0.255038;
var green = -X * 0.707196 + Y * 1.655397 + Z * 0.036152;
var 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];
}
/**
* Converts RGB color space to CIE color space
* @param {Number} red
* @param {Number} green
* @param {Number} blue
* @return {Array} Array that contains the CIE color values for x and y
*/
function toXY(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
var red = (red > 0.04045) ? Math.pow((red + 0.055) / (1.0 + 0.055), 2.4) : (red / 12.92);
var green = (green > 0.04045) ? Math.pow((green + 0.055) / (1.0 + 0.055), 2.4) : (green / 12.92);
var 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
var X = red * 0.664511 + green * 0.154324 + blue * 0.162028;
var Y = red * 0.283881 + green * 0.668433 + blue * 0.047685;
var Z = red * 0.000088 + green * 0.072310 + blue * 0.986039;
//Calculate the xy values from the XYZ values
var x = (X / (X + Y + Z)).toFixed(4);
var y = (Y / (X + Y + Z)).toFixed(4);
if (isNaN(x))
x = 0;
if (isNaN(y))
y = 0;
return [x, y];
}

View file

@ -0,0 +1,41 @@
Vue.component('light-hue-property-selector', {
template: '#tmpl-light-hue-property-selector',
props: ['id','value'],
computed: {
rgb: function() {
if (!(this.value && 'xy' in this.value)) {
return;
}
return toRGB(this.value.xy[0], this.value.xy[1], this.value.bri);
},
},
methods: {
changed: function(event) {
var value = parseInt(event.target.value);
var xy;
if (event.target.getAttribute('class').split(' ').indexOf('bri') > -1) {
this.$emit('bri-changed', {bri: value});
return;
} else if (event.target.getAttribute('class').split(' ').indexOf('ct') > -1) {
this.$emit('ct-changed', {ct: value});
return;
}
if (event.target.getAttribute('class').split(' ').indexOf('red') > -1) {
xy = toXY(value, this.rgb[1], this.rgb[2]);
} else if (event.target.getAttribute('class').split(' ').indexOf('green') > -1) {
xy = toXY(this.rgb[0], value, this.rgb[2]);
} else if (event.target.getAttribute('class').split(' ').indexOf('blue') > -1) {
xy = toXY(this.rgb[0], this.rgb[1], value);
} else {
return;
}
this.$emit('color-changed', {xy: xy});
},
},
});

View file

@ -0,0 +1,118 @@
Vue.component('light-hue-group', {
template: '#tmpl-light-hue-group',
props: ['id','name'],
});
Vue.component('light-hue-group-controller', {
template: '#tmpl-light-hue-group-controller',
props: ['id','groups','value','collapsed'],
computed: {
lights: function() {
return this.groups[this.id].lights;
},
name: function() {
return this.groups[this.id].name;
},
properties: function() {
var self = this;
var avg = function(values) {
if (values.length) {
return values.reduce((sum,value) => sum+value) / values.length;
} else {
return 0;
}
};
var getLightValues = function(attribute) {
return Object.values(self.lights).map(
light => attribute in light.state && light.state.on ? light.state[attribute] : undefined
).filter(value => value !== undefined);
};
return {
xy: [
avg(getLightValues('xy').map(_ => parseFloat(_[0]))),
avg(getLightValues('xy').map(_ => parseFloat(_[1])))
],
ct: avg(getLightValues('ct')),
bri: avg(getLightValues('bri')),
};
},
},
methods: {
toggled: async function(event) {
await request(
'light.hue.' + (event.value ? 'on' : 'off'),
{ groups: [this.id] },
);
this.$emit('input', {
...this.value,
lights: this._updateLights('on', event.value),
});
this.$emit('input', {...this.value, state: {...this.value.state, any_on: event.value, all_on: event.value}});
},
propertiesCollapsedToggled: function() {
this.$emit('properties-collapsed-toggled', {
type: 'group',
id: this.id,
});
},
colorChanged: async function(event) {
await request(
'light.hue.xy',
{ value: event.xy, groups: [this.id] },
);
this.$emit('input', {
...this.value,
lights: this._updateLights('xy', event.xy),
});
},
briChanged: async function(event) {
await request(
'light.hue.bri',
{ value: event.bri, groups: [this.id] },
);
this.$emit('input', {
...this.value,
lights: this._updateLights('bri', event.bri),
});
},
ctChanged: async function(event) {
await request(
'light.hue.ct',
{ value: event.ct, groups: [this.id] },
);
this.$emit('input', {
...this.value,
lights: this._updateLights('ct', event.ct),
});
},
_updateLights: function(attr, value) {
var lights = [];
for (const light of Object.values(this.value.lights)) {
var state = light.state;
state[attr] = value;
lights.push({
...light,
state: state,
});
}
return lights;
},
},
});

View file

@ -0,0 +1,162 @@
Vue.component('light-hue', {
template: '#tmpl-light-hue',
props: ['config'],
data: function() {
return {
groups: {},
lights: {},
scenes: {},
selectedGroup: undefined,
selectedScene: undefined,
selectedProperties: {
type: undefined,
id: undefined,
},
};
},
methods: {
_prepareGroups: function() {
for (const [groupId, group] of Object.entries(this.groups)) {
if (group.type !== 'Room' || group.recycle) {
delete this.groups[groupId];
continue;
}
this.groups[groupId].scenes = {};
var lights = {};
for (const lightId of this.groups[groupId].lights) {
lights[lightId] = this.lights[lightId];
}
this.groups[groupId].lights = lights;
}
},
_prepareScenes: function() {
for (const [sceneId, scene] of Object.entries(this.scenes)) {
if (scene.recycle) {
delete this.scenes[sceneId];
continue;
}
this.scenes[sceneId].groups = {};
}
},
_linkLights: function() {
// Special group for lights with no group
this.groups[-1] = {
type: undefined,
lights: {},
scenes: {},
name: "[No Group]",
recycle: false,
};
for (const [lightId, light] of Object.entries(this.lights)) {
this.lights[lightId].groups = {};
for (const [groupId, group] of Object.entries(this.groups)) {
if (lightId in group.lights) {
this.lights[lightId].groups[groupId] = group;
}
}
if (!light.groups.length) {
this.groups[-1].lights[lightId] = light;
}
}
if (!this.groups[-1].lights.length) {
delete this.groups[-1];
}
},
_linkScenes: function() {
for (const [sceneId, scene] of Object.entries(this.scenes)) {
for (const lightId of scene.lights) {
for (const [groupId, group] of Object.entries(this.lights[lightId].groups)) {
this.scenes[sceneId].groups[groupId] = group;
this.groups[groupId].scenes[sceneId] = scene;
}
}
}
},
refresh: async function() {
const getLights = request('light.hue.get_lights');
const getGroups = request('light.hue.get_groups');
const getScenes = request('light.hue.get_scenes');
[this.lights, this.groups, this.scenes] = await Promise.all([getLights, getGroups, getScenes]);
this._prepareGroups();
this._prepareScenes();
this._linkLights();
this._linkScenes();
},
updatedGroup: function(event) {
for (const light of Object.values(this.groups[this.selectedGroup].lights)) {
if (event.state.any_on === event.state.all_on) {
light.state.on = event.state.all_on;
}
for (const attr in ['bri', 'xy', 'ct']) {
if (attr in event.state) {
light.state[attr] = event.state[attr];
}
}
}
},
selectScene: async function(event) {
await request(
'light.hue.scene', {
name: event.name,
groups: [this.groups[this.selectedGroup].name],
},
);
this.selectedScene = event.id;
for (const light of Object.values(this.scenes[this.selectedScene].lights)) {
this.lights[light].state.on = true;
}
},
collapsedToggled: function(event) {
if (event.type == this.selectedProperties.type
&& event.id == this.selectedProperties.id) {
this.selectedProperties = {
type: undefined,
id: undefined,
};
} else {
this.selectedProperties = {
type: event.type,
id: event.id,
};
}
},
onUnitInput: function(event) {
var groups = this.lights[event.id].groups;
for (const [groupId, group] of Object.entries(groups)) {
if (event.on) {
this.groups[groupId].state.any_on = true;
this.groups[groupId].state.all_on = Object.values(group.lights).filter((l) => l.state.on).length === Object.values(group.lights).length;
} else {
this.groups[groupId].state.all_on = false;
this.groups[groupId].state.any_on = Object.values(group.lights).filter((l) => l.state.on).length > 0;
}
}
},
},
created: function() {
this.refresh();
},
});

View file

@ -0,0 +1,11 @@
Vue.component('light-hue-scene', {
template: '#tmpl-light-hue-scene',
props: ['id','name'],
methods: {
clicked: function(event) {
this.$emit('input', {id: this.id, name: this.name});
},
},
});

View file

@ -0,0 +1,69 @@
Vue.component('light-hue-unit', {
template: '#tmpl-light-hue-unit',
props: ['id','capabilities','config','name',
'uniqueid','type','productname','modelid',
'manufacturername', 'swupdate','swversion','value',
'collapsed'],
methods: {
toggled: async function(event) {
await request(
'light.hue.' + (event.value ? 'on' : 'off'),
{ lights: [this.id] },
);
this.$emit('input', {
...this.value,
id: this.id,
on: event.value
});
},
colorChanged: async function(event) {
await request(
'light.hue.xy',
{ value: event.xy, lights: [this.id] },
);
this.$emit('input', {
...this.value,
id: this.id,
xy: event.xy
});
},
briChanged: async function(event) {
await request(
'light.hue.bri',
{ value: event.bri, lights: [this.id] },
);
this.$emit('input', {
...this.value,
id: this.id,
bri: event.bri
});
},
ctChanged: async function(event) {
await request(
'light.hue.ct',
{ value: event.ct, lights: [this.id] },
);
this.$emit('input', {
...this.value,
id: this.id,
ct: event.ct
});
},
propertiesCollapsedToggled: function() {
this.$emit('properties-collapsed-toggled', {
type: 'unit',
id: this.id
});
},
},
});

View file

@ -1,14 +0,0 @@
$(function() {
$('ul.tab-nav li a.button').click(function() {
var href = $(this).attr('href');
$('li a.active.button', $(this).parent().parent()).removeClass('active');
$(this).addClass('active');
$('.tab-pane.active', $(href).parent()).removeClass('active');
$(href).addClass('active');
return false;
});
});

View file

@ -0,0 +1,2 @@
{% include 'elements/switch.html' %}

View file

@ -0,0 +1,7 @@
<script type="text/x-template" id="tmpl-switch">
<div class="switch" :class="{glow: glow}" @click="toggled">
<input type="checkbox" v-model="value">
<label></label>
</div>
</script>

View file

@ -0,0 +1,18 @@
<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="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>
<app-header></app-header>

View file

@ -1,75 +1,73 @@
<!doctype html> <!doctype html>
<head> <head>
<title>Platypush Web Console</title> <title>Platypush Web Panel</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton.css') }}">
<!-- <link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton-tabs.css') }}"> -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/font-awesome.min.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/font-awesome.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/application.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/dist/webpanel.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/toggles.css') }}">
<script type="text/javascript" src="{{ url_for('static', filename='js/vue.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/lib/vue.js') }}"></script>
<!--<script type="text/javascript" src="{{ url_for('static', filename='js/vue.min.js') }}"></script>--> <script type="text/javascript" src="{{ url_for('static', filename='js/lib/axios.min.js') }}"></script>
<!-- <script type="text/javascript" src="{{ url_for('static', filename='js/jquery-3.3.1.min.js') }}"></script> --> <script type="text/javascript" src="{{ url_for('static', filename='js/api.js') }}"></script>
<!-- <script type="text/javascript" src="{{ url_for('static', filename='js/jquery-ui-1.12.1.min.js') }}"></script> -->
<!-- <script type="text/javascript" src="{{ url_for('static', filename='js/skeleton-tabs.js') }}"></script> --> {% for style in styles.values() %}
<link rel="stylesheet" href="{{ url_for('static', filename=style['_style_file']) }}">
{% endfor %}
<script type="text/javascript" src="{{ url_for('static', filename='js/application.js') }}"></script>
<!-- <script type="text/javascript" src="{{ url_for('static', filename='js/pushbullet.js') }}"></script> -->
<script type="text/javascript"> <script type="text/javascript">
if (!window.config) { if (!window.config) {
window.config = {}; window.config = {};
} }
window.config = { ...window.config, window.config = { ...window.config,
websocket_port: {% print(websocket_port) %}, websocket_port: {{ websocket_port }},
has_ssl: {% print('true' if has_ssl else 'false') %}, has_ssl: {% print('true' if has_ssl else 'false') %},
plugins: JSON.parse('{% print(utils.to_json(plugins))|safe %}'), templates: JSON.parse('{% print(utils.to_json(templates))|safe %}'),
scripts: JSON.parse('{% print(utils.to_json(scripts))|safe %}'),
}; };
{% if token %} {% if token %}
window.config.token = '{% print(token) %}'; window.config.token = '{{ token }}';
{% else %} {% else %}
window.config.token = undefined; window.config.token = undefined;
{% endif %} {% endif %}
</script> </script>
{% include 'elements.html' %}
</head> </head>
<body> <body>
<header> <div id="app">
<div class="row"> {% include 'header.html' %}
<div class="logo nine columns">
<span class="logo-1">Platypush</span>
<span class="logo-2">Web Panel</span>
</div>
<div id="date-time" class="three columns"> {% with plugins=templates.keys() %}
<div class="date"></div> {% include 'nav.html' %}
<div class="time"></div> {% endwith %}
</div>
</div>
</header>
<nav>
{% for plugin in plugins.keys()|sort() %}
<a href="#{% print plugin %}">
{% print plugin %}
</a>
{% endfor %}
</nav>
<main> <main>
<div id="app"> <div class="plugins-container">
{% for plugin in plugins.keys()|sort() %} {% for plugin, conf in templates.items() %}
{% with configuration=plugins[plugin], utils=utils %} {% with configuration=templates[plugin], utils=utils %}
<div class="tab-pane plugin-tab-content" id="{% print plugin %}-container"> {% include conf['_template_file'] %}
{% include 'panel/' + plugin + '/index.html' %} <plugin tag="{{ utils.plugin_name_to_tag(plugin) }}"
</div> :config="{{ conf }}" :class="{hidden: '{{ plugin }}' != selectedPlugin}"/>
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}
</div> </div>
</main> </main>
</div>
{% include 'plugins/template.html' %}
{% for script in scripts.values() %}
<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> </body>

View file

@ -1,78 +0,0 @@
<!doctype html>
<head>
<title>Platypush Web Console</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton-tabs.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/font-awesome.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/application.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/toggles.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/jquery-ui.css') }}">
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery-3.3.1.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery-ui-1.12.1.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/skeleton-tabs.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/pushbullet.js') }}"></script>
<script type="text/javascript">
window.websocket_port = {% print(websocket_port) %};
window.has_ssl = {% print('true' if has_ssl else 'false') %};
{% if token %}
window.token = '{% print(token) %}'
{% else %}
window.token = undefined
{% endif %}
</script>
</head>
<body>
<header>
<div class="row">
<div class="logo nine columns">
<span class="logo-1">Platypush</span>
<span class="logo-2">Web Panel</span>
</div>
<div id="date-time" class="three columns">
<div class="date"></div>
<div class="time"></div>
</div>
</div>
</header>
<main>
<ul class="tab-nav">
{% for plugin in plugins.keys()|sort() %}
<li>
<a class="button plugin-tab-item" href="#{% print plugin %}">
{% print plugin %}
</a>
</li>
{% endfor %}
</ul>
<div class="tab-content">
{% for plugin in plugins.keys()|sort() %}
{% with configuration=plugins[plugin], utils=utils %}
<div class="tab-pane plugin-tab-content" id="{% print plugin %}">
{% include 'plugins/' + plugin + '.html' %}
</div>
{% endwith %}
{% endfor %}
</div>
<div id="notification-container"></div>
<div id="hidden-plugins-container">
{% for plugin in hidden_plugins.keys()|sort() %}
{% set configuration = plugins[plugin] %}
<div class="plugin" id="{% print plugin %}">
{% include 'plugins/' + plugin + '.html' %}
</div>
{% endfor %}
</div>
</main>
</body>

View file

@ -0,0 +1,20 @@
<nav>
<ul class="row">
{% for plugin in plugins|sort %}
<li :class="{selected: '{{ plugin }}' == selectedPlugin}">
<a href="#{{ plugin }}" @click="selectedPlugin = '{{ plugin }}'">
{{ plugin }}
</a>
</li>
<span class="decorator" v-if="'{{ plugin }}' == selectedPlugin"></span>
{% endfor %}
<li>
<a href="#{{ plugin }}">Test tab 2</a>
</li>
<li>
<a href="#{{ plugin }}">Test tab 3</a>
</li>
</ul>
</nav>

View file

@ -1,2 +0,0 @@
IT WORKED!

View file

@ -0,0 +1,51 @@
<script type="text/x-template" id="tmpl-light-hue-property-selector">
<div class="properties color-properties">
<div class="row slider-container red-properties" v-if="value.xy">
<div class="col-2">
<div class="color-logo color-logo-red"></div>
</div>
<div class="slider-container col-10">
<input class="slider red" type="range" min="0" max="255" v-model="rgb[0]" @change="changed">
</div>
</div>
<div class="row slider-container green-properties" v-if="value.xy">
<div class="col-2">
<div class="color-logo color-logo-green"></div>
</div>
<div class="slider-container col-10">
<input class="slider green" type="range" min="0" max="255" v-model="rgb[1]" @change="changed">
</div>
</div>
<div class="row slider-container blue-properties" v-if="value.xy">
<div class="col-2">
<div class="color-logo color-logo-blue"></div>
</div>
<div class="slider-container col-10">
<input class="slider blue" type="range" min="0" max="255" v-model="rgb[2]" @change="changed">
</div>
</div>
<div class="row slider-container bri-properties" v-if="value.bri !== undefined">
<div class="col-2">
<i class="fa fa-lightbulb-o"></i>
</div>
<div class="slider-container col-10">
<input class="slider bri" type="range" min="0" max="255" v-model="value.bri" @change="changed">
</div>
</div>
<div class="row slider-container ct-properties" v-if="value.ct !== undefined">
<div class="col-2">
<i class="fa fa-thermometer"></i>
</div>
<div class="slider-container col-10">
<input class="slider ct" type="range" min="0" max="255" v-model="value.ct" @change="changed">
</div>
</div>
</div>
</script>
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/light.hue/elements.js') }}"></script>

View file

@ -0,0 +1,28 @@
<script type="text/x-template" id="tmpl-light-hue-group">
<div class="group">
<div v-text="name" @click="$emit('set-selected')"></div>
</div>
</script>
<script type="text/x-template" id="tmpl-light-hue-group-controller">
<div class="group-controller">
<div class="row vertical-center">
<div class="col-10" @click="propertiesCollapsedToggled">All Lights</div>
<div class="col-2 pull-right">
<toggle-switch v-model="value.state.any_on" :glow="true" @toggled="toggled"></toggle-switch>
</div>
</div>
<div class="row">
<light-hue-property-selector
v-model="properties"
:class="{hidden: collapsed}"
@bri-changed="briChanged"
@color-changed="colorChanged"
@ct-changed="ctChanged">
</light-hue-property-selector>
</div>
</div>
</script>
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/light.hue/groups.js') }}"></script>

View file

@ -0,0 +1,69 @@
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/light.hue/converter.js') }}"></script>
{% include 'plugins/light.hue/elements.html' %}
{% include 'plugins/light.hue/groups.html' %}
{% include 'plugins/light.hue/scenes.html' %}
{% include 'plugins/light.hue/units.html' %}
<script type="text/x-template" id="tmpl-light-hue">
<div class="row light-hue-container">
<div class="groups col-no-margin-3 col-s-12">
<div class="title">Rooms</div>
<light-hue-group
v-for="(group, id) in groups"
:key="id"
:id="id"
:name="group.name"
:class="{selected: selectedGroup == id}"
@set-selected="selectedGroup = id">
</light-hue-group>
</div>
<div class="scenes col-no-margin-3 col-s-12">
<div class="title">Scenes</div>
<light-hue-scene
v-for="(scene, id) in scenes"
:key="id"
:id="id"
:name="scene.name"
:class="{hidden: !(selectedGroup in scene.groups), selected: selectedScene == id}"
@input="selectScene">
</light-hue-scene>
</div>
<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]"
:id="selectedGroup"
:groups="groups"
:collapsed="!(selectedProperties.type == 'group' && selectedProperties.id == selectedGroup)"
@properties-collapsed-toggled="collapsedToggled"
@input="updatedGroup">
</light-hue-group-controller>
<light-hue-unit
v-for="(light, id) in lights"
v-model="light.state"
:key="id"
:id="id"
:name="light.name"
:config="light.config"
:capabilities="light.capabilities"
:modelid="light.modelid"
:manufacturername="light.manufacturername"
:swupdate="light.swupdate"
:swversion="light.swversion"
:uniqueid="light.uniqueid"
:type="light.type"
:productname="light.productname"
:collapsed="!(selectedProperties.type == 'unit' && selectedProperties.id == id)"
:class="{hidden: !(selectedGroup in light.groups)}"
@input="onUnitInput"
@properties-collapsed-toggled="collapsedToggled">
</light-hue-unit>
</div>
</div>
</script>

View file

@ -0,0 +1,8 @@
<script type="text/x-template" id="tmpl-light-hue-scene">
<div class="scene" @click="clicked">
<div v-text="name"></div>
</div>
</script>
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/light.hue/scenes.js') }}"></script>

View file

@ -0,0 +1,22 @@
<script type="text/x-template" id="tmpl-light-hue-unit">
<div class="unit">
<div class="row vertical-center">
<div class="name col-10" v-text="name" @click="propertiesCollapsedToggled"></div>
<div class="col-2 pull-right">
<toggle-switch v-model="value.on" @toggled="toggled"></toggle-switch>
</div>
</div>
<div class="row">
<light-hue-property-selector
v-model="value"
:class="{hidden: collapsed}"
@bri-changed="briChanged"
@color-changed="colorChanged"
@ct-changed="ctChanged">
</light-hue-property-selector>
</div>
</div>
</script>
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/light.hue/units.js') }}"></script>

View file

@ -0,0 +1,6 @@
<script type="text/x-template" id="tmpl-plugin">
<div class="plugin-container" :class="{selected: selected}">
<component :is="tag" :config="config"/>
</div>
</script>

View file

@ -96,5 +96,9 @@ class HttpUtils(object):
def get_config(cls, attr): def get_config(cls, attr):
return Config.get(attr) return Config.get(attr)
@classmethod
def plugin_name_to_tag(cls, module_name):
return module_name.replace('.','-')
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -135,3 +135,6 @@ websocket-client
# mpv player plugin # mpv player plugin
python-mpv python-mpv
# SCSS/SASS to CSS compiler for web pages style
pyScss

View file

@ -2,10 +2,67 @@
import errno import errno
import os import os
import distutils.cmd
from distutils.command.build import build
from setuptools import setup, find_packages from setuptools import setup, find_packages
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read() class WebBuildCommand(distutils.cmd.Command):
"""
Custom command to build the web files
"""
description = 'Build components and styles for the web pages'
user_options = []
@staticmethod
def generate_css_files():
from scss import Compiler
print('Building CSS files')
base_path = path(os.path.join('platypush','backend','http','static','css'))
input_path = path(os.path.join(base_path,'source'))
output_path = path(os.path.join(base_path,'dist'))
for root, dirs, files in os.walk(input_path):
scss_file = os.path.join(root, 'index.scss')
if os.path.isfile(scss_file):
css_path = os.path.split(scss_file[len(input_path):])[0][1:] + '.css'
css_dir = os.path.join(output_path, os.path.dirname(css_path))
css_file = os.path.join(css_dir, os.path.basename(css_path))
os.makedirs(css_dir, exist_ok=True)
print('\tGenerating CSS {scss} -> {css}'.format(scss=scss_file, css=css_file))
with open(css_file, 'w') as f:
css_content = Compiler(output_style='compressed', search_path=[root, input_path]).compile(scss_file)
f.write(css_content)
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
self.generate_css_files()
class BuildCommand(build):
def run(self):
build.run(self)
self.run_command('web_build')
def path(fname=''):
return os.path.abspath(os.path.join(os.path.dirname(__file__), fname))
def readfile(fname):
with open(path(fname)) as f:
return f.read()
def pkg_files(dir): def pkg_files(dir):
paths = [] paths = []
@ -14,6 +71,7 @@ def pkg_files(dir):
paths.append(os.path.join('..', path, file)) paths.append(os.path.join('..', path, file))
return paths return paths
def create_etc_dir(): def create_etc_dir():
path = '/etc/platypush' path = '/etc/platypush'
try: try:
@ -26,6 +84,7 @@ def create_etc_dir():
else: else:
raise raise
plugins = pkg_files('platypush/plugins') plugins = pkg_files('platypush/plugins')
backend = pkg_files('platypush/backend') backend = pkg_files('platypush/backend')
# create_etc_dir() # create_etc_dir()
@ -37,8 +96,8 @@ setup(
author_email = "info@fabiomanganiello.com", author_email = "info@fabiomanganiello.com",
description = ("Platypush service"), description = ("Platypush service"),
license = "MIT", license = "MIT",
python_requires = '>= 3', python_requires = '>= 3.5',
keywords = "pushbullet notifications automation", keywords = "home-automation iot mqtt websockets redis dashboard notificaions",
url = "https://github.com/BlackLight/platypush", url = "https://github.com/BlackLight/platypush",
packages = find_packages(), packages = find_packages(),
include_package_data = True, include_package_data = True,
@ -50,10 +109,14 @@ setup(
], ],
}, },
scripts = ['bin/platyvenv'], scripts = ['bin/platyvenv'],
cmdclass = {
'web_build': WebBuildCommand,
'build': BuildCommand,
},
# data_files = [ # data_files = [
# ('/etc/platypush', ['platypush/config.example.yaml']) # ('/etc/platypush', ['platypush/config.example.yaml'])
# ], # ],
long_description = read('README.md'), long_description = readfile('README.md'),
classifiers = [ classifiers = [
"Topic :: Utilities", "Topic :: Utilities",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
@ -99,6 +162,7 @@ setup(
'Support for web media subtitles': ['webvtt-py'], 'Support for web media subtitles': ['webvtt-py'],
'Support for mopidy backend': ['websocket-client'], 'Support for mopidy backend': ['websocket-client'],
'Support for mpv player plugin': ['python-mpv'], 'Support for mpv player plugin': ['python-mpv'],
'Support for compiling SASS/SCSS styles to CSS': ['pyScss'],
# 'Support for Leap Motion backend': ['git+ssh://git@github.com:BlackLight/leap-sdk-python3.git'], # 'Support for Leap Motion backend': ['git+ssh://git@github.com:BlackLight/leap-sdk-python3.git'],
# 'Support for Flic buttons': ['git+https://@github.com/50ButtonsEach/fliclib-linux-hci.git'] # 'Support for Flic buttons': ['git+https://@github.com/50ButtonsEach/fliclib-linux-hci.git']
# 'Support for media subtitles': ['git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles'] # 'Support for media subtitles': ['git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles']