Dashboard rewritten in vue.js

This commit is contained in:
Fabio Manganiello 2019-07-07 20:11:32 +02:00
parent 8006f3688c
commit 09165ca0ff
74 changed files with 1129 additions and 3769 deletions

1
.gitignore vendored
View file

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

View file

@ -3,4 +3,3 @@ from platypush import main
main() main()
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,6 +1,6 @@
from flask import Blueprint, request, render_template from flask import Blueprint, request, render_template
from platypush.backend.http.app import template_folder from platypush.backend.http.app import template_folder, static_folder
from platypush.backend.http.app.utils import authenticate, authentication_ok, \ from platypush.backend.http.app.utils import authenticate, authentication_ok, \
get_websocket_port get_websocket_port
@ -14,17 +14,21 @@ __routes__ = [
dashboard, dashboard,
] ]
@dashboard.route('/dashboard', methods=['GET']) @dashboard.route('/dashboard', methods=['GET'])
def dashboard(): def dashboard():
""" Route for the fullscreen dashboard """ """ Route for the fullscreen dashboard """
if not authentication_ok(request): return authenticate() if not authentication_ok(request):
return authenticate()
http_conf = Config.get('backend.http') http_conf = Config.get('backend.http')
dashboard_conf = http_conf.get('dashboard', {}) dashboard_conf = http_conf.get('dashboard', {})
return render_template('dashboard.html', config=dashboard_conf, return render_template('dashboard.html', config=dashboard_conf,
utils=HttpUtils, token=Config.get('token'), utils=HttpUtils, token=Config.get('token'),
websocket_port=get_websocket_port()) static_folder=static_folder, template_folder=template_folder,
websocket_port=get_websocket_port(),
has_ssl=http_conf.get('ssl_cert') is not None)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,95 +0,0 @@
import datetime
import dateutil.parser
from flask import abort, request, render_template, Blueprint
from platypush.backend.http.app import template_folder
from platypush.backend.http.app.utils import authenticate, authentication_ok
from platypush.config import Config
map_ = Blueprint('map', __name__, template_folder=template_folder)
# Declare routes list
__routes__ = [
map_,
]
def parse_time(time_string):
if not time_string:
return None
now = datetime.datetime.now()
if time_string == 'now':
return now.isoformat()
if time_string == 'yesterday':
return (now - datetime.timedelta(days=1)).isoformat()
try:
return dateutil.parser.parse(time_string).isoformat()
except ValueError:
pass
m = re.match('([-+]?)([0-9]+)([dhms])', time_string)
if not m:
raise RuntimeError('Invalid time interval string representation: "{}"'.
format(time_string))
time_delta = (-1 if m.group(1) == '-' else 1) * int(m.group(2))
time_unit = m.group(3)
if time_unit == 'd':
params = { 'days': time_delta }
elif time_unit == 'h':
params = { 'hours': time_delta }
elif time_unit == 'm':
params = { 'minutes': time_delta }
elif time_unit == 's':
params = { 'seconds': time_delta }
return (now + datetime.timedelta(**params)).isoformat()
@map_.route('/map', methods=['GET'])
def map():
"""
Query parameters:
start -- Map timeline start timestamp
end -- Map timeline end timestamp
zoom -- Between 1-20. Set it if you want to override the
Google's API auto-zoom. You may have to set it if you are
trying to embed the map into an iframe
Supported values for `start` and `end`:
- now
- yesterday
- -30s (it means '30 seconds ago')
- -10m (it means '10 minutes ago')
- -24h (it means '24 hours ago')
- -7d (it means '7 days ago')
- 2018-06-04T17:39:22.742Z (ISO strings)
Default: start=yesterday, end=now
"""
if not authentication_ok(request): return authenticate()
map_conf = (Config.get('backend.http') or {}).get('maps', {})
if not map_conf:
abort(500, 'The maps plugin is not configured in backend.http')
api_key = map_conf.get('api_key')
if not api_key:
abort(500, 'Google Maps api_key not set in the maps configuration')
start = parse_time(request.args.get('start', default='yesterday'))
end = parse_time(request.args.get('end', default='now'))
zoom = request.args.get('zoom', default=None)
return render_template('map.html', config=map_conf, utils=HttpUtils,
start=start, end=end, zoom=zoom,
token=Config.get('token'), api_key=api_key,
websocket_port=get_websocket_port())
# vim:sw=4:ts=4:et:

View file

@ -1,227 +0,0 @@
body {
width: 100%;
overflow-x: hidden;
}
header {
width: 100%;
background: #f4f5f6;
padding: 10px 25px;
margin: 0px 10px 35px -10px;
border-bottom: 1px solid #e1e4e8;
}
.logo {
font-size: 25px;
margin-top: 10px;
}
.logo > .logo-1 {
font-weight: bold;
}
.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);
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.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: #f0f0f0;
border-radius: 10px 10px 0 0;
text-transform: uppercase;
font-weight: 400px;
letter-spacing: .1rem;
line-height: 38px;
}
.modal-body {
padding: 2.5rem 2rem 1.5rem 2rem;
}
.form-footer {
text-align: right;
margin-top: 2rem;
border-top: 1px solid #ddd;
}
.form-footer * > input[type=button],
.form-footer * > button {
margin-top: 2rem;
text-transform: uppercase;
font-size: 1.3rem;
}
#date-time {
text-align: right;
padding-right: 30px;
}
#date-time > .date {
color: #666;
}
#date-time > .time {
font-weight: bold;
font-size: 30px;
}
main {
width: 95%;
margin: auto;
}
ul.tab-nav {
padding-left: 0 !important; /* Override skeleton-tabs default */
}
.tab-content {
border: 1px solid #bbb;
border-top: 0;
padding: 20px;
margin-top: -25px;
border-radius: 0 0 7.5px 7.5px;
}
button[disabled] {
color: #bbb;
border: 1px solid;
}
.button[disabled] {
background: rgba(240,240,240,1)
}
.slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 15px;
border-radius: 5px;
background: #e4e4e4;
outline: none;
opacity: 0.7;
-webkit-transition: .2s;
transition: opacity .2s;
}
.slider:hover {
opacity: 1;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 25px;
height: 25px;
border-radius: 50%;
background: #4CAF50;
cursor: pointer;
}
.slider[disabled]::-webkit-slider-thumb {
display: none;
}
.slider::-moz-range-thumb {
width: 25px;
height: 25px;
background: #4CAF50;
cursor: pointer;
}
.btn-primary {
background-color: #d8ffe0 !important;
border: 1px solid #98efb0 !important;
}
.right-side {
text-align: right !important;
}
#notification-container {
position: fixed;
bottom: 0;
right: 0;
width: 25em;
}
#notification-container > .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-container > .notification:hover {
background: rgba(160,235,168,0.9);
}
#notification-container * > .notification-title {
padding: 4px 10px;
font-weight: bold;
text-transform: capitalize;
line-height: 30px;
letter-spacing: .1rem;
}
#notification-container * > .notification-body {
height: 6em;
overflow: hidden;
padding-bottom: 1rem;
letter-spacing: .05rem;
}
#notification-container * > .notification-text {
margin-left: 0 !important;
}
#notification-container * > .notification-image {
height: 100%;
text-align: center;
padding-top: .6em;
padding-bottom: .6em;
}
#notification-container * > .notification-image-item {
width: 100%;
height: 100%;
margin: auto;
}
#notification-container * > .fa.notification-image-item {
margin-top: .8em;
margin-left: .2em;
font-size: 25px;
}
#notification-container * > .notification-image img {
width: 80%;
height: 80%;
}
#hidden-plugins-container > .plugin {
display: none;
}

View file

@ -1,20 +0,0 @@
html {
min-height: 100%;
}
body {
background-image: url('/img/dashboard-background.jpg');
background-size: 100% 100%;
background-repeat: no-repeat;
font-family: Lato;
}
.widget {
background: white;
margin-top: 2em;
border-radius: 5px;
height: 22.5em;
overflow: hidden;
box-shadow: 0 3px 3px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08);
}

View file

@ -1,32 +0,0 @@
#gpio-container {
background-color: #f8f8f8;
padding: 12px;
border: 1px solid #ddd;
border-radius: 10px;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 400;
line-height: 38px;
letter-spacing: .1rem;
}
.gpio-pin {
padding: 1.5em 1em;
border-radius: 10px;
}
.gpio-pin:nth-of-type(odd) {
background-color: #ececec;
}
.gpio-pin:hover {
background-color: #daf8e2 !important;
}
.pin-value {
text-align: right;
}
.pin-ctrl {
width: 8em;
}

View file

@ -1,25 +0,0 @@
#sensors-container {
width: 80%;
max-width: 60rem;
margin: 3em auto;
background: rgba(235,235,235,0.8);
padding: 2em;
border: 1px solid rgba(220,220,220,1.0);
border-radius: 10px;
}
.sensor-data {
text-transform: capitalize;
}
.sensor-data:not(:last-child) {
margin-bottom: .5rem;
padding-bottom: .5rem;
border-bottom: 1px solid rgba(220,220,220,1.0);
}
.sensor-value {
text-align: right;
font-weight: bold;
}

View file

@ -1,24 +0,0 @@
#zb-container {
outline: none !important;
}
.zb-controls-container {
margin: auto;
width: 25%;
}
.zb-ctrl-btn {
white-space: normal;
height: 6rem;
}
.zb-ctrl-btn.selected {
color: #78ff00 !important;
}
.ctrl-bottom-row {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ddd;
}

File diff suppressed because it is too large Load diff

View file

@ -1,9 +0,0 @@
#map {
width: 100%;
height: 100%;
position: static !important;
margin: 0;
padding: 0;
overflow: hidden;
}

View file

@ -1,180 +0,0 @@
#video-search {
max-width: 60em;
margin: 1em auto;
}
#video-search input[type=text] {
width: 100%;
}
form#video-ctrl {
text-align: center;
}
form#video-ctrl * > button[data-modal="#media-subtitles-modal"] {
display: none;
}
#video-seeker-container {
margin-top: 0.5em;
margin-bottom: 1em;
}
#video-volume-ctrl-container {
margin-top: 1em;
}
#video-results {
padding: 1.5rem 1.5rem 0 .5rem;
background: #f8f8f8;
}
.video-result {
padding: 5px;
letter-spacing: .1rem;
line-height: 3.3rem;
cursor: pointer;
}
.video-icon-container {
font-size: 2rem;
margin-right: 2rem;
}
.video-result.selected {
background-color: #c8ffd0 !important;
}
.video-result:hover {
background-color: #daf8e2 !important;
}
.video-result:nth-child(odd) {
background-color: #f2f2f2;
}
.video-result.active {
height: 4rem;
padding-top: 1.5rem;
font-size: 1.7rem;
border-radius: 10px;
animation: active-track 5s;
-moz-animation: active-track 5s infinite;
-webkit-animation: active-track 5s infinite;
}
@keyframes active-track {
0% { background: #d4ffe3; }
50% { background: #9cdfb0; }
100% { background: #d4ffe3; }
}
@-moz-keyframes active-track {
0% { background: #d4ffe3; }
50% { background: #9cdfb0; }
100% { background: #d4ffe3; }
}
@-webkit-keyframes active-track {
0% { background: #d4ffe3; }
50% { background: #9cdfb0; }
100% { background: #d4ffe3; }
}
button.remote[data-panel="#media-devices-panel"] {
color: #34b868;
}
#media-devices-panel,
#media-item-panel {
display: none;
position: absolute;
padding: 1rem;
background: #f0f0f0;
z-index: 10;
border: 1px solid #d0d0d0;
border-radius: 5px;
min-width: 10em;
}
#media-devices-panel .refresh-devices-container {
position: relative;
height: 1em;
}
#media-devices-panel * > .refresh-devices {
text-align: right;
cursor: pointer;
position: absolute;
top: -.5rem;
right: .5rem;
}
#media-devices-panel * > .cast-device,
#media-item-panel > .media-item-action {
padding: 0.5rem;
cursor: pointer;
}
#media-devices-panel * > .cast-device-local {
border-bottom: 1px solid #ddd;
margin-bottom: 0.25rem;
}
#media-devices-panel * > .cast-device.selected {
font-weight: bold;
color: #34b868;
}
#media-devices-panel * > .cast-device.disabled,
#media-devices-panel * > .refresh-devices.disabled,
#media-item-panel > .media-item-action.disabled {
cursor: default;
color: #999 !important;
}
#media-devices-panel * > .cast-device:hover,
#media-item-panel > .media-item-action:hover {
background-color: #daf8e2 !important;
}
#media-devices-panel * > .cast-device-icon
#media-item-panel > .media-item-item {
color: #666;
}
#media-subtitles-modal * > .media-subtitles-results-container {
display: none;
padding: .75rem;
}
#media-subtitles-modal * > .media-subtitles-results-header {
background: #eee;
margin-bottom: 1rem;
padding: 1rem .25rem;
border: 1px solid #ccc;
}
#media-subtitles-modal * > .media-subtitles-results {
padding: .75rem;
}
#media-subtitles-modal * > .media-subtitles-message {
display: none;
}
#media-subtitles-modal * > .media-subtitle-container {
cursor: pointer;
}
#media-subtitles-modal * > .media-subtitle-container:nth-child(odd) {
background-color: #f2f2f2;
}
#media-subtitles-modal * > .media-subtitle-container.selected {
background-color: #c8ffd0 !important;
}
#media-subtitles-modal * > .media-subtitle-container:hover {
background-color: #daf8e2 !important;
}

View file

@ -1,173 +0,0 @@
#player-left-side {
overflow-y: hidden;
}
#player-right-side {
margin-left: 0;
width: 78%;
}
.playback-controls {
text-align: center;
border-bottom: 1px solid #e8eaf0;
padding-bottom: 12px;
}
.playback-controls * > button.enabled {
color: #59df3e;
}
.track-info {
text-align: center;
margin: -20px -20px 0 -20px;
padding: 10px 20px;
}
.track-info > .artist {
font-weight: bold;
display: block;
}
#playlist-controls, #browser-controls {
margin-bottom: 7.5px;
padding-bottom: 7.5px;
border-bottom: 1px solid #ddd;
height: 3.8rem;
}
#playlist-controls {
text-align: right;
}
#playlist-content, #music-browser {
height: 27.2rem;
overflow-y: scroll;
}
#music-browser {
padding-top: 0;
}
#browser-filter {
width: 95%;
margin-bottom: 1.5rem;
}
#playlist-filter-container {
height: 5rem;
}
#playlist-filter {
width: 100%;
margin-bottom: 1.5rem;
}
.music-pane {
height: 40rem;
padding: 15px 15px 0 5px;
background: #f8f8f8;
}
.music-item {
padding: 5px;
cursor: pointer;
}
.music-item.selected {
background-color: #c8ffd0 !important;
}
.music-item:hover {
background-color: #daf8e2 !important;
}
.music-item:nth-child(odd) {
background-color: #f2f2f2;
}
.playlist-track.active {
height: 4rem;
padding-top: 1.5rem;
font-size: 1.7rem;
border-radius: 10px;
animation: active-track 5s;
-moz-animation: active-track 5s infinite;
-webkit-animation: active-track 5s infinite;
}
@keyframes active-track {
0% { background: #d4ffe3; }
50% { background: #9cdfb0; }
100% { background: #d4ffe3; }
}
@-moz-keyframes active-track {
0% { background: #d4ffe3; }
50% { background: #9cdfb0; }
100% { background: #d4ffe3; }
}
@-webkit-keyframes active-track {
0% { background: #d4ffe3; }
50% { background: #9cdfb0; }
100% { background: #d4ffe3; }
}
.playlist-track > .track-time {
text-align: right;
color: #666;
}
#track-seeker-container {
margin-bottom: 10px;
}
#volume-ctrl-container {
margin-top: 15px;
}
#music-search-form {
margin-bottom: 1rem;
}
#music-search-form * > input[type=text] {
width: 100%;
}
#music-search-form > .row {
padding: 0.5rem;
}
.music-form-bottom {
text-align: right;
margin-top: 2rem;
border-top: 1px solid #ddd;
}
.music-form-bottom input {
margin-top: 2rem;
}
#music-search-results-form {
display: none;
margin-top: -2rem;
}
#music-search-results-container {
display: none;
max-height: 50rem;
margin-top: -1.4rem;
overflow-y: auto;
overflow-x: hidden;
}
#music-search-results-head {
padding: 0.5rem 1rem;
background-color: #eaeaea;
border-radius: 5px 5px 0 0;
border-bottom: 1px solid #bbb;
letter-spacing: .1rem;
line-height: 3.5rem;
}

View file

@ -1,153 +0,0 @@
.snapcast-host-container {
min-width: 40em;
max-width: 80em;
margin: 1em auto;
background: rgba(245,245,245,0.6);
border: 1px solid rgba(220,220,220,1.0);
border-radius: 10px;
}
.snapcast-host-header {
border-bottom: 1px solid rgba(220,220,220,1.0);
font-size: 1em;
text-transform: uppercase;
padding: 0 .5em;
}
.snapcast-host-header h1 {
font-size: 1.9em;
}
.snapcast-group-header {
padding: .5em;
margin: 0 1.4em 1.8em .2em;
border-bottom: 1px solid #e8e8e8;
}
.snapcast-settings-btn {
cursor: pointer;
padding: .2em;
}
.snapcast-settings-btn:hover {
border: .05em solid #e0e0e0;
padding: .15em;
}
.snapcast-group-header h2 {
font-size: 1.5em;
margin-top: .6em;
}
.snapcast-client-disconnected {
color: rgba(0, 0, 0, 0.35);
}
.snapcast-client-row {
padding: 0 1.4em;
margin: 1.5em .5em;
}
.snapcast-client-row h3 {
font-size: 1.2em;
}
.snapcast-client-mute-toggle {
margin-top: -1.2em;
}
.snapcast-form {
margin-bottom: 1rem;
}
.snapcast-form * > input[type=text] {
width: 100%;
}
.snapcast-form > .row {
padding: 0.5rem;
}
.snapcast-form-bottom {
text-align: right;
margin-top: 2rem;
border-top: 1px solid #ddd;
}
.snapcast-form-bottom input {
margin-top: 2rem;
}
.snapcast-form * > label {
transform: translateY(25%);
}
.snapcast-form > .snapcast-client-info,
.snapcast-form > .snapcast-host-info,
.snapcast-form > .snapcast-group-clients-container {
margin: 1em auto .5em auto;
padding: 2em;
background: rgba(240,240,240,0.6);
border-radius: 10px;
border: .05em solid rgba(225,225,225,1.0)
}
.snapcast-form > .snapcast-group-clients-container {
max-width: 40em;
}
.snapcast-form > .snapcast-host-info,
.snapcast-form > .snapcast-client-info {
max-width: 70%;
}
.snapcast-form > .snapcast-host-info > .row,
.snapcast-form > .snapcast-client-info > .row {
padding: .2em;
}
.snapcast-form > .snapcast-host-info > .row:hover,
.snapcast-form > .snapcast-client-info > .row:hover {
background-color: #daf8e2 !important;
}
.snapcast-form > .snapcast-host-info * > .info-name,
.snapcast-form > .snapcast-client-info * > .info-name {
font-weight: bold;
}
.snapcast-form > .snapcast-host-info * > .info-value,
.snapcast-form > .snapcast-client-info * > .info-value {
text-align: right;
}
.snapcast-form > .snapcast-group-stream,
.snapcast-form > .snapcast-client-delete {
width: 30%;
margin: 1em auto 0 auto;
text-align: center;
}
.snapcast-form > .snapcast-group-stream > label,
.snapcast-form > .snapcast-client-delete > label {
display: inline;
margin-left: .5em;
text-align: right;
}
.snapcast-form > .snapcast-client-delete > label {
color: rgba(200, 44, 23, 1.0);
}
.snapcast-form * > .snapcast-group-clients {
max-width: 15em;
margin: auto;
text-align: right;
}
.snapcast-form * > .snapcast-group-clients * > label {
display: inline;
float: right;
width: 80%;
}

View file

@ -0,0 +1,49 @@
@import 'common/vars';
@import 'common/mixins';
@import 'common/layout';
@import 'common/elements';
@import 'common/animations';
@import 'common/modal';
@import 'common/notifications';
$background-image: url('/img/dashboard-background.jpg') !default;
$background-color: white !default;
$font-family: Lato !default;
html {
min-height: 100%;
}
body {
--background-image: $background-image;
--background-color: $background-color;
background-image: var(--background-image);
background-color: var(--background-color);
background-size: 100% 100%;
background-repeat: no-repeat;
font-family: $font-family;
}
main {
display: flex;
flex-flow: column;
width: 100%;
height: 100%;
margin: 0;
.widgets-row {
margin: 2em 1em;
display: flex;
}
.widget {
background: $background-color;
border-radius: 5px;
height: 22.5em;
overflow: hidden;
box-shadow: 0 3px 3px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08);
}
}

View file

@ -0,0 +1,16 @@
@import 'common/vars';
.widget .calendar {
padding: 1rem;
.upcoming-event {
text-align: center;
margin-bottom: 1.5rem;
.summary {
text-transform: uppercase;
font-size: 1.35em;
}
}
}

View file

@ -0,0 +1,48 @@
@import 'common/vars';
.widget .date-time-weather {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 1rem;
.date {
font-size: 1.15em;
height: 8%;
}
.time {
font-size: 1.7em;
height: 11%;
}
.weather {
height: 40%;
display: flex;
align-items: center;
.temperature {
font-size: 2em;
margin-left: 1rem;
}
}
.summary {
height: 28%;
}
.sensors {
width: 100%;
height: 13%;
.sensor {
padding: 0 1rem;
}
.humidity {
text-align: right;
}
}
}

View file

@ -0,0 +1,30 @@
@import 'common/vars';
.widget .image-carousel {
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0,0,0,0);
.background {
position: absolute;
top: 0;
width: 100%;
height: 100vh;
background-color: rgba(0,0,0,0);
background-position: center;
background-size: cover;
background-repeat: no-repeat;
filter: blur(13px);
-webkit-filter: blur(13px);
}
img {
position: absolute;
max-height: 100%;
z-index: 2;
}
}

View file

@ -0,0 +1,92 @@
@import 'common/vars';
$progress-bar-bg: #ddd;
$playback-status-color: #757f70;
.widget .music {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
.track {
text-align: center;
.unknown,
.no-track {
font-size: 1.5em;
}
.artist {
font-size: 1.4em;
font-weight: bold;
margin-bottom: .25em;
}
.title {
font-size: 1.25em;
}
}
.time {
width: 100%;
margin-top: .75em;
.row {
padding: 0 .5em;
}
.time-total {
text-align: right;
}
.progress-bar {
width: 100%;
height: 1em;
position: relative;
margin-bottom: .75em;
.total {
position: absolute;
width: 100%;
height: 100%;
top: 0;
background: $progress-bar-bg;
border-radius: 5rem;
}
.elapsed {
position: absolute;
width: 100%;
height: 100%;
top: 0;
background: $selected-bg;
border-radius: 5rem;
z-index: 1;
}
}
}
.playback-status {
position: absolute;
bottom: 0;
border-top: $default-border-2;
color: $playback-status-color;
width: 100%;
height: 2em;
.status-property {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.active {
color: $default-hover-fg;
}
}
}

View file

@ -0,0 +1,29 @@
@import 'common/vars';
.widget .rss-news {
height: 100%;
display: flex;
align-items: center;
.article {
width: 100%;
padding: 0 2em;
.source {
font-size: 1.25em;
font-weight: bold;
margin-bottom: .5em;
}
.title {
font-size: 1.25em;
margin-bottom: .5em;
}
.published {
text-align: right;
font-size: .9em;
}
}
}

View file

@ -1,29 +0,0 @@
#switchbot-container {
background-color: #f8f8f8;
padding: 12px;
border: 1px solid #ddd;
border-radius: 10px;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 400;
line-height: 38px;
letter-spacing: .1rem;
}
.switchbot-device {
padding: 1.5em 1em .2em .5em;
border-radius: 10px;
}
.switchbot-device:nth-of-type(odd) {
background-color: #ececec;
}
.switchbot-device:hover {
background-color: #daf8e2 !important;
}
.toggle-container {
text-align: right;
padding-right: 1em;
}

View file

@ -1,29 +0,0 @@
#tplink-container {
background-color: #f8f8f8;
padding: 12px;
border: 1px solid #ddd;
border-radius: 10px;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 400;
line-height: 38px;
letter-spacing: .1rem;
}
.tplink-device {
padding: 1.5em 1em .2em .5em;
border-radius: 10px;
}
.tplink-device:nth-of-type(odd) {
background-color: #ececec;
}
.tplink-device:hover {
background-color: #daf8e2 !important;
}
.toggle-container {
text-align: right;
padding-right: 1em;
}

View file

@ -1,29 +0,0 @@
#wemo-container {
background-color: #f8f8f8;
padding: 12px;
border: 1px solid #ddd;
border-radius: 10px;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 400;
line-height: 38px;
letter-spacing: .1rem;
}
.wemo-device {
padding: 1.5em 1em .2em .5em;
border-radius: 10px;
}
.wemo-device:nth-of-type(odd) {
background-color: #ececec;
}
.wemo-device:hover {
background-color: #daf8e2 !important;
}
.toggle-container {
text-align: right;
padding-right: 1em;
}

View file

@ -1,9 +0,0 @@
#tts-container {
max-width: 60em;
margin: 3em auto;
}
#tts-form input[type=text] {
width: 100%;
}

View file

@ -1,30 +0,0 @@
.calendar-container {
padding: 2rem;
overflow: hidden;
}
.calendar-container * > .date,
.calendar-container * > .time {
font-size: 0.9em;
}
.calendar-next-event-container {
width: 70%;
margin: auto;
text-align: center;
}
.calendar-next-event-container > .summary {
font-size: 1.5em;
text-transform: uppercase;
}
.calendar-events-list-container {
margin-top: 2.5rem;
}
.calendar-event > .date,
.calendar-event > .time {
padding-top: .15rem;
}

View file

@ -1,50 +0,0 @@
.widget.date-time-weather {
text-align: center;
}
.date-time-weather-container {
padding: 2rem;
position: relative;
min-height: 20.5em;
}
#weather-icon {
margin-left: 2rem;
}
h1.temperature {
font-size: 45px;
margin: 4rem 2rem;
font-weight: bold;
}
h1.temperature > [data-bind=temperature] {
margin-left: -2.5rem;
}
.widget.date-time-weather * > .time {
font-size: 22px;
}
.widget.date-time-weather * > .sensors {
position: absolute;
bottom: 2rem;
width: 90%;
margin-left: -2rem;
padding-left: 1rem;
}
.widget.date-time-weather * > .sensor-temperature,
.widget.date-time-weather * > .sensor-humidity {
display: none;
font-weight: bold;
}
.widget.date-time-weather * > .sensor-temperature {
text-align: left;
}
.widget.date-time-weather * > .sensor-humidity {
text-align: right;
}

View file

@ -1,33 +0,0 @@
.carousel {
overflow: hidden;
color: #999;
position: relative;
width: 100%;
height: 22.5em;
}
.carousel > img {
position: absolute;
top: 0;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
border-radius: 5px;
z-index: 2;
}
.carousel-background {
position: absolute;
width: 100%;
height: 22.5em;
top: 0;
left: 0;
background-color: black;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
filter: blur(10px);
z-index: 1;
}

View file

@ -1,68 +0,0 @@
.music-container {
position: relative;
width: inherit;
height: inherit;
display: table-cell;
vertical-align: middle;
text-align: center;
}
.track-info {
font-size: 20px;
}
.track-info > .artist {
font-weight: bold;
margin-bottom: 1rem;
}
.track-info > .title {
margin-bottom: 2rem;
}
.time-bar {
height: 7.5px;
background: #ddd;
margin: 7.5px;
border-radius: 10px;
}
.time-bar > .elapsed {
height: 7.5px;
background: #98ffb0;
border-radius: 10px;
}
.time-elapsed {
text-align: left;
margin-left: 1rem;
}
.time-total {
float: right;
margin-right: 1rem;
}
.time-elapsed, .time-total {
color: rgba(0, 0, 0, 0.7);
letter-spacing: 1px;
}
.no-track-info {
font-size: 25px;
}
.playback-status-container {
position: absolute;
bottom: 0;
width: 100%;
border-top: 1px solid #ddd;
padding: 1rem 0;
font-size: 1.22rem;
color: #757f70;
}
.playback-status-container > .playback-status-values {
font-weight: bold;
}

View file

@ -1,28 +0,0 @@
.news-container {
position: relative;
height: 100%;
}
.news-container > .article {
width: 80%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.news-container > .article > .source {
font-weight: bold;
margin-bottom: 1rem;
font-size: 1.7em;
}
.news-container > .article > .title {
font-size: 1.5em;
}
.news-container > .article > .publish-time {
margin-top: 1rem;
float: right;
}

View file

@ -1,46 +1,75 @@
$(document).ready(function() { Vue.component('widget', {
var onEvent = function(event) { template: '#tmpl-widget',
if (event.args.type == 'platypush.message.event.web.widget.WidgetUpdateEvent') { props: ['config','tag'],
var $widget = $('#' + event.args.widget);
delete event.args.widget;
for (var key of Object.keys(event.args)) {
$widget.find('[data-bind=' + key + ']').text(event.args[key]);
}
} else if (event.args.type == 'platypush.message.event.web.DashboardIframeUpdateEvent') {
var url = event.args.url;
var $modal = $('#iframe-modal');
var $iframe = $modal.find('iframe');
$iframe.attr('src', url);
$iframe.prop('width', event.args.width || '100%');
$iframe.prop('height', event.args.height || '600');
if ('timeout' in event.args) {
setTimeout(function() {
$iframe.removeAttr('src');
$modal.fadeOut();
}, parseFloat(event.args.timeout) * 1000);
}
$modal.fadeIn();
}
};
var initDashboard = function() {
if ('background_image' in window.config) {
$('body').css('background-image', 'url(' + window.config.background_image + ')');
}
};
var initEvents = function() {
window.registerEventListener(onEvent);
};
var init = function() {
initDashboard();
initEvents();
};
init();
}); });
// Declaration of the main vue app
window.vm = new Vue({
el: '#app',
props: {
config: {
type: Object,
default: () => window.config,
},
},
data: function() {
return {
iframeModal: {
visible: false,
},
};
},
created: function() {
initEvents();
},
});
// $(document).ready(function() {
// var onEvent = function(event) {
// if (event.args.type == 'platypush.message.event.web.widget.WidgetUpdateEvent') {
// var $widget = $('#' + event.args.widget);
// delete event.args.widget;
// for (var key of Object.keys(event.args)) {
// $widget.find('[data-bind=' + key + ']').text(event.args[key]);
// }
// } else if (event.args.type == 'platypush.message.event.web.DashboardIframeUpdateEvent') {
// var url = event.args.url;
// var $modal = $('#iframe-modal');
// var $iframe = $modal.find('iframe');
// $iframe.attr('src', url);
// $iframe.prop('width', event.args.width || '100%');
// $iframe.prop('height', event.args.height || '600');
// if ('timeout' in event.args) {
// setTimeout(function() {
// $iframe.removeAttr('src');
// $modal.fadeOut();
// }, parseFloat(event.args.timeout) * 1000);
// }
// $modal.fadeIn();
// }
// };
// var initDashboard = function() {
// if (window.config.dashboard.background_image) {
// $('body').css('background-image', 'url(' + window.config.dashboard.background_image + ')');
// }
// };
// var initEvents = function() {
// registerEventListener(onEvent);
// };
// var init = function() {
// initDashboard();
// initEvents();
// };
// init();
// });

View file

@ -0,0 +1,46 @@
$(document).ready(function() {
var onEvent = function(event) {
if (event.args.type == 'platypush.message.event.web.widget.WidgetUpdateEvent') {
var $widget = $('#' + event.args.widget);
delete event.args.widget;
for (var key of Object.keys(event.args)) {
$widget.find('[data-bind=' + key + ']').text(event.args[key]);
}
} else if (event.args.type == 'platypush.message.event.web.DashboardIframeUpdateEvent') {
var url = event.args.url;
var $modal = $('#iframe-modal');
var $iframe = $modal.find('iframe');
$iframe.attr('src', url);
$iframe.prop('width', event.args.width || '100%');
$iframe.prop('height', event.args.height || '600');
if ('timeout' in event.args) {
setTimeout(function() {
$iframe.removeAttr('src');
$modal.fadeOut();
}, parseFloat(event.args.timeout) * 1000);
}
$modal.fadeIn();
}
};
var initDashboard = function() {
if ('background_image' in window.config) {
$('body').css('background-image', 'url(' + window.config.background_image + ')');
}
};
var initEvents = function() {
window.registerEventListener(onEvent);
};
var init = function() {
initDashboard();
initEvents();
};
init();
});

View file

@ -1,201 +0,0 @@
function initMapFromGeopoints(points) {
var $mapContainer = $('#map-container'),
$map = $('#map');
var markerArray = [];
var directionsService = new google.maps.DirectionsService;
var map = new google.maps.Map($map.get(0), {
mapTypeId: google.maps.MapTypeId.ROADMAP,
});
var bounds = new google.maps.LatLngBounds();
var infowindow = new google.maps.InfoWindow();
var maxPoints = 22;
var minDist = 100;
var start = points.length > maxPoints ? points.length-maxPoints-1 : 0;
var lastPoint;
for (i = points.length-1; i >= 0 && markerArray.length <= maxPoints; i--) {
if (lastPoint && latLngDistance(
[points[i]['latitude'], points[i]['longitude']],
[lastPoint['latitude'], lastPoint['longitude']]) < minDist) {
continue;
}
lastPoint = points[i];
var marker = new google.maps.Marker({
position: new google.maps.LatLng(points[i]['latitude'], points[i]['longitude']),
map: map
});
google.maps.event.addListener(marker, 'click', (function(marker, i) {
return function() {
infowindow.setContent(
(points[i]['address'] || '[No address]') + ' @ ' +
Date.parse(points[i]['created_at']).toLocaleString());
infowindow.open(map, marker);
}
})(marker, i));
// Extend the bounds to include each marker's position
bounds.extend(marker.position);
markerArray.push(marker);
}
var listener = google.maps.event.addListener(map, 'idle', function () {
// Now fit the map to the newly inclusive bounds
map.fitBounds(bounds);
setTimeout(function() {
if (window.zoom) {
map.setZoom(window.zoom);
} else {
map.setZoom(getBoundsZoomLevel(bounds, $map.children().width(), $map.children().height()));
}
}, 1000);
google.maps.event.removeListener(listener);
});
// Create a renderer for directions and bind it to the map.
var directionsDisplay = new google.maps.DirectionsRenderer({map: map});
// Instantiate an info window to hold step text.
var stepDisplay = new google.maps.InfoWindow;
// Display the route between the initial start and end selections.
calculateAndDisplayRoute(
directionsDisplay, directionsService, markerArray, stepDisplay, map);
}
function calculateAndDisplayRoute(directionsDisplay, directionsService,
markerArray, stepDisplay, map) {
// First, remove any existing markers from the map.
for (var i = 0; i < markerArray.length; i++) {
markerArray[i].setMap(null);
}
var waypoints = [];
for (i=1; i < markerArray.length-1; i++) {
if (!waypoints) waypoints = [];
waypoints.push({
location: markerArray[i].getPosition(),
stopover: true,
});
}
// Retrieve the start and end locations and create a DirectionsRequest using
// WALKING directions.
directionsService.route({
origin: markerArray[0].getPosition(),
destination: markerArray[markerArray.length-1].getPosition(),
waypoints: waypoints,
travelMode: 'WALKING'
}, function(response, status) {
// Route the directions and pass the response to a function to create
// markers for each step.
if (status === 'OK') {
directionsDisplay.setDirections(response);
showSteps(response, markerArray, stepDisplay, map);
} else {
// window.alert('Directions request failed due to ' + status);
}
});
}
function showSteps(directionResult, markerArray, stepDisplay, map) {
// For each step, place a marker, and add the text to the marker's infowindow.
// Also attach the marker to an array so we can keep track of it and remove it
// when calculating new routes.
var myRoute = directionResult.routes[0].legs[0];
for (var i = 0; i < myRoute.steps.length; i++) {
var marker = markerArray[i] = markerArray[i] || new google.maps.Marker;
marker.setMap(map);
marker.setPosition(myRoute.steps[i].start_location);
}
}
function getBoundsZoomLevel(bounds, width, height) {
var WORLD_DIM = { height: 256, width: 256 };
var ZOOM_MAX = 21;
function latRad(lat) {
var sin = Math.sin(lat * Math.PI / 180);
var radX2 = Math.log((1 + sin) / (1 - sin)) / 2;
return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2;
}
function zoom(mapPx, worldPx, fraction) {
return Math.floor(Math.log(mapPx / worldPx / fraction) / Math.LN2);
}
var ne = bounds.getNorthEast();
var sw = bounds.getSouthWest();
var latFraction = (latRad(ne.lat()) - latRad(sw.lat())) / Math.PI;
var lngDiff = ne.lng() - sw.lng();
var lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360;
var latZoom = zoom(height, WORLD_DIM.height, latFraction);
var lngZoom = zoom(width, WORLD_DIM.width, lngFraction);
return Math.min(latZoom, lngZoom, ZOOM_MAX);
}
function latLngDistance(a, b) {
if (typeof(Number.prototype.toRad) === "undefined") {
Number.prototype.toRad = function() {
return this * Math.PI / 180;
}
}
var R = 6371e3; // metres
var phi1 = a[0].toRad();
var phi2 = b[0].toRad();
var delta_phi = (b[0]-a[0]).toRad();
var delta_lambda = (b[1]-a[1]).toRad();
var a = Math.sin(delta_phi/2) * Math.sin(delta_phi/2) +
Math.cos(phi1) * Math.cos(phi2) *
Math.sin(delta_lambda/2) * Math.sin(delta_lambda/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
function updateGeoPoints() {
var from = new Date(window.map_start).toISOString();
var to = new Date(window.map_end).toISOString();
from = from.substring(0, 10) + ' ' + from.substring(11, 19)
to = to.substring(0, 10) + ' ' + to.substring(11, 19)
var engine = window.db_conf.engine;
var table = window.db_conf.table;
var columns = window.db_conf.columns;
execute(
{
type: 'request',
action: 'db.select',
args: {
engine: engine,
query: "SELECT " + columns['latitude'] + " AS latitude, " +
columns['longitude'] + " AS longitude, " +
columns['address'] + " AS address, " +
"DATE_FORMAT(" + columns['created_at'] + ", '%Y-%m-%dT%TZ') " +
"AS created_at FROM " + table + " WHERE created_at BETWEEN '" +
from + "' AND '" + to + "' ORDER BY " + columns['created_at'] + " DESC" }
},
onSuccess = function(response) {
initMapFromGeopoints(response.response.output);
}
);
}
function initMap() {
updateGeoPoints();
}

View file

@ -45,8 +45,6 @@ Vue.component('music-mpd', {
timestamp: null, timestamp: null,
elapsed: null, elapsed: null,
}, },
newTrackLock: false,
}; };
}, },
@ -467,9 +465,6 @@ Vue.component('music-mpd', {
} }
} }
}; };
// adjust(this)();
// setInterval(adjust(this), 2000);
}, },
_parseStatus: async function(status) { _parseStatus: async function(status) {

View file

@ -1,78 +0,0 @@
$(document).ready(function() {
var $widget = $('.widget.calendar'),
$nextEventContainer = $widget.find('.calendar-next-event-container'),
$eventsListContainer = $widget.find('.calendar-events-list-container');
var formatDateString = function(date) {
return date.toDateString().substring(0, 10);
};
var formatTimeString = function(date) {
return date.toTimeString().substring(0, 5);
};
var refreshStatus = function(status) {
setState(state=status.state);
if ('elapsed' in status) {
setTrackElapsed(status.elapsed);
}
};
var refreshCalendar = function() {
execute(
{
type: 'request',
action: 'calendar.get_upcoming_events',
args: {
max_results: 9,
}
},
onSuccess = function(response) {
var events = response.response.output;
$eventsListContainer.html('');
for (var i=0; i < events.length; i++) {
var event = events[i];
var start = new Date('dateTime' in event.start ? event.start.dateTime : event.start.date);
var end = new Date('dateTime' in event.end ? event.end.dateTime : event.end.date);
var summary = event.summary;
if (i == 0) {
$nextEventContainer.find('.summary').text(summary);
$nextEventContainer.find('.date').text(formatDateString(start));
$nextEventContainer.find('.time').text(
formatTimeString(start) + ' - ' + formatTimeString(end));
} else {
var $event = $('<div></div>').addClass('calendar-event').addClass('row');
var $eventDate = $('<div></div>').addClass('date')
.addClass('three columns').text(formatDateString(start));
var $eventTime = $('<div></div>').addClass('time').addClass('one column')
.text('dateTime' in event.start ? formatTimeString(start) : '-');
var $eventSummary = $('<div></div>').addClass('summary')
.addClass('eight columns').text(summary);
$eventDate.appendTo($event);
$eventTime.appendTo($event);
$eventSummary.appendTo($event);
$event.appendTo($eventsListContainer);
}
}
}
);
};
var initWidget = function() {
refreshCalendar();
setInterval(refreshCalendar, 900000);
};
var init = function() {
initWidget();
};
init();
});

View file

@ -0,0 +1,37 @@
Vue.component('calendar', {
template: '#tmpl-widget-calendar',
props: ['config'],
data: function() {
return {
events: [],
};
},
methods: {
refresh: async function() {
this.events = (await request('calendar.get_upcoming_events')).map(event => {
if (event.start)
event.start = new Date(event.start.dateTime || event.start.date);
if (event.end)
event.end = new Date(event.end.dateTime || event.end.date);
return event;
});
},
formatDate: function(date) {
return date.toDateString().substring(0, 10);
},
formatTime: function(date) {
return date.toTimeString().substring(0, 5);
},
},
mounted: function() {
this.refresh();
setInterval(this.refresh, 600000);
},
});

View file

@ -1,108 +0,0 @@
$(document).ready(function() {
var $widget = $('.widget.date-time-weather'),
$dateElement = $widget.find('[data-bind=date]'),
$timeElement = $widget.find('[data-bind=time]'),
$sensorTempElement = $widget.find('[data-bind=sensor-temperature]'),
$sensorHumidityElement = $widget.find('[data-bind=sensor-humidity]'),
$forecastElement = $widget.find('[data-bind=forecast]'),
$tempElement = $widget.find('[data-bind=temperature]'),
currentIcon = undefined;
var onEvent = function(event) {
if (event.args.type == 'platypush.message.event.weather.NewWeatherConditionEvent') {
updateTemperature(event.args.temperature);
updateWeatherIcon(event.args.icon);
} else if (event.args.type == 'platypush.message.event.sensor.SensorDataChangeEvent') {
if ('temperature' in event.args.data) {
updateSensorTemperature(event.args.data.temperature);
}
if ('humidity' in event.args.data) {
updateSensorHumidity(event.args.data.humidity);
}
}
};
var updateTemperature = function(temperature) {
$tempElement.text(Math.round(temperature));
};
var updateWeatherIcon = function(icon) {
var skycons = new Skycons({
'color':'#333', 'resizeClear':'true'
});
if (currentIcon) {
skycons.remove('weather-icon');
}
skycons.add('weather-icon', icon);
currentIcon = icon;
};
var updateSensorTemperature = function(temperature) {
$sensorTempElement.text(Math.round(temperature*10)/10);
$sensorTempElement.parent().show();
};
var updateSensorHumidity = function(humidity) {
$sensorHumidityElement.text(Math.round(humidity));
$sensorHumidityElement.parent().show();
};
var initEvents = function() {
window.registerEventListener(onEvent);
};
var refreshDateTime = function() {
var now = new Date();
$dateElement.text(now.toDateString());
$timeElement.text(now.getHours() + ':' +
(now.getMinutes() < 10 ? '0' : '') + now.getMinutes() + ':' +
(now.getSeconds() < 10 ? '0' : '') + now.getSeconds());
};
var initWeather = function() {
execute(
{
type: 'request',
action: 'weather.forecast.get_current_weather',
},
onSuccess = function(response) {
updateTemperature(status=response.response.output.temperature);
updateWeatherIcon(response.response.output.icon);
}
);
};
var refreshForecast = function() {
execute(
{
type: 'request',
action: 'weather.forecast.get_hourly_forecast',
},
onSuccess = function(response) {
$forecastElement.text(response.response.output.summary);
}
);
};
var initWidget = function() {
refreshDateTime();
setInterval(refreshDateTime, 500);
refreshForecast();
setInterval(refreshForecast, 1200000);
initWeather();
};
var init = function() {
initEvents();
initWidget();
};
init();
});

View file

@ -0,0 +1,68 @@
Vue.component('date-time-weather', {
template: '#tmpl-widget-date-time-weather',
props: ['config'],
data: function() {
return {
weather: undefined,
sensors: {},
now: new Date(),
weatherIcon: undefined,
};
},
methods: {
refresh: async function() {
let weather = (await request('weather.forecast.get_hourly_forecast')).data[0];
this.onWeatherChange(weather);
},
refreshTime: function() {
this.now = new Date();
},
formatDate: function(date) {
return date.toDateString().substring(0, 10);
},
formatTime: function(date) {
return date.toTimeString().substring(0, 8);
},
onWeatherChange: function(event) {
if (!this.weather)
this.weather = {};
Vue.set(this, 'weather', {...this.weather, ...event});
var skycons = new Skycons({
'color':'#333', 'resizeClear':'true'
});
if (this.weatherIcon) {
skycons.remove('weather-icon');
}
skycons.add('weather-icon', this.weather.icon);
this.weatherIcon = this.weather.icon;
},
onSensorData: function(event) {
if ('temperature' in event.data)
this.sensors.temperature = event.data.temperature;
if ('humidity' in event.data)
this.sensors.temperature = event.data.humidity;
},
},
mounted: function() {
this.refresh();
setInterval(this.refresh, 900000);
setInterval(this.refreshTime, 1000);
registerEventHandler(this.onWeatherChange, 'platypush.message.event.weather.NewWeatherConditionEvent');
registerEventHandler(this.onSensorData, 'platypush.message.event.sensor.SensorDataChangeEvent');
},
});

View file

@ -1,57 +0,0 @@
$(document).ready(function() {
var $imgContainer = $('.image-carousel').find('.carousel'),
$imgBackground = $('.image-carousel').find('.carousel-background'),
config = window.widgets['image-carousel'],
images = config.imageUrls,
processedImages = 0;
var shuffleImages = function() {
for (var i=images.length-1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var x = images[i];
images[i] = images[j];
images[j] = x;
}
};
var refreshImage = function() {
var nextImage = images[processedImages++];
var $oldImg = $imgContainer.find('img');
var $newImg = $('<img></img>')
.attr('src', nextImage)
.attr('alt', 'Could not load image')
.appendTo('body').hide();
$newImg.on('load', function() {
$oldImg.remove();
if ($newImg.width() > $newImg.height()) {
$newImg.css('width', '100%');
$imgBackground.css('background-image', '');
} else {
$imgBackground.css('background-image', 'url(' + nextImage + ')');
}
$newImg.css('max-height', '100%');
$newImg.remove().appendTo($imgContainer).show();
});
if (processedImages == images.length-1) {
shuffleImages();
processedImages = 0;
}
};
var initWidget = function() {
shuffleImages();
refreshImage();
setInterval(refreshImage,
'refresh_seconds' in config ? config.refresh_seconds * 1000 : 15000);
};
var init = function() {
initWidget();
};
init();
});

View file

@ -0,0 +1,60 @@
Vue.component('image-carousel', {
template: '#tmpl-widget-image-carousel',
props: ['config'],
data: function() {
return {
images: [],
currentImage: undefined,
};
},
methods: {
refresh: async function() {
if (!this.images.length) {
this.images = await request('utils.search_web_directory', {
directory: this.config.images_path,
extensions: ['.jpg', '.jpeg', '.png'],
});
this.shuffleImages();
}
this.currentImage = this.images.pop();
},
onNewImage: function() {
this.$refs.background.style['background-image'] = 'url(' + this.currentImage + ')';
if (this.$refs.img.width > this.$refs.img.height) {
this.$refs.img.style.width = 'auto';
if ((this.$refs.img.width / this.$refs.img.height) >= 4/3) {
this.$refs.img.style.width = '100%';
}
if ((this.$refs.img.width / this.$refs.img.height) <= 16/9) {
this.$refs.img.style.height = '100%';
}
}
},
shuffleImages: function() {
for (var i=this.images.length-1; i > 0; i--) {
let j = Math.floor(Math.random() * (i+1));
let x = this.images[i];
Vue.set(this.images, i, this.images[j]);
Vue.set(this.images, j, x);
}
},
},
mounted: function() {
this.$refs.img.addEventListener('load', this.onNewImage);
this.$refs.img.addEventListener('error', this.refresh);
this.refresh();
setInterval(this.refresh, 'refresh_seconds' in this.config ? this.config.refresh_seconds*1000 : 15000);
},
});

View file

@ -1,192 +0,0 @@
$(document).ready(function() {
var $widget = $('.widget.music'),
$trackContainer = $widget.find('.track-container'),
$timeContainer = $widget.find('.time-container'),
$playbackStatusContainer = $widget.find('.playback-status-container'),
$noTrackElement = $trackContainer.find('.no-track-info'),
$trackElement = $trackContainer.find('.track-info'),
$artistElement = $trackElement.find('[data-bind=artist]'),
$titleElement = $trackElement.find('[data-bind=title]'),
$timeElapsedElement = $timeContainer.find('.time-elapsed'),
$timeTotalElement = $timeContainer.find('.time-total'),
$elapsedTimeBar = $widget.find('.time-bar > .elapsed'),
$volumeElement = $playbackStatusContainer.find('[data-bind=playback-volume]'),
$randomElement = $playbackStatusContainer.find('[data-bind=playback-random]'),
$repeatElement = $playbackStatusContainer.find('[data-bind=playback-repeat]'),
$singleElement = $playbackStatusContainer.find('[data-bind=playback-single]'),
$consumeElement = $playbackStatusContainer.find('[data-bind=playback-consume]'),
timeElapsed,
timeTotal,
refreshElapsedInterval;
var onEvent = function(event) {
switch (event.args.type) {
case 'platypush.message.event.music.NewPlayingTrackEvent':
createNotification({
'icon': 'play',
'html': '<b>' + ('artist' in event.args.track ? event.args.track.artist : '')
+ '</b><br/>'
+ ('title' in event.args.track ? event.args.track.title : '[No name]'),
});
case 'platypush.message.event.music.MusicPlayEvent':
case 'platypush.message.event.music.MusicPauseEvent':
refreshTrack(event.args.track);
case 'platypush.message.event.music.MusicStopEvent':
refreshStatus(event.args.status);
break;
case 'platypush.message.event.music.VolumeChangeEvent':
case 'platypush.message.event.music.PlaybackRepeatModeChangeEvent':
case 'platypush.message.event.music.PlaybackRandomModeChangeEvent':
case 'platypush.message.event.music.PlaybackConsumeModeChangeEvent':
case 'platypush.message.event.music.PlaybackSingleModeChangeEvent':
refreshPlaybackStatus(event.args.status);
break;
}
};
var initEvents = function() {
window.registerEventListener(onEvent);
};
var setState = function(state) {
if (state === 'play') {
$noTrackElement.hide();
$trackElement.show();
$timeContainer.show();
} else if (state === 'pause') {
$noTrackElement.hide();
$trackElement.show();
$timeContainer.hide();
} else if (state === 'stop') {
$noTrackElement.show();
$trackElement.hide();
$timeContainer.hide();
}
};
var secondsToTimeString = function(seconds) {
seconds = parseInt(seconds);
if (seconds) {
return (parseInt(seconds/60) + ':' +
(seconds%60 < 10 ? '0' : '') + seconds%60);
} else {
return '-:--';
}
};
var setTrackTime = function(time) {
$timeTotalElement.text(secondsToTimeString(time));
timeTotal = parseInt(time);
};
var setTrackElapsed = function(time) {
if (refreshElapsedInterval) {
clearInterval(refreshElapsedInterval);
refreshElapsedInterval = undefined;
}
if (time === undefined) {
$timeElapsedElement.text('-:--');
return;
}
timeElapsed = parseInt(time);
$timeElapsedElement.text(secondsToTimeString(timeElapsed));
var ratio = 100 * Math.min(timeElapsed/timeTotal, 1);
$elapsedTimeBar.css('width', ratio + '%');
refreshElapsedInterval = setInterval(function() {
timeElapsed += 1;
ratio = 100 * Math.min(timeElapsed/timeTotal, 1);
$elapsedTimeBar.css('width', ratio + '%');
$timeElapsedElement.text(secondsToTimeString(timeElapsed));
}, 1000);
};
var refreshStatus = function(status) {
if (!status) {
return;
}
if ('state' in status) {
setState(state=status.state);
if (status.state === 'stop') {
setTrackElapsed();
}
}
if ('elapsed' in status) {
setTrackElapsed(status.elapsed);
} else if ('position' in status) {
setTrackElapsed(status.position);
}
};
var refreshTrack = function(track) {
if (!track) {
return;
}
if ('time' in track) {
setTrackTime(track.time);
}
$artistElement.text(track.artist);
$titleElement.text(track.title);
};
var refreshPlaybackStatus = function(status) {
if (!status) {
return;
}
if ('volume' in status) {
$volumeElement.text(status.volume + '%');
}
if ('random' in status) {
var state = !!parseInt(status.random);
$randomElement.text(state ? 'ON' : 'OFF');
}
if ('repeat' in status) {
var state = !!parseInt(status.repeat);
$repeatElement.text(state ? 'ON' : 'OFF');
}
if ('single' in status) {
var state = !!parseInt(status.single);
$singleElement.text(state ? 'ON' : 'OFF');
}
if ('consume' in status) {
var state = !!parseInt(status.consume);
$consumeElement.text(state ? 'ON' : 'OFF');
}
};
var initWidget = function() {
$.when(
execute({ type: 'request', action: 'music.mpd.currentsong' }),
execute({ type: 'request', action: 'music.mpd.status' })
).done(function(t, s) {
refreshTrack(t[0].response.output);
refreshStatus(s[0].response.output);
refreshPlaybackStatus(s[0].response.output);
});
};
var init = function() {
initEvents();
initWidget();
};
init();
});

View file

@ -0,0 +1,233 @@
Vue.component('music', {
template: '#tmpl-widget-music',
props: ['config'],
data: function() {
return {
track: undefined,
status: undefined,
timer: undefined,
syncTime: {
timestamp: null,
elapsed: null,
},
};
},
methods: {
refresh: async function() {
let status = await request('music.mpd.status');
let track = await request('music.mpd.currentsong');
this._parseStatus(status);
this._parseTrack(track);
if (status.state === 'play' && !this.timer)
this.startTimer();
else if (status.state !== 'play' && this.timer)
this.stopTimer();
},
convertTime: function(time) {
time = parseFloat(time); // Normalize strings
var t = {};
t.h = '' + parseInt(time/3600);
t.m = '' + parseInt(time/60 - t.h*60);
t.s = '' + parseInt(time - (t.h*3600 + t.m*60));
for (var attr of ['m','s']) {
if (parseInt(t[attr]) < 10) {
t[attr] = '0' + t[attr];
}
}
var ret = [];
if (parseInt(t.h)) {
ret.push(t.h);
}
ret.push(t.m, t.s);
return ret.join(':');
},
_parseStatus: async function(status) {
if (!status || status.length === 0)
status = await request('music.mpd.status');
if (!this.status)
this.status = {};
for (const [attr, value] of Object.entries(status)) {
if (['consume','random','repeat','single','bitrate'].indexOf(attr) >= 0) {
Vue.set(this.status, attr, !!parseInt(value));
} else if (['nextsong','nextsongid','playlist','playlistlength',
'volume','xfade','song','songid'].indexOf(attr) >= 0) {
Vue.set(this.status, attr, parseInt(value));
} else if (['elapsed'].indexOf(attr) >= 0) {
Vue.set(this.status, attr, parseFloat(value));
} else {
Vue.set(this.status, attr, value);
}
}
},
_parseTrack: async function(track) {
if (!track || track.length === 0) {
track = await request('music.mpd.currentsong');
}
if (!this.track)
this.track = {};
for (const [attr, value] of Object.entries(track)) {
if (['id','pos','time','track','disc'].indexOf(attr) >= 0) {
Vue.set(this.track, attr, parseInt(value));
} else {
Vue.set(this.track, attr, value);
}
}
},
showNewTrackNotification: function() {
createNotification({
html: '<b>' + (this.track.artist || '[No Artist]') + '</b><br>' +
(this.track.title || '[No Title]'),
image: {
icon: 'play',
}
});
},
onNewPlayingTrack: async function(event) {
let previousTrack = undefined;
if (this.track) {
previousTrack = {
file: this.track.file,
artist: this.track.artist,
title: this.track.title,
};
}
this.status.state = 'play';
Vue.set(this.status, 'elapsed', 0);
this.track = {};
this._parseTrack(event.track);
let status = event.status ? event.status : await request('music.mpd.status');
this._parseStatus(status);
this.startTimer();
if (previousTrack && this.track.file != previousTrack.file
|| this.track.artist != previousTrack.artist
|| this.track.title != previousTrack.title) {
this.showNewTrackNotification();
}
},
onMusicStop: function(event) {
this.status.state = 'stop';
Vue.set(this.status, 'elapsed', 0);
this._parseStatus(event.status);
this._parseTrack(event.track);
this.stopTimer();
},
onMusicPlay: function(event) {
this.status.state = 'play';
this._parseStatus(event.status);
this._parseTrack(event.track);
this.startTimer();
},
onMusicPause: function(event) {
this.status.state = 'pause';
this._parseStatus(event.status);
this._parseTrack(event.track);
Vue.set(this.syncTime, 'timestamp', new Date());
Vue.set(this.syncTime, 'elapsed', this.status.elapsed);
},
onSeekChange: function(event) {
if (event.position != null)
Vue.set(this.status, 'elapsed', parseFloat(event.position));
if (event.status)
this._parseStatus(event.status);
if (event.track)
this._parseTrack(event.track);
Vue.set(this.syncTime, 'timestamp', new Date());
Vue.set(this.syncTime, 'elapsed', this.status.elapsed);
},
onVolumeChange: function(event) {
if (event.volume != null)
this.status.volume = parseFloat(event.volume);
if (event.status)
this._parseStatus(event.status);
if (event.track)
this._parseTrack(event.track);
},
onRepeatChange: function(event) {
this.status.repeat = event.state;
},
onRandomChange: function(event) {
this.status.random = event.state;
},
onConsumeChange: function(event) {
this.status.consume = event.state;
},
onSingleChange: function(event) {
this.status.single = event.state;
},
startTimer: function() {
if (this.timer != null) {
this.stopTimer();
}
Vue.set(this.syncTime, 'timestamp', new Date());
Vue.set(this.syncTime, 'elapsed', this.status.elapsed);
this.timer = setInterval(this.timerFunc, 1000);
},
stopTimer: function() {
if (this.timer == null) {
clearInterval(this.timer);
this.timer = null;
}
},
timerFunc: function() {
if (this.status.state !== 'play' || this.status.elapsed == null) {
return;
}
Vue.set(this.status, 'elapsed', this.syncTime.elapsed +
((new Date()).getTime()/1000) - (this.syncTime.timestamp.getTime()/1000));
},
},
mounted: function() {
this.refresh();
setInterval(this.refresh, 60000);
registerEventHandler(this.onNewPlayingTrack, 'platypush.message.event.music.NewPlayingTrackEvent');
registerEventHandler(this.onMusicStop, 'platypush.message.event.music.MusicStopEvent');
registerEventHandler(this.onMusicPlay, 'platypush.message.event.music.MusicPlayEvent');
registerEventHandler(this.onMusicPause, 'platypush.message.event.music.MusicPauseEvent');
registerEventHandler(this.onSeekChange, 'platypush.message.event.music.SeekChangeEvent');
registerEventHandler(this.onVolumeChange, 'platypush.message.event.music.VolumeChangeEvent');
registerEventHandler(this.onRepeatChange, 'platypush.message.event.music.PlaybackRepeatModeChangeEvent');
registerEventHandler(this.onRandomChange, 'platypush.message.event.music.PlaybackRandomModeChangeEvent');
registerEventHandler(this.onConsumeChange, 'platypush.message.event.music.PlaybackConsumeModeChangeEvent');
registerEventHandler(this.onSingleChange, 'platypush.message.event.music.PlaybackSingleModeChangeEvent');
},
});

View file

@ -1,80 +0,0 @@
$(document).ready(function() {
var $newsElement = $('.rss-news').find('.news-container'),
config = window.widgets['rss-news'],
db = config.db,
news = [],
default_limit = 10,
cur_article_index = -1;
var getNews = function() {
execute(
{
type: 'request',
action: 'db.select',
args: {
engine: db,
query: "select s.title as source, e.title, e.summary, " +
"strftime('%Y-%m-%dT%H:%M:%fZ', e.published) as published " +
"from FeedEntry e join FeedSource s " +
"on e.source_id = s.id order by e.published desc limit " +
('limit' in config ? config.limit : default_limit)
}
},
onSuccess = function(response) {
if (!response.response.output) {
return;
}
var firstRun = news.length === 0;
news = response.response.output;
cur_article_index = -1;
if (firstRun) {
refreshNews();
}
}
);
};
var refreshNews = function() {
if (news.length === 0) {
return;
}
var updateNewsList = cur_article_index == news.length-1;
var nextArticle = news[++cur_article_index % news.length];
var dt = new Date(nextArticle.published);
var $article = $('<div></div>').addClass('article');
var $source = $('<div></div>').addClass('source').text(nextArticle.source);
var $title = $('<div></div>').addClass('title').text(nextArticle.title);
var $publishTime = $('<div></div>').addClass('publish-time')
.text(dt.toDateString() + ', ' + dt.toTimeString().substring(0, 5));
$source.appendTo($article);
$title.appendTo($article);
$publishTime.appendTo($article);
if ($newsElement.find('.article').length) {
$newsElement.find('.article').remove();
}
$article.hide().appendTo($newsElement).show();
if (updateNewsList) {
getNews();
}
};
var initWidget = function() {
getNews();
setInterval(refreshNews, 15000);
};
var init = function() {
initWidget();
};
init();
});

View file

@ -0,0 +1,40 @@
Vue.component('rss-news', {
template: '#tmpl-widget-rss-news',
props: ['config'],
data: function() {
return {
articles: [],
queue: [],
currentArticle: undefined,
};
},
methods: {
refresh: async function() {
if (!this.queue.length) {
this.articles = await request('db.select', {
engine: this.config.db,
query: "select s.title as source, e.title, e.summary, " +
"strftime('%Y-%m-%dT%H:%M:%fZ', e.published) as published " +
"from FeedEntry e join FeedSource s " +
"on e.source_id = s.id order by e.published limit " +
('limit' in this.config ? this.config.limit : 10)
});
this.queue = [...this.articles];
}
if (!this.queue.length)
return;
this.currentArticle = this.queue.pop();
},
},
mounted: function() {
this.refresh();
setInterval(this.refresh, 'refresh_seconds' in this.config ? this.config.refresh_seconds*1000 : 15000);
},
});

View file

@ -1,61 +1,90 @@
<!doctype html> <!doctype html>
<head> <head>
<title>Platypush Dashboard</title> <title>Platypush Dashboard</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Lato" /> <link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Lato">
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton.css') }}"></script> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/skeleton.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton-tabs.css') }}"></script> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/normalize.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}"></script> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='font-awesome/css/all.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/font-awesome.min.css') }}"></script> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/dist/dashboard.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/application.css') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/lib/vue.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/toggles.css') }}"></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/api.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/events.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/notifications.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/plugins/pushbullet/index.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/plugins/assistant.google/index.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/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/dashboard.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/pushbullet.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/assistant.google.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">
window.websocket_port = {% print(websocket_port) %}; if (!window.config) {
window.has_ssl = {% print('true' if has_ssl else 'false') %}; window.config = {};
}
window.config = { ...window.config,
websocket_port: {{ websocket_port }},
has_ssl: {{ 'true' if has_ssl else 'false' }},
templates: JSON.parse('{{ utils.to_json(templates)|safe }}'),
scripts: JSON.parse('{{ utils.to_json(scripts)|safe }}'),
};
{% if token %} {% if token %}
window.token = '{% print(token) %}'; window.config.token = '{{ token }}';
{% else %} {% else %}
window.token = undefined; window.config.token = undefined;
{% endif %} {% endif %}
window.config = {{ config | safe }}; window.config.dashboard = {{ config | safe }};
window.widgets = {{ config['widgets'] | safe }}.reduce(function(map, w) { map[w.widget] = w; return map }, {}) window.config.widgets = {{ config['widgets'] | safe }}.reduce(function(map, w) { map[w.widget] = w; return map }, {})
</script> </script>
{% include 'elements.html' %}
{% for widget in config['widgets'] %}
{% with name = widget['widget'] %}
{% include 'widgets/' + name + '/index.html' %}
{% with js_file = static_folder + '/js/widgets/' + name + '/index.js' %}
{% if utils.isfile(js_file) %}
<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/' + name + '/index.js') }}"></script>
{% endif %}
{% endwith %}
{% with css_file = static_folder + '/css/dist/dashboard/widgets/' + name + '.css' %}
{% if utils.isfile(css_file) %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/dist/dashboard/widgets/' + name + '.css') }}">
{% endif %}
{% endwith %}
{% endwith %}
{% endfor %}
</head> </head>
<body> <body>
<div id="app">
<main> <main>
<!-- You can send events of type platypush.message.event.web.DashboardIframeUpdateEvent <!-- You can send events of type platypush.message.event.web.DashboardIframeUpdateEvent
to control what is shown in the optional iframe modal --> to control what is shown in the optional iframe modal -->
<div id="iframe-modal" class="modal"> <modal id="iframe-modal" v-model="iframeModal.visible">
<div class="modal-container"> <iframe ref="iframeModal"></iframe>
<iframe></iframe> </modal>
</div>
</div>
<div id="widgets-container"> <div id="widgets-container">
{% set used_columns = [0] %} {% set used_columns = [0] %}
{% for widget in config['widgets'] %} {% for widget in config['widgets'] %}
{% with name = widget['widget'] %}
{% if used_columns[0] % 12 == 0 %} {% if used_columns[0] % 12 == 0 %}
<div class="row"> <div class="widgets-row">
{% endif %} {% endif %}
<div class="widget {% print(utils.widget_columns_to_html_class(widget['columns'])) %}
{% print(widget['widget']) %}"
id="{% print(widget['id'] if 'id' in widget else widget['widget']) %}">
{% with properties=widget %} {% with properties=widget %}
{% include 'widgets/' + widget['widget'] + '.html' %} <widget :tag="'{{ name }}'.replace('.', '-')"
key="{{ name }}"
:config="{{ widget | safe }}">
</widget>
{% endwith %} {% endwith %}
</div>
{# increment counter #} {# increment counter #}
{% if used_columns.append(used_columns.pop() + widget['columns']) %}{% endif %} {% if used_columns.append(used_columns.pop() + widget['columns']) %}{% endif %}
@ -63,10 +92,16 @@
{% if used_columns[0] % 12 == 0 %} {% if used_columns[0] % 12 == 0 %}
</div> </div>
{% endif %} {% endif %}
{% endwith %}
{% endfor %} {% endfor %}
</div> </div>
<div id="notification-container"></div> {% include 'notifications.html' %}
</main> </main>
<body> </div>
{% include 'widgets/template.html' %}
<script type="text/javascript" src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
</body>

View file

@ -0,0 +1,72 @@
<!doctype html>
<head>
<title>Platypush Dashboard</title>
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Lato" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton.css') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton-tabs.css') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/font-awesome.min.css') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/application.css') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/toggles.css') }}"></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/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/dashboard.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/pushbullet.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/assistant.google.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 %}
window.config = {{ config | safe }};
window.widgets = {{ config['widgets'] | safe }}.reduce(function(map, w) { map[w.widget] = w; return map }, {})
</script>
</head>
<body>
<main>
<!-- You can send events of type platypush.message.event.web.DashboardIframeUpdateEvent
to control what is shown in the optional iframe modal -->
<div id="iframe-modal" class="modal">
<div class="modal-container">
<iframe></iframe>
</div>
</div>
<div id="widgets-container">
{% set used_columns = [0] %}
{% for widget in config['widgets'] %}
{% if used_columns[0] % 12 == 0 %}
<div class="row">
{% endif %}
<div class="widget {% print(utils.widget_columns_to_html_class(widget['columns'])) %}
{% print(widget['widget']) %}"
id="{% print(widget['id'] if 'id' in widget else widget['widget']) %}">
{% with properties=widget %}
{% include 'widgets/' + widget['widget'] + '.html' %}
{% endwith %}
</div>
{# increment counter #}
{% if used_columns.append(used_columns.pop() + widget['columns']) %}{% endif %}
{% if used_columns[0] % 12 == 0 %}
</div>
{% endif %}
{% endfor %}
</div>
<div id="notification-container"></div>
</main>
<body>

View file

@ -68,7 +68,8 @@
:tag="plugin.replace('.', '-')" :tag="plugin.replace('.', '-')"
:key="plugin" :key="plugin"
:config="conf" :config="conf"
:class="{hidden: plugin != selectedPlugin}"/> :class="{hidden: plugin != selectedPlugin}">
</plugin>
</div> </div>
</main> </main>

View file

@ -1,3 +0,0 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/assistant.google.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/assistant.google.css') }}"></script>

View file

@ -1,5 +0,0 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/gpio.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/gpio.css') }}"></script>
<div class="row" id="gpio-container"></div>

View file

@ -1,5 +0,0 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/gpio.sensor.mcp3008.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/gpio.sensor.mcp3008.css') }}"></script>
<div class="row" id="sensors-container"></div>

View file

@ -1,44 +0,0 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/gpio.zeroborg.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/gpio.zeroborg.css') }}"></script>
<div id="zb-container" class="row" tabindex="1">
<div class="zb-controls-container">
<div class="row">
<div class="four columns">&nbsp;</div>
<button data-direction="up" class="zb-ctrl-btn four columns">
<i class="fa fa-sort-up"></i>
</button>
<div class="four columns">&nbsp;</div>
</div>
<div class="row">
<button data-direction="left" class="zb-ctrl-btn four columns">
<i class="fa fa-caret-left"></i>
</button>
<div class="four columns">&nbsp;</div>
<button data-direction="right" class="zb-ctrl-btn four columns">
<i class="fa fa-caret-right"></i>
</button>
</div>
<div class="row">
<div class="four columns">&nbsp;</div>
<button data-direction="down" class="zb-ctrl-btn four columns">
<i class="fa fa-sort-down"></i>
</button>
<div class="four columns">&nbsp;</div>
</div>
<div class="row ctrl-bottom-row">
<div class="two columns">&nbsp;</div>
<button data-action="auto" class="zb-ctrl-btn four columns">
AUTO PILOT
</button>
<button data-action="stop" class="zb-ctrl-btn four columns">
<i class="fa fa-stop"></i>
</button>
<div class="two columns">&nbsp;</div>
</div>
</div>
</div>

View file

@ -1,154 +0,0 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/media.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/media.css') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='flag-icons/css/flag-icon.min.css') }}"></script>
<script type="text/javascript">
window.config = window.config || {};
window.config.media = window.config.media || {};
window.config.media.subtitles = JSON.parse('{{ utils.to_json(utils.get_config("media.subtitles")) | safe }}');
</script>
<div class="row" id="video-container">
<form action="#" id="video-search">
<div class="row">
<label for="video-search-text">
Supported formats: <tt>file://[path]</tt>, <tt>https://www.youtube.com/?v=[video_id]</tt>, or free text search
</label>
</div>
<div class="row">
<div class="nine columns" id="media-search-bar-container">
<input type="text" name="video-search-text" placeholder="Search query or video URL">
</div>
<div class="three columns" id="media-btns-container">
<button type="submit">
<i class="fa fa-search"></i>
</button>
<button data-panel="#media-devices-panel">
<i id="media-devices-panel-icon" class="fa fa-desktop"></i>
</button>
</div>
</div>
<div class="row panel" id="media-devices-panel">
<div class="refresh-devices-container">
<div class="row refresh-devices disabled">
<i class="fa fa-retweet"></i>
</div>
</div>
<div class="devices-list">
<div class="row cast-device cast-device-local selected" data-local="local" data-name="_server">
<div class="two columns">
<i class="fa fa-desktop cast-device-icon"></i>
</div>
<div class="ten columns">
<span class="cast-device-name">{{ utils.get_config('device_id') }}</span>
</div>
</div>
<div class="row cast-device cast-device-local" data-browser="browser" data-name="_local">
<div class="two columns">
<i class="fa fa-laptop cast-device-icon"></i>
</div>
<div class="ten columns">
<span class="cast-device-name">Browser</span>
</div>
</div>
</div>
</div>
</form>
<form action="#" id="video-ctrl">
<!-- <div class="row"> -->
<!-- <div class="eight columns offset-by-two slider-container" id="video-seeker-container"> -->
<!-- <span class="seek-time" id="video-elapsed">-:--</span>&nbsp; -->
<!-- <input type="range" min="0" id="video-seeker" disabled="disabled" class="slider" style="width:75%"> -->
<!-- &nbsp;<span class="seek-time" id="video-length">-:--</span> -->
<!-- </div> -->
<!-- </div> -->
<div class="row">
<div class="ten columns offset-by-one">
<button data-action="previous">
<i class="fa fa-step-backward"></i>
</button>
<button data-action="back">
<i class="fa fa-backward"></i>
</button>
<button data-action="pause">
<i class="fa fa-pause"></i>
</button>
<button data-action="stop">
<i class="fa fa-stop"></i>
</button>
<button data-action="forward">
<i class="fa fa-forward"></i>
</button>
<button data-action="next">
<i class="fa fa-step-forward"></i>
</button>
<button data-action="subtitles" data-modal="#media-subtitles-modal">
<i class="fa fa-comment"></i>
</button>
</div>
</div>
<div class="row">
<div class="eight columns offset-by-two slider-container" id="video-volume-ctrl-container">
<i class="fa fa-volume-down"></i> &nbsp;
<input type="range" min="0" max="100" value="100" id="video-volume-ctrl" class="slider" style="width:80%">
&nbsp; <i class="fa fa-volume-up"></i>
</div>
</div>
</form>
<div class="row panel" id="media-item-panel">
<div class="row media-item-action" data-action="play">
<div class="two columns media-item-icon">
<i class="fa fa-play"></i>
</div>
<div class="ten columns">Play</div>
</div>
<div class="row media-item-action" data-action="download">
<div class="two columns">
<i class="fa fa-download"></i>
</div>
<div class="ten columns">Download</div>
</div>
</div>
<div id="media-subtitles-modal" class="modal">
<div class="modal-container">
<div class="modal-header">
Subtitles results
</div>
<div class="modal-body">
<div class="row media-subtitles-results-container">
<div class="row media-subtitles-results-header">
<div class="one column">Lang</div>
<div class="five columns">Movie</div>
<div class="six columns">Subtitles</div>
</div>
<div class="row media-subtitles-results">
</div>
</div>
<div class="row media-subtitles-message">No media selected or playing</div>
</div>
</div>
</div>
<div class="row" id="video-results-container">
<div id="video-results"></div>
</div>
</div>

View file

@ -1 +0,0 @@
media.html

View file

@ -1 +0,0 @@
media.html

View file

@ -1 +0,0 @@
gpio.sensor.mcp3008.html

View file

@ -1,17 +0,0 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/switch.switchbot.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/switch.switchbot.css') }}"></script>
<div id="switchbot-container" class="row">
{% for addr, name in configuration['devices'].items() %}
<div class="row switchbot-device" data-addr="{{ addr }}">
<div class="ten columns name">{{ name }}</div>
<div class="two columns toggle-container">
<div class="toggle toggle--push switch-ctrl-container">
<label for="{{ addr }}" class="toggle--btn"></label>
<input type="checkbox" name="{{ addr }}" class="toggle--checkbox switch-ctrl">
</div>
</div>
</div>
{% endfor %}
</div>

View file

@ -1,5 +0,0 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/switch.tplink.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/switch.tplink.css') }}"></script>
<div id="tplink-container" class="row"></div>

View file

@ -1,5 +0,0 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/switch.wemo.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/switch.wemo.css') }}"></script>
<div id="wemo-container" class="row"></div>

View file

@ -1,13 +0,0 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/calendar.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/widgets/calendar.css') }}"></script>
<div class="calendar-container">
<div class="calendar-next-event-container">
<div class="date"></div>
<div class="summary"></div>
<div class="time"></div>
</div>
<div class="calendar-events-list-container"></div>
</div>

View file

@ -0,0 +1,15 @@
<script type="text/x-template" id="tmpl-widget-calendar">
<div class="calendar">
<div class="event upcoming-event" v-if="events.length > 0">
<div class="date" v-text="formatDate(events[0].start)"></div>
<div class="summary" v-text="events[0].summary"></div>
<div class="time">{% raw %}{{ formatTime(events[0].start) }} - {{ formatTime(events[0].end) }}{% endraw %}</div>
</div>
<div class="event" v-for="event in events.slice(1)" v-if="events.length > 1" :key="event.id">
<div class="date col-2" v-text="formatDate(event.start)"></div>
<div class="time col-2" v-text="formatTime(event.start)"></div>
<div class="summary col-8" v-text="event.summary"></div>
</div>
</div>
</script>

View file

@ -1,28 +0,0 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/skycons.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/date-time-weather.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/widgets/date-time-weather.css') }}"></script>
<div class="date-time-weather-container">
<div class="date" data-bind="date"></div>
<div class="time" data-bind="time"></div>
<h1 class="temperature">
<canvas id="weather-icon" width="50" height="50"></canvas> &nbsp;
<span data-bind="temperature">N/A</span>&deg;
</h1>
<div class="forecast" data-bind="forecast"></div>
<div class="sensors row">
<div class="sensor-temperature six columns">
<i class="fa fa-thermometer"></i> &nbsp;
<span data-bind="sensor-temperature">N/A</span>&deg;
</div>
<div class="sensor-humidity six columns">
<i class="fa fa-tint"></i> &nbsp;
<span data-bind="sensor-humidity">N/A</span>%
</div>
</div>
</div>

View file

@ -0,0 +1,39 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/date-time-weather/skycons.js') }}"></script>
<script type="text/x-template" id="tmpl-widget-date-time-weather">
<div class="date-time-weather">
<div class="date" v-text="formatDate(now)"></div>
<div class="time" v-text="formatTime(now)"></div>
<h1 class="weather">
<canvas id="weather-icon" width="50" height="50"></canvas> &nbsp;
<span class="temperature" v-if="weather">
{% raw %}
{{ Math.round(parseFloat(weather.temperature)) + '&deg;' }}
{% endraw %}
</span>
</h1>
<div class="summary" v-if="weather && weather.summary" v-text="weather.summary"></div>
<div class="sensors" v-if="Object.keys(sensors).length">
<div class="sensor temperature col-6" v-if="sensors.temperature">
<i class="fas fa-thermometer-half"></i> &nbsp;
<span class="temperature">
{% raw %}
{{ parseFloat(sensors.temperature).toFixed(1) + '&deg;'}}
{% endraw %}
</span>
</div>
<div class="sensor humidity col-6" v-if="sensors.humidity">
<i class="fa fa-tint"></i> &nbsp;
<span class="humidity">
{% raw %}
{{ parseFloat(sensors.humidity).toFixed(1) + '%'}}
{% endraw %}
</span>
</div>
</div>
</div>
</script>

View file

@ -1,15 +0,0 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/image-carousel.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/widgets/image-carousel.css') }}"></script>
{% set images = utils.search_web_directory(
widget['images_path'], '.jpg', 'jpeg', '.png') %}
<script type="text/javascript">
window.widgets['image-carousel'].imageUrls = {{ images|safe }};
</script>
<div class="carousel">
<div class="carousel-background">&nbsp;</div>
<img alt="Your carousel images">
</div>

View file

@ -0,0 +1,6 @@
<script type="text/x-template" id="tmpl-widget-image-carousel">
<div class="image-carousel">
<div ref="background" class="background"></div>
<img ref="img" :src="currentImage" alt="Your carousel images">
</div>
</script>

View file

@ -1,63 +0,0 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/music.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/widgets/music.css') }}"></script>
<div class="music-container">
<div class="track-container">
<div class="no-track-info">No media is being played</div>
<div class="track-info">
<div class="artist" data-bind="artist"></div>
<div class="title" data-bind="title"></div>
</div>
</div>
<div class="time-container">
<div class="row">
<div class="time-bar">
<div class="elapsed"></div>
</div>
</div>
<div class="row">
<div class="six columns">
<div class="time-elapsed" data-bind="time-elapsed"></div>
</div>
<div class="six columns">
<div class="time-total" data-bind="time-total"></div>
</div>
</div>
</div>
<div class="playback-status-container">
<div class="row playback-status-titles">
<div class="two columns offset-by-one">Volume</div>
<div class="two columns">Random</div>
<div class="two columns">Repeat</div>
<div class="two columns">Single</div>
<div class="two columns">Consume</div>
</div>
<div class="row playback-status-values">
<div class="two columns offset-by-one">
<div data-bind="playback-volume"></div>
</div>
<div class="two columns">
<div data-bind="playback-random"></div>
</div>
<div class="two columns">
<div data-bind="playback-repeat"></div>
</div>
<div class="two columns">
<div data-bind="playback-single"></div>
</div>
<div class="two columns">
<div data-bind="playback-consume"></div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,44 @@
<script type="text/x-template" id="tmpl-widget-music">
<div class="music">
<div class="track">
<div class="unknown" v-if="!status">[Unknown state]</div>
<div class="no-track" v-if="status && status.state === 'stop'">No media is being played</div>
<div class="artist" v-if="status && status.state !== 'stop' && track && track.artist" v-text="track.artist"></div>
<div class="title" v-if="status && status.state !== 'stop' && track && track.title" v-text="track.title"></div>
</div>
<div class="time" v-if="status && status.state === 'play'">
<div class="row">
<div class="progress-bar">
<div class="elapsed" :style="{width: track.time ? 100*(status.elapsed/track.time) + '%' : '100%'}"></div>
<div class="total"></div>
</div>
</div>
<div class="row">
<div class="col-6 time-elapsed" v-text="convertTime(status.elapsed)"></div>
<div class="col-6 time-total" v-if="track.time" v-text="convertTime(track.time)"></div>
</div>
</div>
<div class="playback-status" v-if="status">
<div class="status-property col-4">
<i class="fa fa-volume-up"></i>&nbsp; <span v-text="status.volume + '%'"></span>
</div>
<div class="status-property col-2">
<i class="fas fa-random" :class="{active: status.random}"></i>
</div>
<div class="status-property col-2">
<i class="fas fa-redo" :class="{active: status.repeat}"></i>
</div>
<div class="status-property col-2">
<i class="fa fa-bullseye" :class="{active: status.single}"></i>
</div>
<div class="status-property col-2">
<i class="fa fa-utensils" :class="{active: status.consume}"></i>
</div>
</div>
</div>
</script>

View file

@ -1,5 +0,0 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/rss-news.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/widgets/rss-news.css') }}"></script>
<div class="news-container"></div>

View file

@ -0,0 +1,9 @@
<script type="text/x-template" id="tmpl-widget-rss-news">
<div class="rss-news">
<div class="article" v-if="currentArticle">
<div class="source" v-text="currentArticle.source"></div>
<div class="title" v-text="currentArticle.title"></div>
<div class="published" v-text="new Date(currentArticle.published).toDateString() + ', ' + new Date(currentArticle.published).toTimeString().substring(0,5)"></div>
</div>
</div>
</script>

View file

@ -0,0 +1,6 @@
<script type="text/x-template" id="tmpl-widget">
<div class="widget" :class="'col-' + config.columns">
<component :is="tag" :config="config"></component>
</div>
</script>

View file

@ -13,33 +13,14 @@ class HttpUtils(object):
@staticmethod @staticmethod
def widget_columns_to_html_class(columns): def widget_columns_to_html_class(columns):
if not isinstance(columns, int): if not isinstance(columns, int):
raise RuntimeError('columns should be a number, got "{}"'.format(columns)) try:
columns = int(columns)
except ValueError:
raise RuntimeError('columns should be a number, got {} ({})'.format(type(columns), columns))
if 1 <= columns <= 12:
return 'col-{}'.format(columns)
if columns == 1:
return 'one column'
elif columns == 2:
return 'two columns'
elif columns == 3:
return 'three columns'
elif columns == 4:
return 'four columns'
elif columns == 5:
return 'five columns'
elif columns == 6:
return 'six columns'
elif columns == 7:
return 'seven columns'
elif columns == 8:
return 'eight columns'
elif columns == 9:
return 'nine columns'
elif columns == 10:
return 'ten columns'
elif columns == 11:
return 'eleven columns'
elif columns == 12:
return 'twelve columns'
else:
raise RuntimeError('Constraint violation: should be 1 <= columns <= 12, ' + raise RuntimeError('Constraint violation: should be 1 <= columns <= 12, ' +
'got columns={}'.format(columns)) 'got columns={}'.format(columns))
@ -127,5 +108,8 @@ class HttpUtils(object):
with open(file) as f: with open(file) as f:
return f.read() return f.read()
@classmethod
def isfile(cls, file):
return os.path.isfile(file)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -2,6 +2,7 @@ import json
import threading import threading
import time import time
from platypush.backend.http.utils import HttpUtils
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
from platypush.procedure import Procedure from platypush.procedure import Procedure
@ -313,4 +314,13 @@ class UtilsPlugin(Plugin):
} }
} }
@action
def search_directory(self, directory, extensions, recursive=False):
return HttpUtils.search_directory(directory, recursive=recursive, *extensions)
@action
def search_web_directory(self, directory, extensions):
return HttpUtils.search_web_directory(directory, *extensions)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et: