forked from platypush/platypush
Dashboard rewritten in vue.js
This commit is contained in:
parent
8006f3688c
commit
09165ca0ff
74 changed files with 1129 additions and 3769 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -13,3 +13,4 @@ docs/build
|
|||
.idea/
|
||||
config
|
||||
platypush/backend/http/static/css/*/.sass-cache/
|
||||
.vscode
|
||||
|
|
|
@ -3,4 +3,3 @@ from platypush import main
|
|||
main()
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
1311
platypush/backend/http/static/css/jquery-ui.css
vendored
1311
platypush/backend/http/static/css/jquery-ui.css
vendored
File diff suppressed because it is too large
Load diff
|
@ -1,9 +0,0 @@
|
|||
#map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: static !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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%;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
#tts-container {
|
||||
max-width: 60em;
|
||||
margin: 3em auto;
|
||||
}
|
||||
|
||||
#tts-form input[type=text] {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
// });
|
||||
|
||||
|
|
46
platypush/backend/http/static/js/dashboard_old.js
Normal file
46
platypush/backend/http/static/js/dashboard_old.js
Normal 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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
37
platypush/backend/http/static/js/widgets/calendar/index.js
Normal file
37
platypush/backend/http/static/js/widgets/calendar/index.js
Normal 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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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');
|
||||
},
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
233
platypush/backend/http/static/js/widgets/music/index.js
Normal file
233
platypush/backend/http/static/js/widgets/music/index.js
Normal 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');
|
||||
},
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
40
platypush/backend/http/static/js/widgets/rss-news/index.js
Normal file
40
platypush/backend/http/static/js/widgets/rss-news/index.js
Normal 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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
72
platypush/backend/http/templates/dashboard_old.html
Normal file
72
platypush/backend/http/templates/dashboard_old.html
Normal 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>
|
||||
|
|
@ -68,7 +68,8 @@
|
|||
:tag="plugin.replace('.', '-')"
|
||||
:key="plugin"
|
||||
:config="conf"
|
||||
:class="{hidden: plugin != selectedPlugin}"/>
|
||||
:class="{hidden: plugin != selectedPlugin}">
|
||||
</plugin>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"> </div>
|
||||
<button data-direction="up" class="zb-ctrl-btn four columns">
|
||||
<i class="fa fa-sort-up"></i>
|
||||
</button>
|
||||
<div class="four columns"> </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"> </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"> </div>
|
||||
<button data-direction="down" class="zb-ctrl-btn four columns">
|
||||
<i class="fa fa-sort-down"></i>
|
||||
</button>
|
||||
<div class="four columns"> </div>
|
||||
</div>
|
||||
|
||||
<div class="row ctrl-bottom-row">
|
||||
<div class="two columns"> </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"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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> -->
|
||||
<!-- <input type="range" min="0" id="video-seeker" disabled="disabled" class="slider" style="width:75%"> -->
|
||||
<!-- <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>
|
||||
<input type="range" min="0" max="100" value="100" id="video-volume-ctrl" class="slider" style="width:80%">
|
||||
<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>
|
||||
|
|
@ -1 +0,0 @@
|
|||
media.html
|
|
@ -1 +0,0 @@
|
|||
media.html
|
|
@ -1 +0,0 @@
|
|||
media.html
|
|
@ -1 +0,0 @@
|
|||
media.html
|
|
@ -1 +0,0 @@
|
|||
gpio.sensor.mcp3008.html
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
15
platypush/backend/http/templates/widgets/calendar/index.html
Normal file
15
platypush/backend/http/templates/widgets/calendar/index.html
Normal 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>
|
|
@ -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>
|
||||
<span data-bind="temperature">N/A</span>°
|
||||
</h1>
|
||||
|
||||
<div class="forecast" data-bind="forecast"></div>
|
||||
|
||||
<div class="sensors row">
|
||||
<div class="sensor-temperature six columns">
|
||||
<i class="fa fa-thermometer"></i>
|
||||
<span data-bind="sensor-temperature">N/A</span>°
|
||||
</div>
|
||||
|
||||
<div class="sensor-humidity six columns">
|
||||
<i class="fa fa-tint"></i>
|
||||
<span data-bind="sensor-humidity">N/A</span>%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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>
|
||||
<span class="temperature" v-if="weather">
|
||||
{% raw %}
|
||||
{{ Math.round(parseFloat(weather.temperature)) + '°' }}
|
||||
{% 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>
|
||||
<span class="temperature">
|
||||
{% raw %}
|
||||
{{ parseFloat(sensors.temperature).toFixed(1) + '°'}}
|
||||
{% endraw %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="sensor humidity col-6" v-if="sensors.humidity">
|
||||
<i class="fa fa-tint"></i>
|
||||
<span class="humidity">
|
||||
{% raw %}
|
||||
{{ parseFloat(sensors.humidity).toFixed(1) + '%'}}
|
||||
{% endraw %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
|
@ -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"> </div>
|
||||
<img alt="Your carousel images">
|
||||
</div>
|
||||
|
|
@ -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>
|
|
@ -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>
|
||||
|
44
platypush/backend/http/templates/widgets/music/index.html
Normal file
44
platypush/backend/http/templates/widgets/music/index.html
Normal 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> <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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
6
platypush/backend/http/templates/widgets/template.html
Normal file
6
platypush/backend/http/templates/widgets/template.html
Normal 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>
|
||||
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue