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/
config
platypush/backend/http/static/css/*/.sass-cache/
.vscode

View File

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

View File

@ -1,6 +1,6 @@
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, \
get_websocket_port
@ -14,17 +14,21 @@ __routes__ = [
dashboard,
]
@dashboard.route('/dashboard', methods=['GET'])
def 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')
dashboard_conf = http_conf.get('dashboard', {})
return render_template('dashboard.html', config=dashboard_conf,
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:

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() {
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();
Vue.component('widget', {
template: '#tmpl-widget',
props: ['config','tag'],
});
// 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,
elapsed: null,
},
newTrackLock: false,
};
},
@ -467,9 +465,6 @@ Vue.component('music-mpd', {
}
}
};
// adjust(this)();
// setInterval(adjust(this), 2000);
},
_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,72 +1,107 @@
<!doctype html>
<head>
<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" 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>
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Lato">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/skeleton.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/normalize.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='font-awesome/css/all.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/dist/dashboard.css') }}">
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/vue.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/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">
window.websocket_port = {% print(websocket_port) %};
window.has_ssl = {% print('true' if has_ssl else 'false') %};
if (!window.config) {
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 %}
window.token = '{% print(token) %}';
window.config.token = '{{ token }}';
{% else %}
window.token = undefined;
window.config.token = undefined;
{% endif %}
window.config = {{ config | safe }};
window.widgets = {{ config['widgets'] | safe }}.reduce(function(map, w) { map[w.widget] = w; return map }, {})
window.config.dashboard = {{ config | safe }};
window.config.widgets = {{ config['widgets'] | safe }}.reduce(function(map, w) { map[w.widget] = w; return map }, {})
</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>
<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="app">
<main>
<!-- You can send events of type platypush.message.event.web.DashboardIframeUpdateEvent
to control what is shown in the optional iframe modal -->
<modal id="iframe-modal" v-model="iframeModal.visible">
<iframe ref="iframeModal"></iframe>
</modal>
<div id="widgets-container">
{% set used_columns = [0] %}
{% for widget in config['widgets'] %}
{% if used_columns[0] % 12 == 0 %}
<div class="row">
{% endif %}
<div id="widgets-container">
{% set used_columns = [0] %}
{% for widget in config['widgets'] %}
{% with name = widget['widget'] %}
{% if used_columns[0] % 12 == 0 %}
<div class="widgets-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' %}
{% with properties=widget %}
<widget :tag="'{{ name }}'.replace('.', '-')"
key="{{ name }}"
:config="{{ widget | safe }}">
</widget>
{% endwith %}
{# increment counter #}
{% if used_columns.append(used_columns.pop() + widget['columns']) %}{% endif %}
{% if used_columns[0] % 12 == 0 %}
</div>
{% endif %}
{% endwith %}
</div>
{% endfor %}
</div>
{# increment counter #}
{% if used_columns.append(used_columns.pop() + widget['columns']) %}{% endif %}
{% include 'notifications.html' %}
</main>
</div>
{% if used_columns[0] % 12 == 0 %}
</div>
{% endif %}
{% endfor %}
</div>
{% include 'widgets/template.html' %}
<div id="notification-container"></div>
</main>
<body>
<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('.', '-')"
:key="plugin"
:config="conf"
:class="{hidden: plugin != selectedPlugin}"/>
:class="{hidden: plugin != selectedPlugin}">
</plugin>
</div>
</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,35 +13,16 @@ class HttpUtils(object):
@staticmethod
def widget_columns_to_html_class(columns):
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 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, ' +
'got columns={}'.format(columns))
if 1 <= columns <= 12:
return 'col-{}'.format(columns)
raise RuntimeError('Constraint violation: should be 1 <= columns <= 12, ' +
'got columns={}'.format(columns))
@staticmethod
def search_directory(directory, *extensions, recursive=False):
@ -127,5 +108,8 @@ class HttpUtils(object):
with open(file) as f:
return f.read()
@classmethod
def isfile(cls, file):
return os.path.isfile(file)
# vim:sw=4:ts=4:et:

View File

@ -2,6 +2,7 @@ import json
import threading
import time
from platypush.backend.http.utils import HttpUtils
from platypush.plugins import Plugin, action
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: