Compare commits
42 Commits
c3f815939d
...
dev
Author | SHA1 | Date | |
---|---|---|---|
15e68b88e8 | |||
adb20dea14 | |||
016e52f1e7 | |||
6374c8d983 | |||
39b899283c | |||
0f47ff15dd | |||
70197ee393 | |||
98b09bb778 | |||
36c9b5015f | |||
87dedb8e02 | |||
7b82086ff0 | |||
5d1edbc6fc | |||
722db5feae | |||
120931dc4c | |||
d17a76f4b8 | |||
355ba99ca3 | |||
0cfc801f59 | |||
c9cbb53eea | |||
98a78f2102 | |||
cf9c4f0e85 | |||
3c6d950bbc | |||
46eece9b98 | |||
b0f4e98513 | |||
14670ae871 | |||
934c4f2a1d | |||
4a8ac52201 | |||
ee54dd5daa | |||
1b7980e834 | |||
999ae069da | |||
2ecb5a4b71 | |||
5e7080695d | |||
ae773daad7 | |||
a4265a44f5 | |||
dbb6d170da | |||
76a8b97ae4 | |||
908d64e0a9 | |||
02625299c7 | |||
7952ff2c12 | |||
c89ecd7134 | |||
a071193959 | |||
f156d38739 | |||
8f20be53e1 |
1
.gitignore
vendored
@ -146,4 +146,3 @@ cython_debug/
|
|||||||
|
|
||||||
# static files generated from Django application using `collectstatic`
|
# static files generated from Django application using `collectstatic`
|
||||||
media
|
media
|
||||||
static
|
|
||||||
|
2
.gitmodules
vendored
@ -1,3 +1,3 @@
|
|||||||
[submodule "backend"]
|
[submodule "backend"]
|
||||||
path = backend
|
path = backend
|
||||||
url = git@git.maenle.net:raphael/calenderwatch_server.git
|
url = git@git.maenle.net:raphael/calendarwatch_backend
|
||||||
|
11
README.md
@ -2,3 +2,14 @@
|
|||||||
|
|
||||||
this repository includes the webpage frontend for the calendar watch server
|
this repository includes the webpage frontend for the calendar watch server
|
||||||
it gets served via a python server
|
it gets served via a python server
|
||||||
|
|
||||||
|
### app description:
|
||||||
|
|
||||||
|
Longitude is a 24h slow watch-face.
|
||||||
|
The Sun moves around the watch over the course of the day, while the planet completes a rotation every hour. Add your Calendar via a web-platform, so that every glance at your watch shows a summary of your day.
|
||||||
|
|
||||||
|
Version 0.1 (beta):
|
||||||
|
- This version of the watchface includes the sun and the planet for time visualization
|
||||||
|
|
||||||
|
Future (beta):
|
||||||
|
- The next version will add support for connecting the device to the web platform
|
||||||
|
287
app.py
@ -1,287 +0,0 @@
|
|||||||
# Python standard libraries
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
# Third-party libraries
|
|
||||||
import flask
|
|
||||||
from flask import Flask, redirect, request, url_for
|
|
||||||
from flask_login import (
|
|
||||||
LoginManager,
|
|
||||||
current_user,
|
|
||||||
login_required,
|
|
||||||
login_user,
|
|
||||||
logout_user,
|
|
||||||
)
|
|
||||||
from oauthlib.oauth2 import WebApplicationClient
|
|
||||||
import requests
|
|
||||||
|
|
||||||
# Internal imports
|
|
||||||
from database.db import init_db_command
|
|
||||||
from database.user import User
|
|
||||||
from database.user import dbCalendar
|
|
||||||
|
|
||||||
import backend.caltojson as caltojson
|
|
||||||
|
|
||||||
import google.oauth2.credentials
|
|
||||||
import google_auth_oauthlib.flow
|
|
||||||
import googleapiclient.discovery
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
|
|
||||||
CLIENT_SECRETS_FILE = "certificate/client_secret.json"
|
|
||||||
|
|
||||||
# This OAuth 2.0 access scope allows for full read/write access to the
|
|
||||||
# authenticated user's account and requires requests to use an SSL connection.
|
|
||||||
SCOPES = ["https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/calendar.readonly", "openid"]
|
|
||||||
API_SERVICE_NAME = 'calendar'
|
|
||||||
API_VERSION = 'v3'
|
|
||||||
|
|
||||||
GOOGLE_CLIENT_ID ="377787187748-shuvi4iq5bi4gdet6q3ioataimobs4lh.apps.googleusercontent.com"
|
|
||||||
GOOGLE_CLIENT_SECRET = "Hu_YWmKsVKUcLwyeINYzdKfZ"
|
|
||||||
GOOGLE_DISCOVERY_URL = (
|
|
||||||
"https://accounts.google.com/.well-known/openid-configuration"
|
|
||||||
)
|
|
||||||
# Flask app setup
|
|
||||||
app = Flask(__name__,
|
|
||||||
static_folder='static',
|
|
||||||
template_folder='template')
|
|
||||||
app.secret_key = os.environ.get("SECRET_KEY") or os.urandom(24)
|
|
||||||
|
|
||||||
# User session management setup
|
|
||||||
# https://flask-login.readthedocs.io/en/latest
|
|
||||||
login_manager = LoginManager()
|
|
||||||
login_manager.init_app(app)
|
|
||||||
|
|
||||||
# Naive database setup
|
|
||||||
try:
|
|
||||||
init_db_command()
|
|
||||||
except sqlite3.OperationalError:
|
|
||||||
# Assume it's already been created
|
|
||||||
pass
|
|
||||||
|
|
||||||
# OAuth 2 client setup
|
|
||||||
client = WebApplicationClient(GOOGLE_CLIENT_ID)
|
|
||||||
|
|
||||||
# Flask-Login helper to retrieve a user from our db
|
|
||||||
@login_manager.user_loader
|
|
||||||
def load_user(user_id):
|
|
||||||
return User.get(user_id)
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def account():
|
|
||||||
return flask.redirect('account')
|
|
||||||
|
|
||||||
@app.route("/account")
|
|
||||||
def index():
|
|
||||||
if current_user.is_authenticated:
|
|
||||||
updateCalendars()
|
|
||||||
return (flask.render_template('account.html',
|
|
||||||
username = current_user.name, email = current_user.email, profile_img=current_user.profile_pic
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return flask.render_template('login.html')
|
|
||||||
|
|
||||||
def get_google_provider_cfg():
|
|
||||||
return requests.get(GOOGLE_DISCOVERY_URL).json()
|
|
||||||
|
|
||||||
class Calendar:
|
|
||||||
def __init__(self, name, toggle=0, color="#000000"):
|
|
||||||
self.name = name
|
|
||||||
self.color = color
|
|
||||||
|
|
||||||
if toggle == 0:
|
|
||||||
self.toggle = False
|
|
||||||
else:
|
|
||||||
self.toggle = True
|
|
||||||
|
|
||||||
def calendarsFromDb():
|
|
||||||
calendars = dbCalendar.getCalendars(current_user.id)
|
|
||||||
pyCalendars = []
|
|
||||||
for calendar in calendars:
|
|
||||||
name = calendar[2]
|
|
||||||
calId = calendar[1]
|
|
||||||
toggle = calendar[3]
|
|
||||||
color = calendar[4]
|
|
||||||
|
|
||||||
pyCalendars.append(Calendar(name, toggle, color))
|
|
||||||
|
|
||||||
return pyCalendars
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/calendar")
|
|
||||||
@login_required
|
|
||||||
def calendar():
|
|
||||||
calendars = calendarsFromDb()
|
|
||||||
return flask.render_template('calendar.html', calendars=calendars)
|
|
||||||
|
|
||||||
def getCalendarJson():
|
|
||||||
if 'credentials' not in flask.session:
|
|
||||||
return flask.redirect('login/google')
|
|
||||||
|
|
||||||
# Load credentials from the session.
|
|
||||||
credentials = google.oauth2.credentials.Credentials(
|
|
||||||
**flask.session['credentials'])
|
|
||||||
todaysCal = caltojson.generateJsonFromCalendarEntries(credentials)
|
|
||||||
|
|
||||||
with open('./userinfo/' + current_user.id + '/calendarevents.json', 'w') as outfile:
|
|
||||||
json.dump(todaysCal, outfile)
|
|
||||||
|
|
||||||
return todaysCal
|
|
||||||
|
|
||||||
|
|
||||||
def updateCalendars():
|
|
||||||
if 'credentials' not in flask.session:
|
|
||||||
return flask.redirect('login/google')
|
|
||||||
|
|
||||||
# Load credentials from the session.
|
|
||||||
credentials = google.oauth2.credentials.Credentials(
|
|
||||||
**flask.session['credentials'])
|
|
||||||
|
|
||||||
|
|
||||||
calendars = caltojson.getCalendarList(credentials)
|
|
||||||
|
|
||||||
for calendar in calendars:
|
|
||||||
if dbCalendar.getCalendar(current_user.id, calendar.calendarId) == None:
|
|
||||||
dbCalendar.create(current_user.id, calendar.calendarId, calendar.summary, calendar.color)
|
|
||||||
|
|
||||||
print("updated Calendars")
|
|
||||||
|
|
||||||
# Save credentials back to session in case access token was refreshed.
|
|
||||||
# ACTION ITEM: In a production app, you likely want to save these
|
|
||||||
# credentials in a persistent database instead.
|
|
||||||
flask.session['credentials'] = credentials_to_dict(credentials)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/login/google")
|
|
||||||
def login():
|
|
||||||
|
|
||||||
'''
|
|
||||||
# Find out what URL to hit for Google login
|
|
||||||
google_provider_cfg = get_google_provider_cfg()
|
|
||||||
authorization_endpoint = google_provider_cfg["authorization_endpoint"]
|
|
||||||
|
|
||||||
# Use library to construct the request for Google login and provide
|
|
||||||
# scopes that let you retrieve user's profile from Google
|
|
||||||
request_uri = client.prepare_request_uri(
|
|
||||||
authorization_endpoint,
|
|
||||||
redirect_uri=request.base_url + "/callback",
|
|
||||||
scope=["openid", "email", "profile", "https://www.googleapis.com/auth/calendar.readonly"],
|
|
||||||
)
|
|
||||||
return redirect(request_uri)
|
|
||||||
'''
|
|
||||||
|
|
||||||
# Create flow instance to manage the OAuth 2.0 Authorization Grant Flow steps.
|
|
||||||
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
|
|
||||||
CLIENT_SECRETS_FILE, scopes=SCOPES)
|
|
||||||
# The URI created here must exactly match one of the authorized redirect URIs
|
|
||||||
# for the OAuth 2.0 client, which you configured in the API Console. If this
|
|
||||||
# value doesn't match an authorized URI, you will get a 'redirect_uri_mismatch'
|
|
||||||
# error.
|
|
||||||
flow.redirect_uri = request.base_url + "/callback"
|
|
||||||
authorization_url, state = flow.authorization_url(
|
|
||||||
# Enable offline access so that you can refresh an access token without
|
|
||||||
# re-prompting the user for permission. Recommended for web server apps.
|
|
||||||
access_type='offline',
|
|
||||||
# Enable incremental authorization. Recommended as a best practice.
|
|
||||||
include_granted_scopes='true')
|
|
||||||
|
|
||||||
# Store the state so the callback can verify the auth server response.
|
|
||||||
flask.session['state'] = state
|
|
||||||
|
|
||||||
return flask.redirect(authorization_url)
|
|
||||||
|
|
||||||
@app.route("/login/google/callback")
|
|
||||||
def callback():
|
|
||||||
# Specify the state when creating the flow in the callback so that it can
|
|
||||||
# verified in the authorization server response.
|
|
||||||
state = flask.session['state']
|
|
||||||
|
|
||||||
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
|
|
||||||
CLIENT_SECRETS_FILE, scopes=SCOPES, state=state)
|
|
||||||
flow.redirect_uri = request.base_url
|
|
||||||
|
|
||||||
# Use the authorization server's response to fetch the OAuth 2.0 tokens.
|
|
||||||
authorization_response = flask.request.url
|
|
||||||
flow.fetch_token(authorization_response=authorization_response)
|
|
||||||
|
|
||||||
# Store credentials in the session.
|
|
||||||
# ACTION ITEM: In a production app, you likely want to save these
|
|
||||||
# credentials in a persistent database instead.
|
|
||||||
credentials = flow.credentials
|
|
||||||
flask.session['credentials'] = credentials_to_dict(credentials)
|
|
||||||
|
|
||||||
session = flow.authorized_session()
|
|
||||||
|
|
||||||
userinfo = session.get('https://www.googleapis.com/userinfo/v2/me').json()
|
|
||||||
|
|
||||||
# Create a user in your db with the information provided
|
|
||||||
# by Google
|
|
||||||
user = User(
|
|
||||||
id_=userinfo['id'],
|
|
||||||
name=userinfo['name'],
|
|
||||||
email=userinfo['email'],
|
|
||||||
profile_pic=userinfo['picture']
|
|
||||||
)
|
|
||||||
|
|
||||||
# Doesn't exist? Add it to the database.
|
|
||||||
if not User.get(user.id):
|
|
||||||
User.create(user.id, user.name, user.email, user.profile_pic)
|
|
||||||
|
|
||||||
# Begin user session by logging the user in
|
|
||||||
login_user(user)
|
|
||||||
return flask.redirect(flask.url_for('index'))
|
|
||||||
|
|
||||||
@app.route("/logout")
|
|
||||||
@login_required
|
|
||||||
def logout():
|
|
||||||
logout_user()
|
|
||||||
return redirect(url_for("index"))
|
|
||||||
|
|
||||||
def credentials_to_dict(credentials):
|
|
||||||
return {'token': credentials.token,
|
|
||||||
'refresh_token': credentials.refresh_token,
|
|
||||||
'token_uri': credentials.token_uri,
|
|
||||||
'client_id': credentials.client_id,
|
|
||||||
'client_secret': credentials.client_secret,
|
|
||||||
'scopes': credentials.scopes}
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/userinfo/<path:user>/calendarevents.json")
|
|
||||||
def downloader(user):
|
|
||||||
print(user)
|
|
||||||
path = "/home/raphael/dev/website_ws/website/userinfo/" + user
|
|
||||||
return flask.send_from_directory(path, "calendarevents.json")
|
|
||||||
|
|
||||||
# POST
|
|
||||||
|
|
||||||
@app.route('/calendar', methods = ['POST', 'DELETE'])
|
|
||||||
@login_required
|
|
||||||
def user():
|
|
||||||
if request.method == 'POST':
|
|
||||||
calId = request.json.get('calendar_id')
|
|
||||||
color = request.json.get('color')
|
|
||||||
toggle = request.json.get('toggle')
|
|
||||||
|
|
||||||
print(calId)
|
|
||||||
if color != None:
|
|
||||||
print(color)
|
|
||||||
if toggle != None:
|
|
||||||
print(toggle)
|
|
||||||
# toggle specific calendar of user
|
|
||||||
elif request.method == 'DELETE':
|
|
||||||
# do nothing
|
|
||||||
return 'NONE'
|
|
||||||
else:
|
|
||||||
# POST Error 405
|
|
||||||
print("405")
|
|
||||||
|
|
||||||
return 'OK'
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
context = ('certificate/xip.io.crt', 'certificate/xip.io.key')#certificate and key files
|
|
||||||
app.run('0.0.0.0', 1234, ssl_context=context, debug=True)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
2
backend
7
config.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import os
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
# ...
|
||||||
|
SQLALCHEMY_DATABASE_URI = 'mysql://user:pw@mariadb:3306/calendarwatch'
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
@ -1,38 +0,0 @@
|
|||||||
# http://flask.pocoo.org/docs/1.0/tutorial/database/
|
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
import click
|
|
||||||
from flask import current_app, g
|
|
||||||
from flask.cli import with_appcontext
|
|
||||||
|
|
||||||
def get_db():
|
|
||||||
if "db" not in g:
|
|
||||||
g.db = sqlite3.connect(
|
|
||||||
"database/sqlite_db", detect_types=sqlite3.PARSE_DECLTYPES
|
|
||||||
)
|
|
||||||
g.db.row_factory = sqlite3.Row
|
|
||||||
|
|
||||||
return g.db
|
|
||||||
|
|
||||||
def close_db(e=None):
|
|
||||||
db = g.pop("db/db", None)
|
|
||||||
|
|
||||||
if db is not None:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
def init_db():
|
|
||||||
db = get_db()
|
|
||||||
|
|
||||||
with current_app.open_resource("database/schema.sql") as f:
|
|
||||||
db.executescript(f.read().decode("utf8"))
|
|
||||||
|
|
||||||
@click.command("init-db")
|
|
||||||
@with_appcontext
|
|
||||||
def init_db_command():
|
|
||||||
"""Clear the existing data and create new tables."""
|
|
||||||
init_db()
|
|
||||||
click.echo("Initialized the database.")
|
|
||||||
|
|
||||||
def init_app(app):
|
|
||||||
app.teardown_appcontext(close_db)
|
|
||||||
app.cli.add_command(init_db_command)
|
|
1
database/migrations/README
Normal file
@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
45
database/migrations/alembic.ini
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
96
database/migrations/env.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy import pool
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
logger = logging.getLogger('alembic.env')
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
from flask import current_app
|
||||||
|
config.set_main_option(
|
||||||
|
'sqlalchemy.url',
|
||||||
|
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
|
||||||
|
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url, target_metadata=target_metadata, literal_binds=True
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this callback is used to prevent an auto-migration from being generated
|
||||||
|
# when there are no changes to the schema
|
||||||
|
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||||
|
def process_revision_directives(context, revision, directives):
|
||||||
|
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||||
|
script = directives[0]
|
||||||
|
if script.upgrade_ops.is_empty():
|
||||||
|
directives[:] = []
|
||||||
|
logger.info('No changes in schema detected.')
|
||||||
|
|
||||||
|
connectable = engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section),
|
||||||
|
prefix='sqlalchemy.',
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
process_revision_directives=process_revision_directives,
|
||||||
|
**current_app.extensions['migrate'].configure_args
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
24
database/migrations/script.py.mako
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
34
database/migrations/versions/1e8205594ac1_.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 1e8205594ac1
|
||||||
|
Revises: aeab4aff199b
|
||||||
|
Create Date: 2020-05-27 16:57:54.384047
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1e8205594ac1'
|
||||||
|
down_revision = 'aeab4aff199b'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('user', sa.Column('userid', sa.String(length=64), nullable=True))
|
||||||
|
op.create_index(op.f('ix_user_userid'), 'user', ['userid'], unique=True)
|
||||||
|
op.drop_index('ix_user_username', table_name='user')
|
||||||
|
op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_user_username'), table_name='user')
|
||||||
|
op.create_index('ix_user_username', 'user', ['username'], unique=True)
|
||||||
|
op.drop_index(op.f('ix_user_userid'), table_name='user')
|
||||||
|
op.drop_column('user', 'userid')
|
||||||
|
# ### end Alembic commands ###
|
72
database/migrations/versions/aeab4aff199b_.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: aeab4aff199b
|
||||||
|
Revises:
|
||||||
|
Create Date: 2020-05-27 15:23:20.611265
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'aeab4aff199b'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('user',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('username', sa.String(length=64), nullable=True),
|
||||||
|
sa.Column('email', sa.String(length=120), nullable=True),
|
||||||
|
sa.Column('profile_pic', sa.String(length=256), nullable=True),
|
||||||
|
sa.Column('password_hash', sa.String(length=128), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
|
||||||
|
op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True)
|
||||||
|
op.create_table('calendar',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('calendar_id', sa.String(length=256), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=256), nullable=True),
|
||||||
|
sa.Column('toggle', sa.String(length=8), nullable=True),
|
||||||
|
sa.Column('color', sa.String(length=16), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id', 'calendar_id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_calendar_name'), 'calendar', ['name'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_user_id'), 'calendar', ['user_id'], unique=False)
|
||||||
|
op.create_table('device',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('deviceName', sa.String(length=64), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('deviceName')
|
||||||
|
)
|
||||||
|
op.create_table('google_token',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('token', sa.String(length=256), nullable=True),
|
||||||
|
sa.Column('refresh_token', sa.String(length=256), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('google_token')
|
||||||
|
op.drop_table('device')
|
||||||
|
op.drop_index(op.f('ix_calendar_user_id'), table_name='calendar')
|
||||||
|
op.drop_index(op.f('ix_calendar_name'), table_name='calendar')
|
||||||
|
op.drop_table('calendar')
|
||||||
|
op.drop_index(op.f('ix_user_username'), table_name='user')
|
||||||
|
op.drop_index(op.f('ix_user_email'), table_name='user')
|
||||||
|
op.drop_table('user')
|
||||||
|
# ### end Alembic commands ###
|
28
database/migrations/versions/e5ef5e4a807b_.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: e5ef5e4a807b
|
||||||
|
Revises: 1e8205594ac1
|
||||||
|
Create Date: 2020-05-28 09:01:09.268270
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'e5ef5e4a807b'
|
||||||
|
down_revision = '1e8205594ac1'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('device', sa.Column('connection', sa.Boolean(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('device', 'connection')
|
||||||
|
# ### end Alembic commands ###
|
71
database/models.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import json
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from server import login_manager, db
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(id):
|
||||||
|
return User.query.get(id)
|
||||||
|
|
||||||
|
class User(UserMixin, db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
userid = db.Column(db.String(64), index=True, unique=True)
|
||||||
|
username = db.Column(db.String(64), index=True)
|
||||||
|
email = db.Column(db.String(120), index=True, unique=True)
|
||||||
|
profile_pic = db.Column(db.String(256))
|
||||||
|
password_hash = db.Column(db.String(128))
|
||||||
|
google_token = db.relationship('GoogleToken', uselist=False, backref = 'user', cascade="all, delete-orphan")
|
||||||
|
calendars = db.relationship('Calendar', backref='user', lazy=True, cascade="all, delete-orphan")
|
||||||
|
devices = db.relationship('Device', backref='user', cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<User {}>'.format(self.username)
|
||||||
|
|
||||||
|
def setPassword(self, password):
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
def checkPassword(self, password):
|
||||||
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
def updateCalendar(self, calendar_id, toggle=None, color=None):
|
||||||
|
|
||||||
|
for calendar in self.calendars:
|
||||||
|
if calendar.calendar_id == calendar_id:
|
||||||
|
break
|
||||||
|
|
||||||
|
print("updating", flush=True)
|
||||||
|
if(toggle != None):
|
||||||
|
print(toggle)
|
||||||
|
calendar.toggle = toggle
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if(color != None):
|
||||||
|
calendar.color = color
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def hasCalendar(self, calendar_id):
|
||||||
|
for calendar in self.calendars:
|
||||||
|
if calendar.calendar_id == calendar_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
class GoogleToken(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
|
token = db.Column(db.String(256))
|
||||||
|
refresh_token = db.Column(db.String(256))
|
||||||
|
|
||||||
|
class Device(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
|
deviceName = db.Column(db.String(64), unique=True)
|
||||||
|
connection = db.Column(db.Boolean)
|
||||||
|
|
||||||
|
class Calendar(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True, nullable=False)
|
||||||
|
calendar_id = db.Column(db.String(256), primary_key=True)
|
||||||
|
name = db.Column(db.String(256), index=True)
|
||||||
|
toggle = db.Column(db.String(8))
|
||||||
|
color = db.Column(db.String(16))
|
@ -1,14 +0,0 @@
|
|||||||
CREATE TABLE user (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
email TEXT UNIQUE NOT NULL,
|
|
||||||
profile_pic TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE calendar (
|
|
||||||
usr_id TEXT NOT NULL,
|
|
||||||
calendar_id TEXT PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
toggle INT NOT NULL,
|
|
||||||
color TEXT
|
|
||||||
);
|
|
@ -1,72 +0,0 @@
|
|||||||
from flask_login import UserMixin
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from database.db import get_db
|
|
||||||
|
|
||||||
class User(UserMixin):
|
|
||||||
def __init__(self, id_, name, email, profile_pic):
|
|
||||||
self.id = id_
|
|
||||||
self.name = name
|
|
||||||
self.email = email
|
|
||||||
self.profile_pic = profile_pic
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get(user_id):
|
|
||||||
db = get_db()
|
|
||||||
user = db.execute(
|
|
||||||
"SELECT * FROM user WHERE id = ?", (user_id,)
|
|
||||||
).fetchone()
|
|
||||||
if not user:
|
|
||||||
return None
|
|
||||||
|
|
||||||
user = User(
|
|
||||||
id_=user[0], name=user[1], email=user[2], profile_pic=user[3]
|
|
||||||
)
|
|
||||||
return user
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create(id_, name, email, profile_pic):
|
|
||||||
db = get_db()
|
|
||||||
db.execute(
|
|
||||||
"INSERT INTO user (id, name, email, profile_pic) "
|
|
||||||
"VALUES (?, ?, ?, ?)",
|
|
||||||
(id_, name, email, profile_pic),
|
|
||||||
)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
Path(f"userinfo/{id_}").mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
class dbCalendar():
|
|
||||||
def __init__(self, id_, name, toggle, color):
|
|
||||||
self.usr_id = id_
|
|
||||||
self.name = name
|
|
||||||
self.toggle = toggle
|
|
||||||
self.color = color
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def getCalendars(user_id):
|
|
||||||
db = get_db()
|
|
||||||
calendars = db.execute(
|
|
||||||
"SELECT * FROM calendar WHERE usr_id = ?", (user_id,)
|
|
||||||
).fetchall()
|
|
||||||
|
|
||||||
return calendars
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def getCalendar(user_id, calendar_id):
|
|
||||||
db = get_db()
|
|
||||||
calendar = db.execute(
|
|
||||||
"SELECT * FROM calendar WHERE usr_id = ? AND calendar_id = ?", (user_id, calendar_id,)
|
|
||||||
).fetchone()
|
|
||||||
if not calendar:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create(user_id, calendar_id, name, color, toggle = False):
|
|
||||||
db = get_db()
|
|
||||||
db.execute(
|
|
||||||
"INSERT INTO calendar (usr_id, calendar_id, name, toggle, color) "
|
|
||||||
"VALUES (?, ?, ?, ?, ?)",
|
|
||||||
(user_id, calendar_id, name, toggle, color),
|
|
||||||
)
|
|
||||||
db.commit()
|
|
10
docker/calendarwatch/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FROM python:3.8-slim-buster
|
||||||
|
RUN apt-get update && apt-get upgrade
|
||||||
|
RUN pip3 install flask Flask-SQLAlchemy flask_migrate flask_login flask_wtf python-dotenv
|
||||||
|
RUN apt-get install gcc libpcre3 libpcre3-dev libmariadbclient-dev -y
|
||||||
|
RUN pip3 install uwsgi email-validator RandomWords
|
||||||
|
RUN pip3 install google google-oauth google-auth-oauthlib google-api-python-client mysqlclient
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/
|
||||||
|
EXPOSE 8084
|
||||||
|
EXPOSE 3001
|
||||||
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
5
docker/calendarwatch/docker-entrypoint.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
cd /home/calendarwatch
|
||||||
|
uwsgi --ini wsgi.ini
|
||||||
|
echo "server has been started"
|
||||||
|
|
29
docker/docker-compose.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
calendarwatch:
|
||||||
|
build:
|
||||||
|
context: ./calendarwatch
|
||||||
|
image: calendarwatch:latest
|
||||||
|
container_name: calendarwatch
|
||||||
|
environment:
|
||||||
|
- FLASK_APP=/home/calendarwatch/server.py
|
||||||
|
volumes:
|
||||||
|
- ../:/home/calendarwatch
|
||||||
|
ports:
|
||||||
|
- "0.0.0.0:8084:8084"
|
||||||
|
|
||||||
|
mariadb:
|
||||||
|
image: mariadb
|
||||||
|
container_name: maridab
|
||||||
|
environment:
|
||||||
|
- MYSQL_ROOT_PASSWORD=pw
|
||||||
|
- MYSQL_DATABASE=calendarwatch
|
||||||
|
- MYSQL_USER=user
|
||||||
|
- MYSQL_PASSWORD=pw
|
||||||
|
volumes:
|
||||||
|
- database:/var/lib/mysql
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
database:
|
||||||
|
driver: local
|
||||||
|
|
5
server.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Configuration
|
||||||
|
from server import app
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host='0.0.0.0', port=8084, debug=True)
|
26
server/__init__.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from config import Config
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
|
||||||
|
from flask_login import LoginManager
|
||||||
|
|
||||||
|
# Flask app setup
|
||||||
|
app = Flask(__name__,
|
||||||
|
static_folder='static',
|
||||||
|
template_folder='template')
|
||||||
|
app.secret_key = os.environ.get("SECRET_KEY") or os.urandom(24)
|
||||||
|
app.config.from_object(Config)
|
||||||
|
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
migrate = Migrate(app, db)
|
||||||
|
|
||||||
|
# User session management setup
|
||||||
|
# https://flask-login.readthedocs.io/en/latest
|
||||||
|
login_manager = LoginManager(app)
|
||||||
|
|
||||||
|
from server import routes
|
||||||
|
from database import models
|
48
server/forms.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
||||||
|
from wtforms.validators import DataRequired, ValidationError, Email, EqualTo
|
||||||
|
from database.models import User, Device
|
||||||
|
import email_validator
|
||||||
|
|
||||||
|
'''
|
||||||
|
LoginForm defines the flask form used for the
|
||||||
|
email-based login procedure
|
||||||
|
This Form is based on 'The Flask Mega-Tutorial'
|
||||||
|
'''
|
||||||
|
class LoginForm(FlaskForm):
|
||||||
|
username = StringField('Username', validators=[DataRequired()])
|
||||||
|
password = PasswordField('Password', validators=[DataRequired()])
|
||||||
|
remember_me = BooleanField('Remember Me')
|
||||||
|
submit = SubmitField('Sign In')
|
||||||
|
|
||||||
|
'''
|
||||||
|
RegiatrationForm validates username, email and pw
|
||||||
|
This Form is based on 'The Flask mega-Tutorial'
|
||||||
|
'''
|
||||||
|
|
||||||
|
class RegistrationForm(FlaskForm):
|
||||||
|
username = StringField('Username', validators=[DataRequired()])
|
||||||
|
email = StringField('Email', validators=[DataRequired(), Email()])
|
||||||
|
password = PasswordField('Password', validators=[DataRequired()])
|
||||||
|
password2 = PasswordField(
|
||||||
|
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
|
||||||
|
submit = SubmitField('Register')
|
||||||
|
|
||||||
|
def validate_username(self, username):
|
||||||
|
user = User.query.filter_by(username=username.data).first()
|
||||||
|
if user is not None:
|
||||||
|
raise ValidationError('Please use a different username.')
|
||||||
|
|
||||||
|
def validate_email(self, email):
|
||||||
|
user = User.query.filter_by(email=email.data).first()
|
||||||
|
if user is not None:
|
||||||
|
raise ValidationError('Please use a different email address.')
|
||||||
|
|
||||||
|
class DeviceForm(FlaskForm):
|
||||||
|
deviceName=StringField('New Device ID', validators=[DataRequired()])
|
||||||
|
submit = SubmitField('Add New Device')
|
||||||
|
#
|
||||||
|
def validate_deviceName (self, deviceName):
|
||||||
|
device = Device.query.filter_by(deviceName=deviceName.data).first()
|
||||||
|
if device is None:
|
||||||
|
raise ValidationError('Device not Found')
|
210
server/googleHandler.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import google.oauth2.credentials
|
||||||
|
import google_auth_oauthlib.flow
|
||||||
|
import googleapiclient.discovery
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
|
||||||
|
from oauthlib.oauth2 import WebApplicationClient
|
||||||
|
import flask
|
||||||
|
|
||||||
|
# Python standard libraries
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Third-party libraries
|
||||||
|
import flask
|
||||||
|
from flask import Flask, redirect, request, url_for
|
||||||
|
from flask_login import (
|
||||||
|
LoginManager,
|
||||||
|
current_user,
|
||||||
|
login_required,
|
||||||
|
login_user,
|
||||||
|
logout_user,
|
||||||
|
)
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from database.models import Calendar as dbCalendar
|
||||||
|
from backend import Calendar, Event
|
||||||
|
|
||||||
|
# Configuration class for the google client
|
||||||
|
# all necessary variables and secrets are defined in this
|
||||||
|
# aswell as some helper functions
|
||||||
|
class GoogleClient():
|
||||||
|
def __init__(self):
|
||||||
|
self.CLIENT_SECRETS_FILE = "certificate/client_secret.json"
|
||||||
|
|
||||||
|
with open("/home/calendarwatch/certificate/google_client.json", encoding='utf-8') as json_file:
|
||||||
|
self.google_client = json.load(json_file)
|
||||||
|
|
||||||
|
self.SCOPES = self.google_client.get('scopes')
|
||||||
|
self.API_SERVICE_NAME = 'calendar'
|
||||||
|
self.API_VERSION = 'v3'
|
||||||
|
|
||||||
|
# GOOGLE_CLIENT_ID ="377787187748-shuvi4iq5bi4gdet6q3ioataimobs4lh.apps.googleusercontent.com"
|
||||||
|
self.GOOGLE_CLIENT_ID = self.google_client.get('client_id')
|
||||||
|
# GOOGLE_CLIENT_SECRET = "Hu_YWmKsVKUcLwyeINYzdKfZ"
|
||||||
|
self.GOOGLE_CLIENT_SECRET = self.google_client.get('client_secret')
|
||||||
|
self.GOOGLE_DISCOVERY_URL = (
|
||||||
|
"https://accounts.google.com/.well-known/openid-configuration"
|
||||||
|
)
|
||||||
|
|
||||||
|
# OAuth 2 client setup
|
||||||
|
self.client = WebApplicationClient(self.GOOGLE_CLIENT_ID)
|
||||||
|
|
||||||
|
|
||||||
|
def build_credentials(self, token, refresh_token):
|
||||||
|
data = {}
|
||||||
|
data['token'] = token
|
||||||
|
data['refresh_token'] = refresh_token
|
||||||
|
data['token_uri'] = self.google_client.get('token_uri')
|
||||||
|
data['client_id'] = self.google_client.get('client_id')
|
||||||
|
data['client_secret'] = self.google_client.get('client_secret')
|
||||||
|
data['scopes'] = self.google_client.get('scopes')
|
||||||
|
return data
|
||||||
|
|
||||||
|
GC = GoogleClient()
|
||||||
|
|
||||||
|
# stuff for OAuth login
|
||||||
|
def login():
|
||||||
|
# Create flow instance to manage the OAuth 2.0 Authorization Grant Flow steps.
|
||||||
|
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
|
||||||
|
GC.CLIENT_SECRETS_FILE, scopes=GC.SCOPES)
|
||||||
|
# The URI created here must exactly match one of the authorized redirect URIs
|
||||||
|
# for the OAuth 2.0 client, which you configured in the API Console. If this
|
||||||
|
# value doesn't match an authorized URI, you will get a 'redirect_uri_mismatch'
|
||||||
|
# error.
|
||||||
|
flow.redirect_uri = "https://longitudecalendar.com/login/google/callback"
|
||||||
|
authorization_url, state = flow.authorization_url(
|
||||||
|
# Enable offline access so that you can refresh an access token without
|
||||||
|
# re-prompting the user for permission. Recommended for web server apps.
|
||||||
|
access_type='offline',
|
||||||
|
# Enable incremental authorization. Recommended as a best practice.
|
||||||
|
include_granted_scopes='true')
|
||||||
|
|
||||||
|
# Store the state so the callback can verify the auth server response.
|
||||||
|
flask.session['state'] = state
|
||||||
|
# Flask-Login helper to retrieve a user from our db
|
||||||
|
return authorization_url
|
||||||
|
|
||||||
|
def verifyResponse():
|
||||||
|
# Specify the state when creating the flow in the callback so that it can
|
||||||
|
# verified in the authorization server response.
|
||||||
|
state = flask.session['state']
|
||||||
|
|
||||||
|
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
|
||||||
|
GC.CLIENT_SECRETS_FILE, scopes=GC.SCOPES, state=state)
|
||||||
|
flow.redirect_uri = "https://longitudecalendar.com/login/google/callback"
|
||||||
|
|
||||||
|
# Use the authorization server's response to fetch the OAuth 2.0 tokens.
|
||||||
|
authorization_response = flask.request.url
|
||||||
|
flow.fetch_token(authorization_response=authorization_response)
|
||||||
|
|
||||||
|
# Store credentials in the session.
|
||||||
|
# ACTION ITEM: In a production app, you likely want to save these
|
||||||
|
# credentials in a persistent database instead.
|
||||||
|
|
||||||
|
credentials = flow.credentials
|
||||||
|
flask.session['credentials'] = credentials_to_dict(credentials)
|
||||||
|
print(credentials_to_dict(credentials), flush=True)
|
||||||
|
session = flow.authorized_session()
|
||||||
|
return session, credentials_to_dict(credentials)
|
||||||
|
|
||||||
|
|
||||||
|
def get_google_provider_cfg():
|
||||||
|
return requests.get(GC.GOOGLE_DISCOVERY_URL).json()
|
||||||
|
|
||||||
|
def deleteAccount(user):
|
||||||
|
result = requests.post('https://oauth2.googleapis.com/revoke',
|
||||||
|
params={'token': user.google_token.token},
|
||||||
|
headers = {'content-type': 'applixation/x-www-form-urlencoded'})
|
||||||
|
print(result, flush=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def fetchCalendarEvents(user, calendars, startDate, endDate):
|
||||||
|
|
||||||
|
client_token = GC.build_credentials(user.google_token.token,
|
||||||
|
user.google_token.refresh_token)
|
||||||
|
credentials = google.oauth2.credentials.Credentials(**client_token)
|
||||||
|
|
||||||
|
service = build(GC.API_SERVICE_NAME, GC.API_VERSION, credentials=credentials)
|
||||||
|
|
||||||
|
all_events = []
|
||||||
|
for calendar in calendars:
|
||||||
|
if calendar.toggle == "True":
|
||||||
|
event_result = service.events().list(calendarId=calendar.calendar_id,
|
||||||
|
timeMin=startDate,
|
||||||
|
timeMax=endDate,
|
||||||
|
maxResults=10,
|
||||||
|
singleEvents=True,
|
||||||
|
orderBy='startTime').execute()
|
||||||
|
|
||||||
|
for event in event_result.get('items', []):
|
||||||
|
|
||||||
|
# create simple event
|
||||||
|
name = event.get('summary', '(no Title)')
|
||||||
|
start = event['start'].get('dateTime')
|
||||||
|
end = event['end'].get('dateTime')
|
||||||
|
newEvent = Event(name, start, end)
|
||||||
|
|
||||||
|
# handle weird colors from google
|
||||||
|
color = event.get('colorId')
|
||||||
|
if color == None:
|
||||||
|
newEvent.colorHex = calendar.color
|
||||||
|
newEvent.eventColorId = None
|
||||||
|
else:
|
||||||
|
newEvent.eventColorId = color
|
||||||
|
|
||||||
|
all_events.append(newEvent)
|
||||||
|
|
||||||
|
colors = service.colors().get().execute()
|
||||||
|
|
||||||
|
return all_events, colors
|
||||||
|
|
||||||
|
|
||||||
|
def fetchCalendars():
|
||||||
|
# get client api service
|
||||||
|
if current_user.google_token == None:
|
||||||
|
return [], None, None
|
||||||
|
client_token = GC.build_credentials(current_user.google_token.token,
|
||||||
|
current_user.google_token.refresh_token)
|
||||||
|
credentials = google.oauth2.credentials.Credentials(**client_token)
|
||||||
|
|
||||||
|
service = build(GC.API_SERVICE_NAME, GC.API_VERSION, credentials=credentials)
|
||||||
|
|
||||||
|
# get all calendars and put them into Calendar Class
|
||||||
|
page_token = None
|
||||||
|
calendars = []
|
||||||
|
while True:
|
||||||
|
calendar_list = service.calendarList().list(pageToken=page_token).execute()
|
||||||
|
for calendar in calendar_list['items']:
|
||||||
|
calendars.append(Calendar(name=calendar['summary'],
|
||||||
|
calendarId=calendar['id'],
|
||||||
|
color=calendar['colorId']))
|
||||||
|
page_token = calendar_list.get('nextPageToken')
|
||||||
|
if not page_token:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
colors = service.colors().get().execute()
|
||||||
|
|
||||||
|
return calendars, colors, credentials.token
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def getUserCredentials(user):
|
||||||
|
credentials = GC.build_credentials(user.google_token.token,
|
||||||
|
user.google_token.refresh_token)
|
||||||
|
googleCreds = google.oauth2.credentials.Credentials(**credentials)
|
||||||
|
return googleCreds
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def credentials_to_dict(credentials):
|
||||||
|
return {'token': credentials.token,
|
||||||
|
'refresh_token': credentials.refresh_token,
|
||||||
|
'token_uri': credentials.token_uri,
|
||||||
|
'client_id': credentials.client_id,
|
||||||
|
'client_secret': credentials.client_secret,
|
||||||
|
'scopes': credentials.scopes}
|
||||||
|
|
||||||
|
|
266
server/routes.py
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
# Python standard libraries
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Third-party libraries
|
||||||
|
import flask
|
||||||
|
from flask import render_template, flash
|
||||||
|
from flask import Flask, redirect, request, url_for, jsonify
|
||||||
|
from flask_login import (
|
||||||
|
LoginManager,
|
||||||
|
current_user,
|
||||||
|
login_required,
|
||||||
|
login_user,
|
||||||
|
logout_user,
|
||||||
|
)
|
||||||
|
from random_words import RandomWords
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import server.googleHandler as google
|
||||||
|
|
||||||
|
from server import login_manager, app, db
|
||||||
|
from server.forms import LoginForm, RegistrationForm, DeviceForm
|
||||||
|
import backend
|
||||||
|
from database.models import User, Calendar, Device, GoogleToken
|
||||||
|
|
||||||
|
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def startpage():
|
||||||
|
return flask.render_template('startpage.html')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/privacy")
|
||||||
|
def privacy():
|
||||||
|
return flask.render_template('privacy.html')
|
||||||
|
|
||||||
|
@app.route("/login")
|
||||||
|
def login():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for('account'))
|
||||||
|
return flask.render_template('login.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/account")
|
||||||
|
def account():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
calendars = []
|
||||||
|
gCalendars, colors, token = google.fetchCalendars()
|
||||||
|
if token != None:
|
||||||
|
current_user.google_token.token = token
|
||||||
|
db.session.commit()
|
||||||
|
calendars.extend(gCalendars)
|
||||||
|
backend.updateCalendars(current_user, calendars, colors)
|
||||||
|
|
||||||
|
return (flask.render_template('account.html',
|
||||||
|
username = current_user.username, email = current_user.email, profile_img=current_user.profile_pic
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
@app.route("/view")
|
||||||
|
def view():
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
return flask.render_template('login.html')
|
||||||
|
else:
|
||||||
|
return (flask.render_template('view.html'))
|
||||||
|
|
||||||
|
@app.route("/devices", methods=['GET', 'POST'])
|
||||||
|
def devices():
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
return flask.render_template('login.html')
|
||||||
|
|
||||||
|
# if this is a post request from the 'unlink' submittion form
|
||||||
|
# delete this device from the list
|
||||||
|
form = DeviceForm()
|
||||||
|
if request.method == 'POST':
|
||||||
|
|
||||||
|
if request.form.get("submit") == "Unlink":
|
||||||
|
device = db.session.query(Device).filter(Device.deviceName==request.form.get("device")).first()
|
||||||
|
db.session.delete(device)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# if this is part of the device form
|
||||||
|
elif form.validate_on_submit():
|
||||||
|
device = db.session.query(Device).filter(Device.deviceName==form.deviceName.data).first()
|
||||||
|
current_user.devices.append(device)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return flask.render_template('devices.html',
|
||||||
|
devices=current_user.devices,
|
||||||
|
form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/calendar")
|
||||||
|
@login_required
|
||||||
|
def calendar():
|
||||||
|
calendars = backend.calendarsFromDb(current_user)
|
||||||
|
return flask.render_template('calendar.html', calendars=calendars)
|
||||||
|
|
||||||
|
@app.route('/login/email', methods=['GET', 'POST'])
|
||||||
|
def emaillogin():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for('account') )
|
||||||
|
form = LoginForm()
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user = User.query.filter_by(username=form.username.data).first()
|
||||||
|
if user is None or not user.checkPassword(form.password.data):
|
||||||
|
flash('invalid username or password')
|
||||||
|
return redirect(url_for('emaillogin'))
|
||||||
|
login_user(user, remember=form.remember_me.data)
|
||||||
|
return redirect(url_for('account'))
|
||||||
|
return render_template('emaillogin.html', title='Sign In', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for('account'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
form = RegistrationForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user = User(userid=form.username.data,
|
||||||
|
username=form.username.data,
|
||||||
|
email=form.email.data)
|
||||||
|
user.setPassword(form.password.data)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Congratulations, you are now a registered user!')
|
||||||
|
return redirect(url_for('emaillogin'))
|
||||||
|
return flask.render_template('register.html', title='Register', form=form)
|
||||||
|
|
||||||
|
@app.route("/delete_account")
|
||||||
|
def deleteAccount():
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
return redirect(url_for('account'))
|
||||||
|
if current_user.google_token != None:
|
||||||
|
google.deleteAccount(current_user)
|
||||||
|
|
||||||
|
db.session.delete(current_user)
|
||||||
|
db.session.commit()
|
||||||
|
logout_user()
|
||||||
|
|
||||||
|
return redirect(url_for('account'))
|
||||||
|
|
||||||
|
@app.route("/login/google")
|
||||||
|
def googlelogin():
|
||||||
|
if current_user.is_authenticated and current_user.google_token != None:
|
||||||
|
return redirect(url_for('account'))
|
||||||
|
|
||||||
|
authorization_url = google.login()
|
||||||
|
|
||||||
|
return flask.redirect(authorization_url)
|
||||||
|
|
||||||
|
@app.route("/login/google/callback")
|
||||||
|
def callback():
|
||||||
|
session, credentials = google.verifyResponse()
|
||||||
|
userinfo = session.get('https://www.googleapis.com/userinfo/v2/me').json()
|
||||||
|
|
||||||
|
# Create a user in your db with the information provided
|
||||||
|
# by Google
|
||||||
|
|
||||||
|
# Doesn't exist? Add it to the database.
|
||||||
|
if not db.session.query(User).filter(User.userid==userinfo['id']).first():
|
||||||
|
gc = GoogleToken(token=credentials.get("token"),
|
||||||
|
refresh_token=credentials.get("refresh_token"))
|
||||||
|
db.session.add(gc)
|
||||||
|
newser = User(
|
||||||
|
|
||||||
|
userid=userinfo['id'],
|
||||||
|
username=userinfo['name'],
|
||||||
|
email=userinfo['email'],
|
||||||
|
profile_pic=userinfo['picture'],
|
||||||
|
password_hash="",
|
||||||
|
google_token = gc
|
||||||
|
)
|
||||||
|
db.session.add(newser)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
user = db.session.query(User).filter(User.userid==userinfo['id']).first()
|
||||||
|
|
||||||
|
# Begin user session by logging the user in
|
||||||
|
login_user(user)
|
||||||
|
|
||||||
|
return flask.redirect(flask.url_for('account'))
|
||||||
|
|
||||||
|
@app.route("/logout")
|
||||||
|
@login_required
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
return redirect(url_for("startpage"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/device/<path:device>/calendarevents.json")
|
||||||
|
def downloader(device):
|
||||||
|
path = "/home/calendarwatch/device/" + device + "/"
|
||||||
|
request_device = db.session.query(Device).filter(Device.deviceName==device).first()
|
||||||
|
if request_device == None:
|
||||||
|
return jsonify(kind="not found")
|
||||||
|
if request_device.user_id == None:
|
||||||
|
return jsonify(kind="unregistered")
|
||||||
|
|
||||||
|
request_device.connection=True
|
||||||
|
db.session.commit()
|
||||||
|
request_user = db.session.query(User).filter(User.id==request_device.user_id).first()
|
||||||
|
|
||||||
|
# TODO add test if googke token exists
|
||||||
|
# if request_user.google_token != Null:
|
||||||
|
# TODO only pass along google calendars form user
|
||||||
|
startDate, endDate = backend.getTimeStamps()
|
||||||
|
events, colors = google.fetchCalendarEvents(request_user, request_user.calendars, startDate, endDate)
|
||||||
|
calendarjson = backend.generateJsonFromCalendarEntries(events, colors)
|
||||||
|
return jsonify(calendarjson)
|
||||||
|
|
||||||
|
@app.route("/devicefingerprint.json")
|
||||||
|
def generateDeviceFingerprint():
|
||||||
|
# Create Three Random Words
|
||||||
|
r = RandomWords()
|
||||||
|
while True:
|
||||||
|
fingerprint = ""
|
||||||
|
length = 3
|
||||||
|
randos = r.random_words(count=length)
|
||||||
|
for i in range(len(randos)):
|
||||||
|
fingerprint += randos[i]
|
||||||
|
if i < length-1:
|
||||||
|
fingerprint += "-"
|
||||||
|
|
||||||
|
# check not in Device Database
|
||||||
|
if not db.session.query(Device).filter(Device.deviceName==fingerprint).first():
|
||||||
|
# Save as new Device
|
||||||
|
device = Device(deviceName=fingerprint, connection=False)
|
||||||
|
db.session.add(device)
|
||||||
|
db.session.commit()
|
||||||
|
break;
|
||||||
|
|
||||||
|
# Send to Device
|
||||||
|
return jsonify(deviceName=fingerprint)
|
||||||
|
|
||||||
|
# POST
|
||||||
|
|
||||||
|
@app.route('/calendar', methods = ['POST', 'DELETE'])
|
||||||
|
@login_required
|
||||||
|
def user():
|
||||||
|
if request.method == 'POST':
|
||||||
|
calId = request.json.get('calendar_id')
|
||||||
|
color = request.json.get('color', None)
|
||||||
|
toggle = request.json.get('toggle', None)
|
||||||
|
|
||||||
|
if color != None:
|
||||||
|
current_user.updateCalendar(calId, color=color)
|
||||||
|
if toggle != None:
|
||||||
|
current_user.updateCalendar(calId, toggle=toggle)
|
||||||
|
# toggle specific calendar of user
|
||||||
|
|
||||||
|
elif request.method == 'DELETE':
|
||||||
|
# do nothing
|
||||||
|
return 'NONE'
|
||||||
|
else:
|
||||||
|
# POST Error 405
|
||||||
|
print("405")
|
||||||
|
|
||||||
|
return 'OK'
|
103
server/static/css/colorPick.css
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
/*!
|
||||||
|
*
|
||||||
|
* ColorPick jQuery plugin
|
||||||
|
* https://github.com/philzet/ColorPick.js
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017-2019 Phil Zet (a.k.a. Phil Zakharchenko)
|
||||||
|
* Licensed under the MIT License
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local("Open Sans"), local("OpenSans"),
|
||||||
|
url(https://fonts.gstatic.com/s/opensans/v13/cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff)
|
||||||
|
format("woff");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local("Open Sans Bold"), local("OpenSans-Bold"),
|
||||||
|
url(https://fonts.gstatic.com/s/opensans/v13/k3k702ZOKiLJc3WVjuplzKRDOzjiPcYnFooOUGCOsRk.woff)
|
||||||
|
format("woff");
|
||||||
|
}
|
||||||
|
|
||||||
|
#colorPick * {
|
||||||
|
-webkit-transition: all linear 0.2s;
|
||||||
|
-moz-transition: all linear 0.2s;
|
||||||
|
-ms-transition: all linear 0.2s;
|
||||||
|
-o-transition: all linear 0.2s;
|
||||||
|
transition: all linear 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#colorPick {
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 15px;
|
||||||
|
font-family: "Open Sans", sans-serif;
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#colorPick span {
|
||||||
|
font-size: 9pt;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #bbb;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customColorHash {
|
||||||
|
border-radius: 5px;
|
||||||
|
height: 23px;
|
||||||
|
width: 122px;
|
||||||
|
margin: 1px 4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
border: 1px solid #babbba;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.customColorHash.error {
|
||||||
|
border-color: #ff424c;
|
||||||
|
color: #ff424c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPickButton {
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: 0px 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
border: thin solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPickButton:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPickDummy {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px dashed #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPickSelector {
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-transition: all linear .2s;
|
||||||
|
-moz-transition: all linear .2s;
|
||||||
|
-ms-transition: all linear .2s;
|
||||||
|
-o-transition: all linear .2s;
|
||||||
|
transition: all linear .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPickSelector:hover { transform: scale(1.1); }
|
||||||
|
|
26
server/static/css/colorPick.dark.theme.css
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/*!
|
||||||
|
*
|
||||||
|
* ColorPick jQuery plugin
|
||||||
|
* https://github.com/philzet/ColorPick.js
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017-2019 Phil Zet (a.k.a. Phil Zakharchenko)
|
||||||
|
* Licensed under the MIT License
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#colorPick {
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
#colorPick span {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customColorHash {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPickDummy {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
10
server/static/css/colorPick.min.css
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/*!
|
||||||
|
*
|
||||||
|
* ColorPick jQuery plugin
|
||||||
|
* https://github.com/philzet/ColorPick.js
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017-2019 Phil Zet (a.k.a. Phil Zakharchenko)
|
||||||
|
* Licensed under the MIT License
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;src:local('Open Sans'),local('OpenSans'),url(http://fonts.gstatic.com/s/opensans/v13/cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff) format('woff')}@font-face{font-family:'Open Sans';font-style:normal;font-weight:700;src:local('Open Sans Bold'),local('OpenSans-Bold'),url(http://fonts.gstatic.com/s/opensans/v13/k3k702ZOKiLJc3WVjuplzKRDOzjiPcYnFooOUGCOsRk.woff) format('woff')}#colorPick *{-webkit-transition:all linear .2s;-moz-transition:all linear .2s;-ms-transition:all linear .2s;-o-transition:all linear .2s;transition:all linear .2s}#colorPick{background:rgba(255,255,255,.85);-webkit-backdrop-filter:blur(15px);position:absolute;border-radius:5px;box-shadow:0 3px 8px rgba(0,0,0,.2);padding:15px;font-family:"Open Sans",sans-serif;width:140px}#colorPick span{font-size:9pt;text-transform:uppercase;font-weight:700;color:#bbb;margin-bottom:5px;display:block;clear:both}.customColorHash{border-radius:5px;height:23px;width:122px;margin:1px 4px;padding:0 4px;border:1px solid #babbba;outline:0}.customColorHash.error{border-color:#ff424c;color:#ff424c}.colorPickButton{border-radius:5px;width:20px;height:20px;margin:0 3px;cursor:pointer;display:inline-block;border:thin solid #eee}.colorPickButton:hover{transform:scale(1.1)}.colorPickDummy{background:#fff;border:1px dashed #bbb}
|
368
server/static/css/main.css
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
|
||||||
|
html,
|
||||||
|
body
|
||||||
|
{
|
||||||
|
font-family: "Trebuchet MS", Helvetica, sans-serif;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
height: calc(100% - 0.5rem)
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
min-height:100%;
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
#main {
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
padding-top: 3rem;
|
||||||
|
padding: 30px 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
display:flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 5rem;
|
||||||
|
align-items:center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
margin: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primeblue {
|
||||||
|
color: #1b75bc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whiteblock {
|
||||||
|
display: flex;
|
||||||
|
background-color: #fff;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 10rem;
|
||||||
|
margin-right: 10rem;
|
||||||
|
}
|
||||||
|
.grayblock {
|
||||||
|
background-color: #ddd;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grayblock .padded {
|
||||||
|
padding: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.vertical {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.vertical .content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 2rem
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical .content .image {
|
||||||
|
width: 25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical .content .text {
|
||||||
|
margin-left: 2rem;
|
||||||
|
width: 26rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal .content {
|
||||||
|
font-size: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal .image {
|
||||||
|
margin: 1rem;
|
||||||
|
margin-left: 4rem;
|
||||||
|
margin-right: 4rem;
|
||||||
|
height: 20rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logins {
|
||||||
|
height: 100px;
|
||||||
|
width: 500px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login_google {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login_email {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* bot navigation */
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background-color: #d8d8d8;
|
||||||
|
display: flex;
|
||||||
|
justify-content:center;
|
||||||
|
align-items:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer p {
|
||||||
|
margin: 0px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
float: left;
|
||||||
|
color: #424242;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
.footer a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #085a87;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* top navigation */
|
||||||
|
.navigation {
|
||||||
|
background-color: #eaeaea;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation_rightside {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation a {
|
||||||
|
float: left;
|
||||||
|
display: flex;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
padding: 14px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation a:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add an active class to highlight the current page */
|
||||||
|
.navigation a.active {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the link that should open and close the navigation on small screens */
|
||||||
|
.navigation .icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When the screen is less than 600 pixels wide, hide all links, except for the first one ("Home"). Show the link that contains should open and close the navigation (.icon) */
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.navigation a:not(:first-child) {display: none;}
|
||||||
|
.navigation a.icon {
|
||||||
|
float: right;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The "responsive" class is added to the navigation with JavaScript when the user clicks on the icon. This class makes the navigation look good on small screens (display the links vertically instead of horizontally) */
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.navigation.responsive {position: relative;}
|
||||||
|
.navigation.responsive a.icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.navigation.responsive a {
|
||||||
|
float: none;
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style page content */
|
||||||
|
.main {
|
||||||
|
padding: 30px 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The switch - the box around the slider */
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 38px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide default HTML checkbox */
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The slider */
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ddd;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.on {
|
||||||
|
background-color: #2196F3;
|
||||||
|
}
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
right: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider.on{
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus + .slider.on {
|
||||||
|
box-shadow: 0 0 1px #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider.on:before {
|
||||||
|
-webkit-transform: translateX(-13px);
|
||||||
|
-ms-transform: translateX(-13px);
|
||||||
|
transform: translateX(-13px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rounded sliders */
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items:center;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 0px 2rem 0px 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .button {
|
||||||
|
padding: 1rem 1.5rem 1rem 1.5rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 4rem;
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .preview {
|
||||||
|
width: 20rem;
|
||||||
|
height: 20rem;
|
||||||
|
margin: 1rem 3rem 4rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .button.logout {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .button.delete {
|
||||||
|
background-color: #b51409;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .button.adddevice {
|
||||||
|
background-color: #ddd;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.container .button.addcalendar {
|
||||||
|
background-color: #ddd;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub.container {
|
||||||
|
width: 20rem;
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items:center;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .picture {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .name {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grey {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub.container .name {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.sub.container .data {
|
||||||
|
background: #ddd;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
163
server/static/js/colorPick.js
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
/*!
|
||||||
|
*
|
||||||
|
* ColorPick jQuery plugin
|
||||||
|
* https://github.com/philzet/ColorPick.js
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017-2019 Phil Zet (a.k.a. Phil Zakharchenko)
|
||||||
|
* Licensed under the MIT License
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
(function( $ ) {
|
||||||
|
|
||||||
|
$.fn.colorPick = function(config) {
|
||||||
|
|
||||||
|
return this.each(function() {
|
||||||
|
new $.colorPick(this, config || {});
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
$.colorPick = function (element, options) {
|
||||||
|
options = options || {};
|
||||||
|
this.options = $.extend({}, $.fn.colorPick.defaults, options);
|
||||||
|
if(options.str) {
|
||||||
|
this.options.str = $.extend({}, $.fn.colorPick.defaults.str, options.str);
|
||||||
|
}
|
||||||
|
$.fn.colorPick.defaults = this.options;
|
||||||
|
this.color = this.options.initialColor.toUpperCase();
|
||||||
|
this.element = $(element);
|
||||||
|
|
||||||
|
var dataInitialColor = this.element.data('initialcolor');
|
||||||
|
if (dataInitialColor) {
|
||||||
|
this.color = dataInitialColor;
|
||||||
|
this.appendToStorage(this.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
var uniquePalette = [];
|
||||||
|
$.each($.fn.colorPick.defaults.palette.map(function(x){ return x.toUpperCase() }), function(i, el){
|
||||||
|
if($.inArray(el, uniquePalette) === -1) uniquePalette.push(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.palette = uniquePalette;
|
||||||
|
|
||||||
|
return this.element.hasClass(this.options.pickrclass) ? this : this.init();
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.colorPick.defaults = {
|
||||||
|
'initialColor': '#3498db',
|
||||||
|
'paletteLabel': 'Default palette:',
|
||||||
|
'allowRecent': true,
|
||||||
|
'recentMax': 5,
|
||||||
|
'allowCustomColor': false,
|
||||||
|
'palette': ["#1abc9c", "#16a085", "#2ecc71", "#27ae60", "#3498db", "#2980b9", "#9b59b6", "#8e44ad", "#34495e", "#2c3e50", "#f1c40f", "#f39c12", "#e67e22", "#d35400", "#e74c3c", "#c0392b", "#ecf0f1", "#bdc3c7", "#95a5a6", "#7f8c8d"],
|
||||||
|
'onColorSelected': function() {
|
||||||
|
this.element.css({'backgroundColor': this.color, 'color': this.color});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.colorPick.prototype = {
|
||||||
|
|
||||||
|
init : function(){
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var o = this.options;
|
||||||
|
|
||||||
|
$.proxy($.fn.colorPick.defaults.onColorSelected, this)();
|
||||||
|
|
||||||
|
this.element.click(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
self.show(event.pageX, event.pageY);
|
||||||
|
|
||||||
|
$('.customColorHash').val(self.color);
|
||||||
|
|
||||||
|
$('.colorPickButton').click(function(event) {
|
||||||
|
self.color = $(event.target).attr('hexValue');
|
||||||
|
self.appendToStorage($(event.target).attr('hexValue'));
|
||||||
|
self.hide();
|
||||||
|
$.proxy(self.options.onColorSelected, self)();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
$('.customColorHash').click(function(event) {
|
||||||
|
return false;
|
||||||
|
}).keyup(function (event) {
|
||||||
|
var hash = $(this).val();
|
||||||
|
if (hash.indexOf('#') !== 0) {
|
||||||
|
hash = "#"+hash;
|
||||||
|
}
|
||||||
|
if (/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hash)) {
|
||||||
|
self.color = hash;
|
||||||
|
self.appendToStorage(hash);
|
||||||
|
$.proxy(self.options.onColorSelected, self)();
|
||||||
|
$(this).removeClass('error');
|
||||||
|
} else {
|
||||||
|
$(this).addClass('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}).blur(function() {
|
||||||
|
self.element.val(self.color);
|
||||||
|
$.proxy(self.options.onColorSelected, self)();
|
||||||
|
self.hide();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', function(event) {
|
||||||
|
self.hide();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
appendToStorage: function(color) {
|
||||||
|
if ($.fn.colorPick.defaults.allowRecent === true) {
|
||||||
|
var storedColors = JSON.parse(localStorage.getItem("colorPickRecentItems"));
|
||||||
|
if (storedColors == null) {
|
||||||
|
storedColors = [];
|
||||||
|
}
|
||||||
|
if ($.inArray(color, storedColors) == -1) {
|
||||||
|
storedColors.unshift(color);
|
||||||
|
storedColors = storedColors.slice(0, $.fn.colorPick.defaults.recentMax)
|
||||||
|
localStorage.setItem("colorPickRecentItems", JSON.stringify(storedColors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
show: function(left, top) {
|
||||||
|
|
||||||
|
$("#colorPick").remove();
|
||||||
|
|
||||||
|
$("body").append('<div id="colorPick" style="display:none;top:' + top + 'px;left:' + left + 'px"><span>'+$.fn.colorPick.defaults.paletteLabel+'</span></div>');
|
||||||
|
jQuery.each(this.palette, function (index, item) {
|
||||||
|
$("#colorPick").append('<div class="colorPickButton" hexValue="' + item + '" style="background:' + item + '"></div>');
|
||||||
|
});
|
||||||
|
if ($.fn.colorPick.defaults.allowCustomColor === true) {
|
||||||
|
$("#colorPick").append('<input type="text" style="margin-top:5px" class="customColorHash" />');
|
||||||
|
}
|
||||||
|
if ($.fn.colorPick.defaults.allowRecent === true) {
|
||||||
|
$("#colorPick").append('<span style="margin-top:5px">Recent:</span>');
|
||||||
|
if (JSON.parse(localStorage.getItem("colorPickRecentItems")) == null || JSON.parse(localStorage.getItem("colorPickRecentItems")) == []) {
|
||||||
|
$("#colorPick").append('<div class="colorPickButton colorPickDummy"></div>');
|
||||||
|
} else {
|
||||||
|
jQuery.each(JSON.parse(localStorage.getItem("colorPickRecentItems")), function (index, item) {
|
||||||
|
$("#colorPick").append('<div class="colorPickButton" hexValue="' + item + '" style="background:' + item + '"></div>');
|
||||||
|
if (index == $.fn.colorPick.defaults.recentMax-1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$("#colorPick").fadeIn(200);
|
||||||
|
},
|
||||||
|
|
||||||
|
hide: function() {
|
||||||
|
$( "#colorPick" ).fadeOut(200, function() {
|
||||||
|
$("#colorPick").remove();
|
||||||
|
return this;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}( jQuery ));
|
10
server/static/js/colorPick.min.js
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/*!
|
||||||
|
*
|
||||||
|
* ColorPick jQuery plugin
|
||||||
|
* https://github.com/philzet/ColorPick.js
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017-2019 Phil Zet (a.k.a. Phil Zakharchenko)
|
||||||
|
* Licensed under the MIT License
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
!function(o){o.fn.colorPick=function(e){return this.each(function(){new o.colorPick(this,e||{})})},o.colorPick=function(e,t){t=t||{},this.options=o.extend({},o.fn.colorPick.defaults,t),t.str&&(this.options.str=o.extend({},o.fn.colorPick.defaults.str,t.str)),o.fn.colorPick.defaults=this.options,this.color=this.options.initialColor.toUpperCase(),this.element=o(e);var c=this.element.data("initialcolor");c&&(this.color=c,this.appendToStorage(this.color));var l=[];return o.each(o.fn.colorPick.defaults.palette.map(function(o){return o.toUpperCase()}),function(e,t){-1===o.inArray(t,l)&&l.push(t)}),this.palette=l,this.element.hasClass(this.options.pickrclass)?this:this.init()},o.fn.colorPick.defaults={initialColor:"#3498db",paletteLabel:"Default palette:",allowRecent:!0,recentMax:5,allowCustomColor:!1,palette:["#1abc9c","#16a085","#2ecc71","#27ae60","#3498db","#2980b9","#9b59b6","#8e44ad","#34495e","#2c3e50","#f1c40f","#f39c12","#e67e22","#d35400","#e74c3c","#c0392b","#ecf0f1","#bdc3c7","#95a5a6","#7f8c8d"],onColorSelected:function(){this.element.css({backgroundColor:this.color,color:this.color})}},o.colorPick.prototype={init:function(){var e=this;this.options;return o.proxy(o.fn.colorPick.defaults.onColorSelected,this)(),this.element.click(function(t){return t.preventDefault(),e.show(t.pageX,t.pageY),o(".customColorHash").val(e.color),o(".colorPickButton").click(function(t){return e.color=o(t.target).attr("hexValue"),e.appendToStorage(o(t.target).attr("hexValue")),e.hide(),o.proxy(e.options.onColorSelected,e)(),!1}),o(".customColorHash").click(function(o){return!1}).keyup(function(t){var c=o(this).val();0!==c.indexOf("#")&&(c="#"+c),/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(c)?(e.color=c,e.appendToStorage(c),o.proxy(e.options.onColorSelected,e)(),o(this).removeClass("error")):o(this).addClass("error")}),!1}).blur(function(){return e.element.val(e.color),o.proxy(e.options.onColorSelected,e)(),e.hide(),!1}),o(document).on("click",function(o){return e.hide(),!0}),this},appendToStorage:function(e){if(!0===o.fn.colorPick.defaults.allowRecent){var t=JSON.parse(localStorage.getItem("colorPickRecentItems"));null==t&&(t=[]),-1==o.inArray(e,t)&&(t.unshift(e),t=t.slice(0,o.fn.colorPick.defaults.recentMax),localStorage.setItem("colorPickRecentItems",JSON.stringify(t)))}},show:function(e,t){o("#colorPick").remove(),o("body").append('<div id="colorPick" style="display:none;top:'+t+"px;left:"+e+'px"><span>'+o.fn.colorPick.defaults.paletteLabel+"</span></div>"),jQuery.each(this.palette,function(e,t){o("#colorPick").append('<div class="colorPickButton" hexValue="'+t+'" style="background:'+t+'"></div>')}),!0===o.fn.colorPick.defaults.allowCustomColor&&o("#colorPick").append('<input type="text" style="margin-top:5px" class="customColorHash" />'),!0===o.fn.colorPick.defaults.allowRecent&&(o("#colorPick").append('<span style="margin-top:5px">Recent:</span>'),null==JSON.parse(localStorage.getItem("colorPickRecentItems"))||JSON.parse(localStorage.getItem("colorPickRecentItems"))==[]?o("#colorPick").append('<div class="colorPickButton colorPickDummy"></div>'):jQuery.each(JSON.parse(localStorage.getItem("colorPickRecentItems")),function(e,t){if(o("#colorPick").append('<div class="colorPickButton" hexValue="'+t+'" style="background:'+t+'"></div>'),e==o.fn.colorPick.defaults.recentMax-1)return!1})),o("#colorPick").fadeIn(200)},hide:function(){o("#colorPick").fadeOut(200,function(){return o("#colorPick").remove(),this})}}}(jQuery);
|
9
server/static/js/index.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/* Toggle between adding and removing the "responsive" class to navigation when the user clicks on the icon */
|
||||||
|
function menuBars() {
|
||||||
|
var x = document.getElementById("navigation");
|
||||||
|
if (x.className === "navigation") {
|
||||||
|
x.className += " responsive";
|
||||||
|
} else {
|
||||||
|
x.className = "navigation";
|
||||||
|
}
|
||||||
|
}
|
2
server/static/js/jquery-3.5.0.min.js
vendored
Normal file
12
server/static/res/arrow.svg
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.88073 91.52294">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<title>Artboard 1</title>
|
||||||
|
<path class="cls-1" d="M190.17035,42.08149,122.7379,3.14935a2.52251,2.52251,0,0,0-3.78377,2.18456V83.1982a2.52251,2.52251,0,0,0,3.78377,2.18456l67.43245-38.93214A2.52252,2.52252,0,0,0,190.17035,42.08149Z"/>
|
||||||
|
<path class="cls-1" d="M118.93578,66.72477H4.29316a2.52252,2.52252,0,0,1-2.52252-2.52252V23.66931a2.52252,2.52252,0,0,1,2.52252-2.52252H118.93578Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 602 B |
102
server/static/res/calendar.svg
Executable file
@ -0,0 +1,102 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201.61468 195.55963">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #808285;
|
||||||
|
stroke: #a7a9ac;
|
||||||
|
stroke-width: 0.25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-10, .cls-11, .cls-12, .cls-2, .cls-5, .cls-6, .cls-7, .cls-8, .cls-9 {
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-10, .cls-3 {
|
||||||
|
fill: #939598;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4, .cls-6 {
|
||||||
|
fill: #1b75bc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #92278f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-10, .cls-11, .cls-12, .cls-5, .cls-6, .cls-7, .cls-8, .cls-9 {
|
||||||
|
stroke: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-7 {
|
||||||
|
fill: #39b54a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-8 {
|
||||||
|
fill: #27aae1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-9 {
|
||||||
|
fill: #ee2a7b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-11 {
|
||||||
|
fill: #f7941d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-12 {
|
||||||
|
fill: #ffcd34;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<title>Artboard 1</title>
|
||||||
|
<g id="background">
|
||||||
|
<rect class="cls-1" x="0.2844" y="0.22936" width="201.13761" height="195.19266" rx="12.7156"/>
|
||||||
|
</g>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<line class="cls-2" x1="18.11927" y1="81.24771" x2="193.16514" y2="81.24771"/>
|
||||||
|
<line class="cls-2" x1="18.11927" y1="116.24771" x2="193.16514" y2="116.24771"/>
|
||||||
|
<line class="cls-2" x1="18.11927" y1="151.24771" x2="193.16514" y2="151.24771"/>
|
||||||
|
<circle class="cls-3" cx="45.87156" cy="16.41284" r="9.57798"/>
|
||||||
|
<circle class="cls-4" cx="105.87156" cy="16.41284" r="9.57798"/>
|
||||||
|
<circle class="cls-3" cx="165.87156" cy="16.41284" r="9.57798"/>
|
||||||
|
<rect class="cls-4" x="78.37156" y="31.07339" width="55" height="8" rx="2"/>
|
||||||
|
<rect class="cls-5" x="78.37156" y="46.92661" width="55" height="39.61468" rx="2"/>
|
||||||
|
<rect class="cls-6" x="78.37156" y="91.77982" width="55" height="8" rx="2"/>
|
||||||
|
<rect class="cls-7" x="78.37156" y="111.6422" width="55" height="21.62385" rx="2"/>
|
||||||
|
<rect class="cls-8" x="78.37156" y="135.63303" width="55" height="24.90826" rx="2"/>
|
||||||
|
<rect class="cls-9" x="78.37156" y="164.55963" width="55" height="8" rx="2"/>
|
||||||
|
<rect class="cls-5" x="18.37156" y="46.92661" width="55" height="39.17431" rx="2"/>
|
||||||
|
<rect class="cls-6" x="18.37156" y="91.77982" width="55" height="8" rx="2"/>
|
||||||
|
<rect class="cls-8" x="18.37156" y="117.20183" width="55" height="21.08257" rx="2"/>
|
||||||
|
<rect class="cls-8" x="18.37156" y="139.93578" width="55" height="16.6789" rx="2"/>
|
||||||
|
<rect class="cls-9" x="18.37156" y="107.19266" width="55" height="8" rx="2"/>
|
||||||
|
<rect class="cls-5" x="138.37156" y="46.92661" width="55" height="49.30275" rx="2"/>
|
||||||
|
<rect class="cls-10" x="138.37156" y="117.36697" width="55" height="8" rx="2"/>
|
||||||
|
<rect class="cls-11" x="138.37156" y="128.77982" width="55" height="21.6422" rx="2"/>
|
||||||
|
<rect class="cls-12" x="138.37156" y="177.30275" width="55" height="9.77982" rx="2"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M7.9614,46.45853c0,1.32519-.4917,2.05664-1.355,2.05664-.76172,0-1.27734-.71338-1.28955-2.00293,0-1.30713.564-2.02686,1.35547-2.02686C7.49363,44.48538,7.9614,45.21683,7.9614,46.45853Zm-2.1167.05957c0,1.01367.31153,1.58935.79151,1.58935.53955,0,.79736-.62988.79736-1.625,0-.95947-.2456-1.58935-.7915-1.58935C6.18064,44.8931,5.8447,45.45706,5.8447,46.5181Z"/>
|
||||||
|
<path class="cls-3" d="M11.03953,46.45853c0,1.32519-.4917,2.05664-1.355,2.05664-.76172,0-1.27735-.71338-1.28955-2.00293,0-1.30713.564-2.02686,1.35547-2.02686C10.57175,44.48538,11.03953,45.21683,11.03953,46.45853Zm-2.1167.05957c0,1.01367.31152,1.58935.7915,1.58935.53955,0,.79737-.62988.79737-1.625,0-.95947-.24561-1.58935-.79151-1.58935C9.25877,44.8931,8.92283,45.45706,8.92283,46.5181Z"/>
|
||||||
|
<path class="cls-3" d="M12.92381,45.29105c0,.77246-.28663,1.19873-.79,1.19873-.44385,0-.74414-.416-.751-1.167,0-.76221.32813-1.18116.78955-1.18116C12.65086,44.14163,12.92381,44.5679,12.92381,45.29105Zm-1.23389.03515c0,.59034.18213.92578.46142.92578.31446,0,.46485-.36669.46485-.94677,0-.55908-.14356-.92627-.46143-.92627C11.88572,44.37894,11.68992,44.70755,11.68992,45.3262Z"/>
|
||||||
|
<path class="cls-3" d="M14.71824,45.29105c0,.77246-.28662,1.19873-.79,1.19873-.44385,0-.74414-.416-.751-1.167,0-.76221.32813-1.18116.78955-1.18116C14.44529,44.14163,14.71824,44.5679,14.71824,45.29105Zm-1.23389.03515c0,.59034.18164.92578.46143.92578.31445,0,.46484-.36669.46484-.94677,0-.55908-.14355-.92627-.46142-.92627C13.68015,44.37894,13.48435,44.70755,13.48435,45.3262Z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M6.51609,115.043H6.50437l-.67773.36572-.10205-.40136.85156-.45606h.44971v3.898H6.51609Z"/>
|
||||||
|
<path class="cls-3" d="M8.4492,118.44925v-.32373l.41357-.40186c.99561-.94726,1.44531-1.45117,1.45117-2.03906a.69522.69522,0,0,0-.77344-.76123,1.32667,1.32667,0,0,0-.82763.32959l-.168-.37158a1.7104,1.7104,0,0,1,1.10352-.396,1.09685,1.09685,0,0,1,1.19335,1.1333c0,.71973-.522,1.30127-1.34326,2.09278l-.312.28808v.01172h1.751v.438Z"/>
|
||||||
|
<path class="cls-3" d="M12.92381,115.29105c0,.77246-.28663,1.19873-.79,1.19873-.44385,0-.74414-.416-.751-1.167,0-.76221.32813-1.18116.78955-1.18116C12.65086,114.14163,12.92381,114.5679,12.92381,115.29105Zm-1.23389.03515c0,.59034.18213.92578.46142.92578.31446,0,.46485-.36669.46485-.94677,0-.55908-.14356-.92627-.46143-.92627C11.88572,114.37894,11.68992,114.70755,11.68992,115.3262Z"/>
|
||||||
|
<path class="cls-3" d="M14.71824,115.29105c0,.77246-.28662,1.19873-.79,1.19873-.44385,0-.74414-.416-.751-1.167,0-.76221.32813-1.18116.78955-1.18116C14.44529,114.14163,14.71824,114.5679,14.71824,115.29105Zm-1.23389.03515c0,.59034.18164.92578.46143.92578.31445,0,.46484-.36669.46484-.94677,0-.55908-.14355-.92627-.46142-.92627C13.68015,114.37894,13.48435,114.70755,13.48435,115.3262Z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M5.37107,188.44925v-.32373l.41358-.40186c.9956-.94726,1.44531-1.45117,1.45117-2.03906a.69522.69522,0,0,0-.77344-.76123,1.3267,1.3267,0,0,0-.82764.32959l-.168-.37158a1.7104,1.7104,0,0,1,1.10352-.396,1.09685,1.09685,0,0,1,1.19336,1.1333c0,.71973-.522,1.30127-1.34326,2.09278l-.312.28808v.01172h1.751v.438Z"/>
|
||||||
|
<path class="cls-3" d="M10.08006,188.44925v-1.06152H8.269v-.34766l1.73926-2.48877h.56982v2.42285h.54541v.41358H10.5781v1.06152Zm0-1.4751v-1.30127q0-.30615.01806-.61181h-.01806c-.11963.228-.21582.396-.32373.57568l-.95362,1.3252v.0122Z"/>
|
||||||
|
<path class="cls-3" d="M12.92381,185.29105c0,.77246-.28663,1.19873-.79,1.19873-.44385,0-.74414-.416-.751-1.167,0-.76221.32813-1.18116.78955-1.18116C12.65086,184.14163,12.92381,184.5679,12.92381,185.29105Zm-1.23389.03515c0,.59034.18213.92578.46142.92578.31446,0,.46485-.36669.46485-.94677,0-.55908-.14356-.92627-.46143-.92627C11.88572,184.37894,11.68992,184.70755,11.68992,185.3262Z"/>
|
||||||
|
<path class="cls-3" d="M14.71824,185.29105c0,.77246-.28662,1.19873-.79,1.19873-.44385,0-.74414-.416-.751-1.167,0-.76221.32813-1.18116.78955-1.18116C14.44529,184.14163,14.71824,184.5679,14.71824,185.29105Zm-1.23389.03515c0,.59034.18164.92578.46143.92578.31445,0,.46484-.36669.46484-.94677,0-.55908-.14355-.92627-.46142-.92627C13.68015,184.37894,13.48435,184.70755,13.48435,185.3262Z"/>
|
||||||
|
</g>
|
||||||
|
<line class="cls-2" x1="18.11927" y1="46.24771" x2="193.16514" y2="46.24771"/>
|
||||||
|
<line class="cls-2" x1="18.11927" y1="186.24771" x2="137.6789" y2="186.24771"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 7.1 KiB |
70
server/static/res/connect_calendar.svg
Executable file
@ -0,0 +1,70 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 25">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #27aae1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3, .cls-4 {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 1.5615px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
stroke: #939598;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3, .cls-4, .cls-6 {
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
stroke: #a7a9ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #f7941d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: #00aeef;
|
||||||
|
stroke: #fff;
|
||||||
|
stroke-width: 0.819px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<title>Artboard 1</title>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M12.377,6.70433V20.036a.60366.60366,0,0,0,.60366.60367H24.69291a.60365.60365,0,0,0,.42685-.17681l2.08043-2.08043a.60366.60366,0,0,0,.17681-.42832L27.34962,6.70269a.60368.60368,0,0,0-.60367-.6022l-13.76529.00017A.60366.60366,0,0,0,12.377,6.70433Z"/>
|
||||||
|
<circle class="cls-2" cx="15.4964" cy="8.97936" r="1.01362"/>
|
||||||
|
<circle class="cls-2" cx="24.36129" cy="8.97936" r="1.01362"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M15.28135,18.48v-.02979c0-.81982.69971-1.83935,2.00879-2.8584,1.13965-.88965,1.4292-1.15967,1.4292-1.98926a1.23962,1.23962,0,0,0-1.34912-1.33935c-.89942,0-1.36914.43017-1.53907,1.38965h-.50976a1.94123,1.94123,0,0,1,2.04883-1.86914,1.74262,1.74262,0,0,1,1.89892,1.78906c0,1.01953-.42969,1.35937-1.64892,2.3584a4.96814,4.96814,0,0,0-1.75928,2.05908h3.67822l-.08984.48975Z"/>
|
||||||
|
<path class="cls-2" d="M20.38486,16.64111a1.814,1.814,0,0,1,1.26954-1.75928,1.4784,1.4784,0,0,1-.9795-1.40918,1.68665,1.68665,0,0,1,1.91895-1.689,1.66465,1.66465,0,0,1,1.82861,1.6792,1.43291,1.43291,0,0,1-.96924,1.39892,1.77374,1.77374,0,0,1,1.23926,1.75928,1.98384,1.98384,0,0,1-2.17871,1.979A1.96437,1.96437,0,0,1,20.38486,16.64111Zm3.76807-.02a1.40857,1.40857,0,0,0-1.60937-1.48926,1.42461,1.42461,0,0,0-1.61915,1.48926,1.47113,1.47113,0,0,0,1.60938,1.50928A1.45349,1.45349,0,0,0,24.15293,16.62109Zm-2.93848-3.17822c0,.62939.37989,1.209,1.36914,1.209.91944,0,1.29932-.47949,1.29932-1.16894a1.18858,1.18858,0,0,0-1.31933-1.2295C21.57432,12.25341,21.21445,12.77294,21.21445,13.44287Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path class="cls-3" d="M44.24948,8.87635l1.41044.0142a3.896,3.896,0,0,1,3.85656,3.935h0a3.635,3.635,0,0,1-3.67142,3.59822l-1.52229-.01533"/>
|
||||||
|
<path class="cls-3" d="M44.24948,8.87635l1.41044.0142a3.896,3.896,0,0,1,3.85656,3.935h0a3.635,3.635,0,0,1-3.67142,3.59822l-1.52229-.01533"/>
|
||||||
|
<path class="cls-3" d="M45.26721,16.418l-1.41044-.0142a3.896,3.896,0,0,1-3.85657-3.935h0a3.635,3.635,0,0,1,3.67143-3.59822l1.52228.01533"/>
|
||||||
|
<path class="cls-3" d="M49.51648,12.82557h0a3.635,3.635,0,0,1-3.67142,3.59822l-1.52229-.01533"/>
|
||||||
|
<path class="cls-3" d="M40.0002,12.46875a3.635,3.635,0,0,1,3.67143-3.59822l1.52228.01533"/>
|
||||||
|
<path class="cls-4" d="M44.24948,8.87635l1.41044.0142a3.896,3.896,0,0,1,3.85656,3.935h0a3.635,3.635,0,0,1-3.67142,3.59822l-1.52229-.01533"/>
|
||||||
|
<path class="cls-4" d="M45.26721,16.418l-1.41044-.0142a3.896,3.896,0,0,1-3.85657-3.935h0a3.635,3.635,0,0,1,3.67143-3.59822l1.52228.01533"/>
|
||||||
|
<path class="cls-3" d="M50.75052,16.418l-1.41044-.0142a3.896,3.896,0,0,1-3.85656-3.935h0a3.635,3.635,0,0,1,3.67142-3.59822l1.52229.01533"/>
|
||||||
|
<path class="cls-3" d="M49.73279,8.87635l1.41044.0142a3.896,3.896,0,0,1,3.85657,3.935h0a3.635,3.635,0,0,1-3.67143,3.59822l-1.52228-.01533"/>
|
||||||
|
<path class="cls-3" d="M49.73279,8.87635l1.41044.0142a3.896,3.896,0,0,1,3.85657,3.935h0a3.635,3.635,0,0,1-3.67143,3.59822l-1.52228-.01533"/>
|
||||||
|
<path class="cls-3" d="M50.75052,16.418l-1.41044-.0142a3.896,3.896,0,0,1-3.85656-3.935h0a3.635,3.635,0,0,1,3.67142-3.59822l1.52229.01533"/>
|
||||||
|
<path class="cls-3" d="M54.9998,12.82557h0a3.635,3.635,0,0,1-3.67143,3.59822l-1.52228-.01533"/>
|
||||||
|
<path class="cls-3" d="M45.48352,12.46875a3.635,3.635,0,0,1,3.67142-3.59822l1.52229.01533"/>
|
||||||
|
<path class="cls-3" d="M49.73279,8.87635l1.41044.0142a3.896,3.896,0,0,1,3.85657,3.935h0a3.635,3.635,0,0,1-3.67143,3.59822l-1.52228-.01533"/>
|
||||||
|
<path class="cls-3" d="M45.48352,12.46875a3.635,3.635,0,0,1,3.67142-3.59822l1.52229.01533"/>
|
||||||
|
<g>
|
||||||
|
<ellipse class="cls-5" cx="72.04133" cy="12.8295" rx="6.98436" ry="6.85133"/>
|
||||||
|
<ellipse class="cls-6" cx="67.30085" cy="18.57004" rx="3.27515" ry="3.21277"/>
|
||||||
|
</g>
|
||||||
|
<path class="cls-4" d="M49.51648,12.82557h0a3.635,3.635,0,0,1-3.67142,3.59822l-1.52229-.01533"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.3 KiB |
133
server/static/res/connect_device.svg
Executable file
@ -0,0 +1,133 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 25">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #fbb040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #00aeef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #f15a29;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #231f20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-10, .cls-6, .cls-9 {
|
||||||
|
fill: none;
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
stroke: #231f20;
|
||||||
|
stroke-width: 0.09565px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-7 {
|
||||||
|
fill: #808285;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-8 {
|
||||||
|
fill: #f1f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-9 {
|
||||||
|
stroke: #939598;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-10, .cls-9 {
|
||||||
|
stroke-width: 1.5615px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-10 {
|
||||||
|
stroke: #a7a9ac;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<title>Artboard 1</title>
|
||||||
|
<g>
|
||||||
|
<circle class="cls-1" cx="17.76735" cy="12.5" r="12.76748"/>
|
||||||
|
<circle class="cls-2" cx="21.94283" cy="7.24563" r="1.83373"/>
|
||||||
|
<circle class="cls-3" cx="19.90843" cy="17.18926" r="0.94108"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-4" d="M6.28254,11.94644a2.72215,2.72215,0,0,1,.40039-.03027.838.838,0,0,1,.5918.18213.673.673,0,0,1,.20605.52441.77117.77117,0,0,1-.21094.56738.89642.89642,0,0,1-.6455.21045,3.04493,3.04493,0,0,1-.3418-.01758ZM6.471,13.238a1.16551,1.16551,0,0,0,.19043.01074.56127.56127,0,0,0,.62207-.61963.52132.52132,0,0,0-.5918-.56348,1.07246,1.07246,0,0,0-.2207.01953Z"/>
|
||||||
|
<path class="cls-4" d="M8.29524,13.38736,8.27961,13.255H8.27277a.38979.38979,0,0,1-.32031.15625A.29868.29868,0,0,1,7.63215,13.11c0-.25342.22461-.39209.62988-.39014v-.02149a.21588.21588,0,0,0-.23828-.24267.5174.5174,0,0,0-.27246.07812l-.04395-.126a.65483.65483,0,0,1,.34473-.09326c.32031,0,.39844.21923.39844.42919v.39209a1.50378,1.50378,0,0,0,.01758.25147Zm-.02832-.53516c-.208-.00439-.44434.03272-.44434.23633a.16823.16823,0,0,0,.17969.18164.261.261,0,0,0,.2539-.17529.21954.21954,0,0,0,.01075-.06055Z"/>
|
||||||
|
<path class="cls-4" d="M8.7552,12.66568c0-.12354-.002-.22949-.00879-.32715h.167l.00684.20606h.00781a.31545.31545,0,0,1,.291-.23.202.202,0,0,1,.05371.00683v.17969a.28207.28207,0,0,0-.06445-.00635.26661.26661,0,0,0-.25586.24463.48486.48486,0,0,0-.00879.08887v.55908H8.7552Z"/>
|
||||||
|
<path class="cls-4" d="M9.65852,12.81949h.0039c.02637-.03662.06348-.082.09375-.11914l.30762-.36182h.22949l-.40527.43115.46191.61768H10.1175l-.36133-.50244-.09765.10791v.39453H9.47V11.84879h.18848Z"/>
|
||||||
|
<path class="cls-4" d="M10.90363,12.73062v.14111h-.53515v-.14111Z"/>
|
||||||
|
<path class="cls-4" d="M12.182,13.33951a.93578.93578,0,0,1-.38574.06933.68222.68222,0,0,1-.72168-.73877.72673.72673,0,0,1,.7627-.76709.78612.78612,0,0,1,.34668.065l-.0459.15429a.67575.67575,0,0,0-.29395-.061.54315.54315,0,0,0-.57031.60254.53087.53087,0,0,0,.56152.58691.753.753,0,0,0,.30762-.06055Z"/>
|
||||||
|
<path class="cls-4" d="M13.33234,12.85416a.51258.51258,0,0,1-.52246.55713.4986.4986,0,0,1-.50293-.53956.51228.51228,0,0,1,.52051-.55712A.49707.49707,0,0,1,13.33234,12.85416Zm-.832.01123c0,.22949.13184.40283.31836.40283.18164,0,.31836-.17139.31836-.40723,0-.17773-.08886-.40332-.31445-.40332C12.598,12.45767,12.50031,12.66568,12.50031,12.86539Z"/>
|
||||||
|
<path class="cls-4" d="M13.56867,12.62271c0-.1084-.00195-.19726-.00879-.28418h.167l.00879.16895h.00585a.36233.36233,0,0,1,.33008-.19287.311.311,0,0,1,.29688.21044h.0039a.43586.43586,0,0,1,.11719-.13671.35793.35793,0,0,1,.23145-.07373c.13867,0,.34472.0913.34472.45507v.61768h-.18652v-.59375c0-.20166-.07324-.32275-.22754-.32275a.24546.24546,0,0,0-.22461.17334.30819.30819,0,0,0-.01562.09521v.64795h-.18653v-.62842c0-.167-.07324-.28808-.21875-.28808a.26063.26063,0,0,0-.23633.19043.26687.26687,0,0,0-.01464.09326v.63281h-.18653Z"/>
|
||||||
|
<path class="cls-4" d="M15.37043,12.68082c0-.13428-.00391-.24268-.00879-.34229h.1709l.00879.18018h.00488a.41189.41189,0,0,1,.37207-.2041.47521.47521,0,0,1,.44434.5332.49637.49637,0,0,1-.47656.56348.36913.36913,0,0,1-.32325-.16456h-.0039v.56983h-.18848Zm.18848.27978a.38972.38972,0,0,0,.00879.07813.29409.29409,0,0,0,.28613.22314c.20117,0,.31836-.165.31836-.40527,0-.21045-.11035-.39014-.3125-.39014a.30459.30459,0,0,0-.28809.23633.33934.33934,0,0,0-.01269.07764Z"/>
|
||||||
|
<path class="cls-4" d="M17.54914,12.85416a.51258.51258,0,0,1-.52246.55713.4986.4986,0,0,1-.50293-.53956.51227.51227,0,0,1,.52051-.55712A.49707.49707,0,0,1,17.54914,12.85416Zm-.832.01123c0,.22949.13184.40283.31836.40283.18164,0,.31836-.17139.31836-.40723,0-.17773-.08887-.40332-.31445-.40332C16.81477,12.45767,16.71711,12.66568,16.71711,12.86539Z"/>
|
||||||
|
<path class="cls-4" d="M17.7591,13.19254a.504.504,0,0,0,.252.07568c.13867,0,.20312-.06934.20312-.15625,0-.09082-.05371-.14063-.19434-.19287-.18847-.0669-.27734-.1709-.27734-.29639a.32288.32288,0,0,1,.36133-.3081.5225.5225,0,0,1,.25781.06543l-.04785.13867a.401.401,0,0,0-.21387-.061c-.11328,0-.17578.06494-.17578.14307,0,.08691.0625.126.19922.17773.18164.06934.27539.16016.27539.31641,0,.18408-.14355.314-.39258.314a.59421.59421,0,0,1-.29394-.07129Z"/>
|
||||||
|
<path class="cls-4" d="M18.85285,12.0441a.11768.11768,0,0,1-.23535,0,.116.116,0,0,1,.11914-.11914A.11252.11252,0,0,1,18.85285,12.0441Zm-.21191,1.34326V12.33853h.19043v1.04883Z"/>
|
||||||
|
<path class="cls-4" d="M19.37629,12.03726v.30127h.27344V12.484h-.27344v.56543c0,.12988.03711.20361.14355.20361a.411.411,0,0,0,.11036-.01318l.00879.14306a.47026.47026,0,0,1-.16895.02588.26294.26294,0,0,1-.20605-.08007.38541.38541,0,0,1-.07422-.27295V12.484h-.16211v-.14551h.16211v-.25146Z"/>
|
||||||
|
<path class="cls-4" d="M20.07356,12.0441a.11768.11768,0,0,1-.23536,0,.116.116,0,0,1,.11914-.11914A.11253.11253,0,0,1,20.07356,12.0441Zm-.21192,1.34326V12.33853h.19043v1.04883Z"/>
|
||||||
|
<path class="cls-4" d="M21.31672,12.85416a.51258.51258,0,0,1-.52246.55713.4986.4986,0,0,1-.50293-.53956.51227.51227,0,0,1,.52051-.55712A.49707.49707,0,0,1,21.31672,12.85416Zm-.832.01123c0,.22949.13183.40283.31836.40283.18164,0,.31836-.17139.31836-.40723,0-.17773-.08887-.40332-.31446-.40332C20.58234,12.45767,20.48469,12.66568,20.48469,12.86539Z"/>
|
||||||
|
<path class="cls-4" d="M21.55305,12.62271c0-.1084-.002-.19726-.00879-.28418h.16894l.01075.17334h.00488a.38541.38541,0,0,1,.34766-.19726c.14453,0,.37011.08691.37011.44677v.626h-.19043v-.60449c0-.169-.0625-.31006-.24414-.31006a.28249.28249,0,0,0-.26855.28418v.63037h-.19043Z"/>
|
||||||
|
<path class="cls-4" d="M23.19563,12.73062v.14111h-.53516v-.14111Z"/>
|
||||||
|
<path class="cls-4" d="M23.6507,12.92789l-.15136.45947H23.304l.49707-1.46045h.22754l.498,1.46045h-.20215l-.15527-.45947Zm.4795-.14746-.14356-.41993c-.03222-.0957-.05371-.18213-.07519-.2666h-.00489c-.02148.08643-.04492.1753-.07324.26416l-.14355.42237Z"/>
|
||||||
|
<path class="cls-4" d="M24.72395,12.68082c0-.13428-.00391-.24268-.00879-.34229h.1709l.00878.18018h.00489a.41188.41188,0,0,1,.37207-.2041.47521.47521,0,0,1,.44433.5332.49637.49637,0,0,1-.47656.56348.36912.36912,0,0,1-.32324-.16456h-.00391v.56983h-.18847Zm.18847.27978a.39045.39045,0,0,0,.00879.07813.2941.2941,0,0,0,.28613.22314c.20118,0,.31836-.165.31836-.40527,0-.21045-.11035-.39014-.3125-.39014a.30458.30458,0,0,0-.28808.23633.33935.33935,0,0,0-.0127.07764Z"/>
|
||||||
|
<path class="cls-4" d="M25.95344,12.68082c0-.13428-.00391-.24268-.00879-.34229h.1709l.00879.18018h.00488a.41188.41188,0,0,1,.37207-.2041.47521.47521,0,0,1,.44434.5332.49638.49638,0,0,1-.47657.56348.36912.36912,0,0,1-.32324-.16456h-.0039v.56983h-.18848Zm.18848.27978a.39047.39047,0,0,0,.00878.07813.2941.2941,0,0,0,.28614.22314c.20117,0,.31836-.165.31836-.40527,0-.21045-.11036-.39014-.3125-.39014a.30458.30458,0,0,0-.28809.23633.33934.33934,0,0,0-.01269.07764Z"/>
|
||||||
|
<path class="cls-4" d="M27.182,11.84879h.19043v1.53857H27.182Z"/>
|
||||||
|
<path class="cls-4" d="M27.79817,12.89761a.33586.33586,0,0,0,.35937.36426.68723.68723,0,0,0,.29-.0542l.0332.13623a.85506.85506,0,0,1-.34961.06494.48716.48716,0,0,1-.51562-.52832.50957.50957,0,0,1,.49219-.56591.44431.44431,0,0,1,.43359.49414.74715.74715,0,0,1-.00684.08886Zm.55859-.13623a.27171.27171,0,0,0-.26465-.31006.31557.31557,0,0,0-.292.31006Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-5" d="M59.49445,9.612a2.72215,2.72215,0,0,1,.40039-.03027.838.838,0,0,1,.5918.18212.67306.67306,0,0,1,.20606.52442.77119.77119,0,0,1-.21094.56738.89644.89644,0,0,1-.64551.21045,3.04732,3.04732,0,0,1-.3418-.01758Zm.18848,1.2915a1.16446,1.16446,0,0,0,.19043.01074.56147.56147,0,0,0,.62207-.61962.52132.52132,0,0,0-.5918-.56348,1.07261,1.07261,0,0,0-.2207.01953Z"/>
|
||||||
|
<path class="cls-5" d="M61.50715,11.05289l-.01563-.13233h-.00683a.3898.3898,0,0,1-.32031.15625.29867.29867,0,0,1-.32032-.30127c0-.25342.22461-.39209.62989-.39013v-.02149a.21588.21588,0,0,0-.23828-.24267.51747.51747,0,0,0-.27247.07812l-.04394-.126a.65487.65487,0,0,1,.34473-.09326c.32031,0,.39843.21924.39843.4292v.39209a1.50485,1.50485,0,0,0,.01758.25147Zm-.02832-.53516c-.208-.00439-.44434.03223-.44434.23633a.16824.16824,0,0,0,.17969.18164.261.261,0,0,0,.25391-.17529.21958.21958,0,0,0,.01074-.06055Z"/>
|
||||||
|
<path class="cls-5" d="M61.96711,10.33121c0-.12354-.002-.2295-.00879-.32715h.167l.00684.206H62.14a.31547.31547,0,0,1,.291-.23.20166.20166,0,0,1,.05371.00684v.17969a.28207.28207,0,0,0-.06445-.00635.26661.26661,0,0,0-.25586.24463.484.484,0,0,0-.00879.08886v.55909h-.18848Z"/>
|
||||||
|
<path class="cls-5" d="M62.87043,10.485h.00391c.02636-.03663.06347-.082.09375-.11915l.30761-.36181h.2295l-.40528.43115.46192.61768h-.23242L62.96809,10.55l-.09766.1084v.39453H62.682V9.51431h.18848Z"/>
|
||||||
|
<path class="cls-5" d="M64.11555,10.39615v.14111h-.53516v-.14111Z"/>
|
||||||
|
<path class="cls-5" d="M65.39387,11.005a.936.936,0,0,1-.38574.06933.68222.68222,0,0,1-.72168-.73877.72673.72673,0,0,1,.76269-.76709.78612.78612,0,0,1,.34668.06494l-.0459.15381a.69056.69056,0,0,0-.29394-.06055.54315.54315,0,0,0-.57031.60254.53086.53086,0,0,0,.56152.58692.753.753,0,0,0,.30762-.06055Z"/>
|
||||||
|
<path class="cls-5" d="M66.54426,10.51968a.51258.51258,0,0,1-.52246.55713.49859.49859,0,0,1-.50293-.53955.51228.51228,0,0,1,.52051-.55713A.49707.49707,0,0,1,66.54426,10.51968Zm-.832.01123c0,.2295.13183.40284.31836.40284.18164,0,.31836-.17139.31836-.40723,0-.17773-.08887-.40332-.31446-.40332C65.80988,10.1232,65.71223,10.33121,65.71223,10.53091Z"/>
|
||||||
|
<path class="cls-5" d="M66.78059,10.28824c0-.1084-.002-.19727-.00879-.28418h.167l.00879.16894h.00586a.36232.36232,0,0,1,.33008-.19287.311.311,0,0,1,.29687.21045h.00391a.43589.43589,0,0,1,.11719-.13672.358.358,0,0,1,.23144-.07373c.13867,0,.34473.09131.34473.45508v.61768h-.18653v-.59375c0-.20166-.07324-.32276-.22754-.32276a.24546.24546,0,0,0-.2246.17334.3083.3083,0,0,0-.01563.09522v.64795h-.18652v-.62842c0-.167-.07325-.28809-.21875-.28809a.26063.26063,0,0,0-.23633.19043.26691.26691,0,0,0-.01465.09326v.63282h-.18652Z"/>
|
||||||
|
<path class="cls-5" d="M68.58234,10.34634c0-.13427-.0039-.24267-.00878-.34228h.17089l.00879.18017h.00489a.41188.41188,0,0,1,.37207-.2041.47522.47522,0,0,1,.44433.53321.49637.49637,0,0,1-.47656.56347.3691.3691,0,0,1-.32324-.16455h-.00391v.56983h-.18848Zm.18848.27979a.38616.38616,0,0,0,.00879.07812.29409.29409,0,0,0,.28613.22315c.20118,0,.31836-.165.31836-.40528,0-.21044-.11035-.39013-.3125-.39013a.30458.30458,0,0,0-.28808.23633.33911.33911,0,0,0-.0127.07812Z"/>
|
||||||
|
<path class="cls-5" d="M70.76106,10.51968a.51258.51258,0,0,1-.52247.55713.49859.49859,0,0,1-.50292-.53955.51228.51228,0,0,1,.5205-.55713A.49708.49708,0,0,1,70.76106,10.51968Zm-.832.01123c0,.2295.13184.40284.31836.40284.18164,0,.31836-.17139.31836-.40723,0-.17773-.08886-.40332-.31445-.40332C70.02668,10.1232,69.929,10.33121,69.929,10.53091Z"/>
|
||||||
|
<path class="cls-5" d="M70.971,10.85806a.504.504,0,0,0,.252.07569c.13867,0,.20312-.06934.20312-.15625,0-.09082-.05371-.14063-.19433-.19239-.18848-.06738-.27734-.17138-.27734-.29687a.32287.32287,0,0,1,.36132-.30811.52254.52254,0,0,1,.25782.06543l-.04786.13867a.401.401,0,0,0-.21386-.061c-.11328,0-.17578.06494-.17578.14307,0,.08691.0625.126.19921.17773.18165.06934.2754.16016.2754.31641,0,.18408-.14356.314-.39258.314a.59422.59422,0,0,1-.294-.07129Z"/>
|
||||||
|
<path class="cls-5" d="M72.06477,9.70962a.11768.11768,0,0,1-.23535,0,.116.116,0,0,1,.11914-.11914A.11253.11253,0,0,1,72.06477,9.70962Zm-.21192,1.34327V10.00406h.19043v1.04883Z"/>
|
||||||
|
<path class="cls-5" d="M72.53156,9.42789v2.1665H72.387V9.42789Z"/>
|
||||||
|
</g>
|
||||||
|
<rect class="cls-6" x="58.54489" y="8.74619" width="26.45526" height="3.21029"/>
|
||||||
|
<path class="cls-7" d="M62.00373,14.51282H81.26555a2.051,2.051,0,0,1,2.051,2.051v0a2.051,2.051,0,0,1-2.051,2.051H62.00371a2.051,2.051,0,0,1-2.051-2.051v0a2.051,2.051,0,0,1,2.051-2.051Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-8" d="M65.012,17.33609a1.07013,1.07013,0,0,1-.44824.08105.79272.79272,0,0,1-.83789-.85888.84386.84386,0,0,1,.88574-.89112.908.908,0,0,1,.40332.07569l-.05273.17871a.79665.79665,0,0,0-.34278-.0708.63123.63123,0,0,0-.66211.70019.61714.61714,0,0,0,.65235.68262.8744.8744,0,0,0,.35742-.07129Z"/>
|
||||||
|
<path class="cls-8" d="M66.35578,16.77261a.59491.59491,0,0,1-.60644.64649.57871.57871,0,0,1-.584-.62647.59494.59494,0,0,1,.60449-.647A.57654.57654,0,0,1,66.35578,16.77261Zm-.96679.01221c0,.26709.15429.46826.37011.46826.21192,0,.37012-.19873.37012-.47314,0-.20655-.10254-.46826-.36426-.46826C65.50227,16.31168,65.389,16.55337,65.389,16.78482Z"/>
|
||||||
|
<path class="cls-8" d="M66.637,16.50308c0-.126-.00195-.229-.00976-.33008h.19629l.01269.20166h.00488a.44832.44832,0,0,1,.40332-.229c.168,0,.42969.10059.42969.51855v.72754h-.2207v-.70263c0-.19629-.07324-.35987-.28223-.35987a.31323.31323,0,0,0-.29687.22657.30158.30158,0,0,0-.01563.10351v.73242H66.637Z"/>
|
||||||
|
<path class="cls-8" d="M68.03547,16.50308c0-.126-.002-.229-.00977-.33008H68.222l.0127.20166h.00488a.4483.4483,0,0,1,.40332-.229c.168,0,.42969.10059.42969.51855v.72754h-.2207v-.70263c0-.19629-.07325-.35987-.28223-.35987a.31324.31324,0,0,0-.29688.22657.30183.30183,0,0,0-.01562.10351v.73242h-.22168Z"/>
|
||||||
|
<path class="cls-8" d="M69.55793,16.82291a.38967.38967,0,0,0,.417.42236.804.804,0,0,0,.33789-.0625l.03809.1582a.97185.97185,0,0,1-.40527.07617.566.566,0,0,1-.59961-.61474.59156.59156,0,0,1,.57129-.65674.51589.51589,0,0,1,.5039.57373.76.76,0,0,1-.00781.10352Zm.64941-.1587a.31647.31647,0,0,0-.30761-.36035.3676.3676,0,0,0-.33985.36035Z"/>
|
||||||
|
<path class="cls-8" d="M71.56574,17.34683a.82005.82005,0,0,1-.35058.07031.57883.57883,0,0,1-.60645-.62207.6191.6191,0,0,1,.6543-.647.74274.74274,0,0,1,.30762.063l-.05079.1709a.51248.51248,0,0,0-.25683-.05762.45913.45913,0,0,0-.00684.916.6197.6197,0,0,0,.27149-.06055Z"/>
|
||||||
|
<path class="cls-8" d="M72.12434,15.82339V16.173h.31738v.169h-.31738v.65723c0,.15039.043.23632.167.23632a.52663.52663,0,0,0,.12793-.01464l.00976.166a.538.538,0,0,1-.19628.03027.30934.30934,0,0,1-.23926-.09375.45342.45342,0,0,1-.085-.31689V16.342h-.18946v-.169h.18946v-.292Z"/>
|
||||||
|
<path class="cls-8" d="M73.23371,15.71744a3.15816,3.15816,0,0,1,.46582-.03516.9716.9716,0,0,1,.6875.21143.78446.78446,0,0,1,.23926.60937.8976.8976,0,0,1-.24414.65918,1.04519,1.04519,0,0,1-.751.24414,3.511,3.511,0,0,1-.39746-.01953Zm.21875,1.50049a1.42013,1.42013,0,0,0,.22168.01269.65214.65214,0,0,0,.72266-.72021.60472.60472,0,0,0-.6875-.6543,1.24505,1.24505,0,0,0-.25684.02246Z"/>
|
||||||
|
<path class="cls-8" d="M75.0384,16.82291a.38967.38967,0,0,0,.417.42236.804.804,0,0,0,.33789-.0625l.03809.1582a.97191.97191,0,0,1-.40528.07617.566.566,0,0,1-.5996-.61474.59156.59156,0,0,1,.57128-.65674.5159.5159,0,0,1,.50391.57373.76.76,0,0,1-.00781.10352Zm.64941-.1587a.31647.31647,0,0,0-.30761-.36035.3676.3676,0,0,0-.33985.36035Z"/>
|
||||||
|
<path class="cls-8" d="M76.263,16.173l.23926.68506a3.005,3.005,0,0,1,.09765.312h.00782c.02734-.10058.06347-.20117.10351-.312l.23633-.68506H77.179l-.47851,1.21875h-.21094L76.02668,16.173Z"/>
|
||||||
|
<path class="cls-8" d="M77.63606,15.83072a.13721.13721,0,0,1-.27442,0,.13459.13459,0,0,1,.13867-.13818A.1311.1311,0,0,1,77.63606,15.83072Zm-.24707,1.561V16.173h.22168v1.21875Z"/>
|
||||||
|
<path class="cls-8" d="M78.848,17.34683a.82007.82007,0,0,1-.35059.07031.57883.57883,0,0,1-.60644-.62207.6191.6191,0,0,1,.6543-.647.74269.74269,0,0,1,.30761.063l-.05078.1709a.51248.51248,0,0,0-.25683-.05762.45913.45913,0,0,0-.00684.916.61965.61965,0,0,0,.27148-.06055Z"/>
|
||||||
|
<path class="cls-8" d="M79.21711,16.82291a.38993.38993,0,0,0,.418.42236.80229.80229,0,0,0,.33691-.0625l.03809.1582a.97185.97185,0,0,1-.40527.07617.566.566,0,0,1-.59961-.61474.59156.59156,0,0,1,.57129-.65674.51589.51589,0,0,1,.5039.57373.76.76,0,0,1-.00781.10352Zm.64941-.1587a.31647.31647,0,0,0-.30761-.36035.3676.3676,0,0,0-.33985.36035Z"/>
|
||||||
|
</g>
|
||||||
|
<path class="cls-9" d="M41.24948,8.87635l1.41044.0142a3.896,3.896,0,0,1,3.85656,3.935h0a3.635,3.635,0,0,1-3.67142,3.59822l-1.52229-.01533"/>
|
||||||
|
<path class="cls-9" d="M41.24948,8.87635l1.41044.0142a3.896,3.896,0,0,1,3.85656,3.935h0a3.635,3.635,0,0,1-3.67142,3.59822l-1.52229-.01533"/>
|
||||||
|
<path class="cls-9" d="M42.26721,16.418l-1.41044-.0142a3.896,3.896,0,0,1-3.85657-3.935h0a3.635,3.635,0,0,1,3.67143-3.59822l1.52228.01533"/>
|
||||||
|
<path class="cls-9" d="M46.51648,12.82557h0a3.635,3.635,0,0,1-3.67142,3.59822l-1.52229-.01533"/>
|
||||||
|
<path class="cls-9" d="M37.0002,12.46875a3.635,3.635,0,0,1,3.67143-3.59822l1.52228.01533"/>
|
||||||
|
<path class="cls-10" d="M41.24948,8.87635l1.41044.0142a3.896,3.896,0,0,1,3.85656,3.935h0a3.635,3.635,0,0,1-3.67142,3.59822l-1.52229-.01533"/>
|
||||||
|
<path class="cls-10" d="M42.26721,16.418l-1.41044-.0142a3.896,3.896,0,0,1-3.85657-3.935h0a3.635,3.635,0,0,1,3.67143-3.59822l1.52228.01533"/>
|
||||||
|
<path class="cls-9" d="M47.75052,16.418l-1.41044-.0142a3.896,3.896,0,0,1-3.85656-3.935h0a3.635,3.635,0,0,1,3.67142-3.59822l1.52229.01533"/>
|
||||||
|
<path class="cls-9" d="M46.73279,8.87635l1.41044.0142a3.896,3.896,0,0,1,3.85657,3.935h0a3.635,3.635,0,0,1-3.67143,3.59822l-1.52228-.01533"/>
|
||||||
|
<path class="cls-9" d="M46.73279,8.87635l1.41044.0142a3.896,3.896,0,0,1,3.85657,3.935h0a3.635,3.635,0,0,1-3.67143,3.59822l-1.52228-.01533"/>
|
||||||
|
<path class="cls-9" d="M47.75052,16.418l-1.41044-.0142a3.896,3.896,0,0,1-3.85656-3.935h0a3.635,3.635,0,0,1,3.67142-3.59822l1.52229.01533"/>
|
||||||
|
<path class="cls-9" d="M51.9998,12.82557h0a3.635,3.635,0,0,1-3.67143,3.59822l-1.52228-.01533"/>
|
||||||
|
<path class="cls-9" d="M42.48352,12.46875a3.635,3.635,0,0,1,3.67142-3.59822l1.52229.01533"/>
|
||||||
|
<path class="cls-9" d="M46.73279,8.87635l1.41044.0142a3.896,3.896,0,0,1,3.85657,3.935h0a3.635,3.635,0,0,1-3.67143,3.59822l-1.52228-.01533"/>
|
||||||
|
<path class="cls-9" d="M42.48352,12.46875a3.635,3.635,0,0,1,3.67142-3.59822l1.52229.01533"/>
|
||||||
|
<path class="cls-10" d="M46.51648,12.82557h0a3.635,3.635,0,0,1-3.67142,3.59822l-1.52229-.01533"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 18 KiB |
BIN
server/static/res/favicon.ico
Executable file
After Width: | Height: | Size: 1.1 KiB |
BIN
server/static/res/googlelogo.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
100
server/static/res/personalize_view.svg
Executable file
@ -0,0 +1,100 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 75">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #1b75bc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #ec008c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #231f20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #f7941d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: #a7a9ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-7 {
|
||||||
|
fill: #8dc63f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-8 {
|
||||||
|
fill: #be1e2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-9 {
|
||||||
|
fill: #00aeef;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<title>Artboard 1</title>
|
||||||
|
<rect class="cls-1" x="43.80966" y="6.00119" width="12.56532" height="6.47899" rx="3.23949"/>
|
||||||
|
<circle class="cls-2" cx="53.13548" cy="9.26874" r="2.31393"/>
|
||||||
|
<circle class="cls-3" cx="81.65535" cy="9.26874" r="3.34465"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-4" d="M5.3986,7.25093a4.42753,4.42753,0,0,1,.83448-.07324,1.46005,1.46005,0,0,1,.97119.25732.74138.74138,0,0,1,.29394.62451.84011.84011,0,0,1-.59814.77686v.01025a.91189.91189,0,0,1,.72949.88721.95413.95413,0,0,1-.29932.70849,1.77465,1.77465,0,0,1-1.22314.33057,5.25541,5.25541,0,0,1-.7085-.042Zm.45655,1.44873h.415c.48242,0,.76611-.252.76611-.59278,0-.415-.31494-.57763-.77686-.57763a1.92933,1.92933,0,0,0-.40429.03174Zm0,1.69531a2.3895,2.3895,0,0,0,.3833.02148c.47216,0,.9082-.17334.9082-.688,0-.48291-.41455-.68213-.91357-.68213H5.85515Z"/>
|
||||||
|
<path class="cls-4" d="M10.5446,9.45063A1.24131,1.24131,0,0,1,9.28,10.79927,1.20752,1.20752,0,0,1,8.06218,9.49263,1.24036,1.24036,0,0,1,9.32194,8.14351,1.20269,1.20269,0,0,1,10.5446,9.45063ZM8.529,9.47651c0,.55664.32032.97657.77149.97657.4414,0,.772-.41456.772-.98682,0-.43066-.21533-.97656-.76123-.97656S8.529,8.9936,8.529,9.47651Z"/>
|
||||||
|
<path class="cls-4" d="M13.318,7.01509V10.0854c0,.22559.00537.48291.021.65625h-.41455l-.021-.44092h-.01026a.94.94,0,0,1-.86621.49854,1.15822,1.15822,0,0,1-1.08691-1.291,1.21563,1.21563,0,0,1,1.13916-1.36474.85.85,0,0,1,.7666.38818h.01025V7.01509ZM12.85612,9.2353a.79549.79549,0,0,0-.021-.19433.6789.6789,0,0,0-.66651-.53565c-.47754,0-.76123.41992-.76123.98194,0,.51416.252.93945.75049.93945a.69409.69409,0,0,0,.67725-.55127.78488.78488,0,0,0,.021-.19922Z"/>
|
||||||
|
<path class="cls-4" d="M14.24919,8.20112l.55615,1.50147c.05762.168.1211.36718.16309.51953h.01025c.04736-.15235.09961-.34668.1626-.53027l.50391-1.49073h.48828l-.69287,1.811a3.86132,3.86132,0,0,1-.87159,1.59033,1.24923,1.24923,0,0,1-.5664.29932l-.11573-.38868a1.22385,1.22385,0,0,0,.4043-.22558,1.39924,1.39924,0,0,0,.38818-.51465.33274.33274,0,0,0,.03711-.10986.40318.40318,0,0,0-.03173-.12061l-.93946-2.34131Z"/>
|
||||||
|
</g>
|
||||||
|
<rect class="cls-1" x="43.80966" y="6.00119" width="12.56532" height="6.47899" rx="3.23949"/>
|
||||||
|
<circle class="cls-2" cx="53.13548" cy="9.26874" r="2.31393"/>
|
||||||
|
<circle class="cls-3" cx="81.65535" cy="9.26874" r="3.34465"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-4" d="M5.85515,21.31977v1.48H7.56657v-1.48h.46192v3.5376H7.56657V23.19868H5.85515v1.65869H5.3986v-3.5376Z"/>
|
||||||
|
<path class="cls-4" d="M11.10026,23.56587A1.24146,1.24146,0,0,1,9.83561,24.915a1.20768,1.20768,0,0,1-1.21777-1.30713,1.24021,1.24021,0,0,1,1.25977-1.34863A1.2028,1.2028,0,0,1,11.10026,23.56587Zm-2.01562.02637c0,.55664.32031.97656.77148.97656.44141,0,.772-.415.772-.98682,0-.43066-.21534-.97656-.76124-.97656S9.08464,23.10933,9.08464,23.59224Z"/>
|
||||||
|
<path class="cls-4" d="M11.65886,24.85737c.01025-.17334.021-.43017.021-.65625v-3.0708h.45655V22.726h.01074a.946.946,0,0,1,.86572-.46679,1.13985,1.13985,0,0,1,1.0708,1.29638A1.20708,1.20708,0,0,1,12.945,24.915a.90507.90507,0,0,1-.85059-.47754h-.01562l-.021.41992Zm.47754-1.01806a.88806.88806,0,0,0,.021.168.71314.71314,0,0,0,.69287.54053c.48291,0,.77149-.394.77149-.97656,0-.50928-.26221-.94482-.75586-.94482a.73609.73609,0,0,0-.70313.56689.89836.89836,0,0,0-.02636.189Z"/>
|
||||||
|
<path class="cls-4" d="M14.64226,24.85737c.01025-.17334.021-.43017.021-.65625v-3.0708h.45654V22.726h.01075a.946.946,0,0,1,.86572-.46679,1.13985,1.13985,0,0,1,1.0708,1.29638A1.20709,1.20709,0,0,1,15.92839,24.915a.90507.90507,0,0,1-.85059-.47754h-.01562l-.021.41992Zm.47753-1.01806a.88806.88806,0,0,0,.021.168.71313.71313,0,0,0,.69287.54053c.48291,0,.77149-.394.77149-.97656,0-.50928-.26221-.94482-.75586-.94482a.73609.73609,0,0,0-.70313.56689.8983.8983,0,0,0-.02637.189Z"/>
|
||||||
|
<path class="cls-4" d="M17.778,22.31685l.55615,1.501c.05811.168.12061.36768.1626.52h.01074c.04736-.15234.09961-.34668.1626-.53027l.5039-1.49072h.48829l-.69288,1.811a3.87332,3.87332,0,0,1-.87109,1.59033,1.25636,1.25636,0,0,1-.56738.29932l-.11524-.38867a1.21978,1.21978,0,0,0,.4043-.22559,1.40115,1.40115,0,0,0,.38818-.51416.331.331,0,0,0,.03662-.11035.40324.40324,0,0,0-.03125-.12109l-.93945-2.34082Z"/>
|
||||||
|
</g>
|
||||||
|
<rect class="cls-1" x="43.80966" y="20.11682" width="12.56532" height="6.47899" rx="3.23949"/>
|
||||||
|
<circle class="cls-2" cx="53.13548" cy="23.38437" r="2.31393"/>
|
||||||
|
<circle class="cls-5" cx="81.65535" cy="23.38437" r="3.34465"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-4" d="M5.85515,35.435v1.48047H7.56657V35.435h.46192V38.9731H7.56657V37.3144H5.85515v1.6587H5.3986V35.435Z"/>
|
||||||
|
<path class="cls-4" d="M11.10026,37.68208a1.24131,1.24131,0,0,1-1.26465,1.34863,1.20752,1.20752,0,0,1-1.21777-1.30664A1.24037,1.24037,0,0,1,9.87761,36.375,1.2027,1.2027,0,0,1,11.10026,37.68208ZM9.08464,37.708c0,.55664.32031.97656.77148.97656.44141,0,.772-.41455.772-.98682,0-.43066-.21534-.97656-.76124-.97656S9.08464,37.22505,9.08464,37.708Z"/>
|
||||||
|
<path class="cls-4" d="M11.67985,37.12007c0-.26221-.00537-.47754-.021-.6875h.40429l.02051.40967h.01612a.87514.87514,0,0,1,.79785-.46729.75147.75147,0,0,1,.71923.50928h.01026a1.041,1.041,0,0,1,.28369-.33106.86676.86676,0,0,1,.56152-.17822c.33594,0,.83448.22022.83448,1.102v1.4961h-.45166V37.53462c0-.48779-.17823-.78174-.55079-.78174a.59715.59715,0,0,0-.54589.41992.76011.76011,0,0,0-.03711.231V38.9731h-.45117V37.45063c0-.4038-.17872-.69775-.53028-.69775a.63172.63172,0,0,0-.57226.46191.6434.6434,0,0,0-.03662.22559V38.9731h-.45118Z"/>
|
||||||
|
<path class="cls-4" d="M16.30925,37.78657a.814.814,0,0,0,.87158.88184,1.67715,1.67715,0,0,0,.70361-.13086l.07862.33056a2.05238,2.05238,0,0,1-.84522.15772,1.17977,1.17977,0,0,1-1.249-1.28125,1.233,1.233,0,0,1,1.1914-1.36963A1.07643,1.07643,0,0,1,18.11,37.57173a1.81233,1.81233,0,0,1-.01563.21484Zm1.35449-.33056a.65747.65747,0,0,0-.64063-.75049.76576.76576,0,0,0-.70849.75049Z"/>
|
||||||
|
</g>
|
||||||
|
<rect class="cls-6" x="43.80966" y="34.23246" width="12.56532" height="6.47899" rx="3.23949"/>
|
||||||
|
<circle class="cls-2" cx="47.00823" cy="37.5" r="2.31393"/>
|
||||||
|
<circle class="cls-7" cx="81.65535" cy="37.5" r="3.34465"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-4" d="M7.23063,51.42964H5.85515V52.705H7.38786v.3833H5.3986V49.55024H7.30925v.3833H5.85515v1.11817H7.23063Z"/>
|
||||||
|
<path class="cls-4" d="M7.96257,51.2353c0-.2622-.00537-.47754-.021-.6875h.40918l.02637.41992h.01025a.93369.93369,0,0,1,.83985-.47753c.352,0,.89795.21.89795,1.08105v1.51709H9.66325V51.624c0-.40966-.15234-.75048-.58789-.75048a.65631.65631,0,0,0-.61963.47216.66339.66339,0,0,0-.03125.21534v1.52734H7.96257Z"/>
|
||||||
|
<path class="cls-4" d="M11.40739,49.81831v.72949H12.069v.35156h-.66162v1.37012c0,.31494.08936.49365.34668.49365a1.02994,1.02994,0,0,0,.26758-.03173l.021.34619a1.12112,1.12112,0,0,1-.40918.06347.641.641,0,0,1-.499-.19433.94393.94393,0,0,1-.17822-.66162V50.89936h-.394V50.5478h.394v-.60888Z"/>
|
||||||
|
<path class="cls-4" d="M12.81218,51.90181a.814.814,0,0,0,.87158.88183,1.67715,1.67715,0,0,0,.70361-.13086l.07862.33057a2.05258,2.05258,0,0,1-.84522.15771,1.17977,1.17977,0,0,1-1.249-1.28125,1.233,1.233,0,0,1,1.1914-1.36962A1.07643,1.07643,0,0,1,14.613,51.687a1.81272,1.81272,0,0,1-.01563.21485Zm1.35449-.33057a.65747.65747,0,0,0-.64063-.75049.76577.76577,0,0,0-.70849.75049Z"/>
|
||||||
|
<path class="cls-4" d="M15.18181,51.34028c0-.29883-.00537-.55615-.021-.79248h.4043l.01562.49854h.021a.762.762,0,0,1,.70312-.55615.51016.51016,0,0,1,.13135.01562v.43555a.70988.70988,0,0,0-.15723-.01563.6479.6479,0,0,0-.61962.59326,1.3229,1.3229,0,0,0-.021.21485v1.35449h-.45654Z"/>
|
||||||
|
<path class="cls-4" d="M17.5778,49.81831v.72949h.66162v.35156H17.5778v1.37012c0,.31494.08936.49365.34668.49365a1.02994,1.02994,0,0,0,.26758-.03173l.021.34619a1.12216,1.12216,0,0,1-.40967.06347.64064.64064,0,0,1-.49854-.19433.94155.94155,0,0,1-.17822-.66162V50.89936h-.39355V50.5478h.39355v-.60888Z"/>
|
||||||
|
<path class="cls-4" d="M20.16374,53.08833l-.03711-.32031H20.111a.94529.94529,0,0,1-.77686.37793.724.724,0,0,1-.77686-.7295c0-.61425.5459-.95019,1.52735-.94482v-.05225a.52359.52359,0,0,0-.57715-.58789,1.26587,1.26587,0,0,0-.66113.18848l-.10547-.3042a1.57993,1.57993,0,0,1,.835-.22558c.77685,0,.96582.52978.96582,1.03906v.95019a3.55479,3.55479,0,0,0,.042.60889Zm-.06836-1.29639c-.50391-.01074-1.07617.07862-1.07617.57178a.40918.40918,0,0,0,.43555.44141.63159.63159,0,0,0,.61425-.4253.4885.4885,0,0,0,.02637-.147Z"/>
|
||||||
|
<path class="cls-4" d="M21.798,49.83394a.2859.2859,0,0,1-.57178,0,.28133.28133,0,0,1,.28857-.28858A.27389.27389,0,0,1,21.798,49.83394Zm-.51416,3.25439V50.5478h.46191v2.54053Z"/>
|
||||||
|
<path class="cls-4" d="M22.51091,51.2353c0-.2622-.00537-.47754-.021-.6875h.40918l.02637.41992h.01074a.93368.93368,0,0,1,.83984-.47753c.35157,0,.89747.21.89747,1.08105v1.51709h-.46192V51.624c0-.40966-.15234-.75048-.58789-.75048a.65585.65585,0,0,0-.61963.47216.66339.66339,0,0,0-.03125.21534v1.52734h-.46191Z"/>
|
||||||
|
<path class="cls-4" d="M25.42058,51.2353c0-.2622-.00538-.47754-.021-.6875h.40381l.02148.40967h.01563a.87515.87515,0,0,1,.79785-.46728.7504.7504,0,0,1,.71875.50927h.01074a1.05221,1.05221,0,0,1,.2832-.331.86909.86909,0,0,1,.562-.17822c.33593,0,.83447.22021.83447,1.102v1.49609h-.45117V51.64985c0-.48779-.17871-.78174-.55127-.78174a.59715.59715,0,0,0-.5459.41993.76065.76065,0,0,0-.03662.231v1.56934h-.45166V51.56587c0-.40381-.17822-.69776-.53028-.69776a.68993.68993,0,0,0-.60888.6875v1.53272h-.45117Z"/>
|
||||||
|
<path class="cls-4" d="M30.05046,51.90181a.8134.8134,0,0,0,.87109.88183,1.67294,1.67294,0,0,0,.70313-.13086l.0791.33057a2.05447,2.05447,0,0,1-.84522.15771,1.18014,1.18014,0,0,1-1.24951-1.28125,1.23332,1.23332,0,0,1,1.1919-1.36962,1.07642,1.07642,0,0,1,1.0498,1.19677,1.69487,1.69487,0,0,1-.01611.21485Zm1.354-.33057a.65737.65737,0,0,0-.64013-.75049.76622.76622,0,0,0-.709.75049Z"/>
|
||||||
|
<path class="cls-4" d="M32.4196,51.2353c0-.2622-.00537-.47754-.021-.6875h.40918l.02637.41992h.01025a.9337.9337,0,0,1,.83985-.47753c.352,0,.898.21.898,1.08105v1.51709h-.46192V51.624c0-.40966-.15234-.75048-.58789-.75048a.65631.65631,0,0,0-.61963.47216.66339.66339,0,0,0-.03125.21534v1.52734H32.4196Z"/>
|
||||||
|
<path class="cls-4" d="M35.86442,49.81831v.72949H36.526v.35156h-.66162v1.37012c0,.31494.08936.49365.34668.49365a1.02994,1.02994,0,0,0,.26758-.03173l.021.34619a1.12112,1.12112,0,0,1-.40918.06347.64106.64106,0,0,1-.499-.19433.94393.94393,0,0,1-.17822-.66162V50.89936h-.394V50.5478h.394v-.60888Z"/>
|
||||||
|
</g>
|
||||||
|
<rect class="cls-1" x="43.80966" y="48.34809" width="12.56532" height="6.47899" rx="3.23949"/>
|
||||||
|
<circle class="cls-2" cx="53.13548" cy="51.61563" r="2.31393"/>
|
||||||
|
<circle class="cls-8" cx="81.65535" cy="51.61563" r="3.34465"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-4" d="M5.97624,67.20454l-.89795-3.53809H5.5612l.41992,1.79c.105.44141.19971.88184.2627,1.22266h.01025c.05811-.35156.168-.77148.28858-1.22754l.47265-1.78516h.47754l.43067,1.7959c.0996.41992.19433.83985.24658,1.21192h.01025c.07373-.38867.17334-.78223.28369-1.22266l.46729-1.78516h.46679L8.39567,67.20454H7.91813l-.44628-1.84277a10.77444,10.77444,0,0,1-.231-1.1543H7.23063a11.11184,11.11184,0,0,1-.27294,1.1543l-.50391,1.84277Z"/>
|
||||||
|
<path class="cls-4" d="M11.9445,65.91352a1.24141,1.24141,0,0,1-1.26514,1.34864,1.20779,1.20779,0,0,1-1.21777-1.30664,1.241,1.241,0,0,1,1.25977-1.34961A1.2032,1.2032,0,0,1,11.9445,65.91352Zm-2.01562.02637c0,.55567.32031.97559.772.97559.44043,0,.77148-.41406.77148-.98633,0-.43066-.21533-.97656-.76123-.97656S9.92888,65.45649,9.92888,65.93989Z"/>
|
||||||
|
<path class="cls-4" d="M12.52409,65.45649c0-.29882-.00537-.55664-.021-.793h.4038l.01612.499h.021a.76256.76256,0,0,1,.70313-.55664.48779.48779,0,0,1,.13135.0166v.43555a.68042.68042,0,0,0-.15772-.01563.64737.64737,0,0,0-.61914.59277,1.27134,1.27134,0,0,0-.021.21485v1.35449h-.45654Z"/>
|
||||||
|
<path class="cls-4" d="M14.716,65.82954h.01025c.06348-.08984.15234-.19922.22607-.28906l.74512-.877h.55664l-.98193,1.04493,1.11816,1.49609h-.56152l-.87647-1.21777-.23632.26269v.95508h-.45655V63.478H14.716Z"/>
|
||||||
|
</g>
|
||||||
|
<rect class="cls-1" x="43.80966" y="62.46372" width="12.56532" height="6.47899" rx="3.23949"/>
|
||||||
|
<circle class="cls-2" cx="53.13548" cy="65.73126" r="2.31393"/>
|
||||||
|
<circle class="cls-9" cx="81.65535" cy="65.73126" r="3.34465"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 12 KiB |
BIN
server/static/res/sunview.png
Executable file
After Width: | Height: | Size: 15 KiB |
BIN
server/static/res/tizenlogo.png
Normal file
After Width: | Height: | Size: 32 KiB |
61
server/static/res/watchface.svg
Executable file
@ -0,0 +1,61 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 473.42255 469.30275">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2, .cls-3, .cls-4, .cls-5, .cls-6 {
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
stroke: #21409a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2, .cls-4, .cls-5, .cls-6 {
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
stroke: #92278f;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
stroke: #ee2a7b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
stroke: #00aeef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
stroke: #39b54a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-7 {
|
||||||
|
fill: #fbb040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-8 {
|
||||||
|
fill: #454da1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<title>Artboard 1</title>
|
||||||
|
<g id="background">
|
||||||
|
<circle class="cls-1" cx="237.48192" cy="234.48624" r="203.12844"/>
|
||||||
|
</g>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path class="cls-2" d="M100.10931,128.61269a174.61778,174.61778,0,0,1,32.93776-32.07842"/>
|
||||||
|
<path class="cls-3" d="M236.62631,409.45124A173.63773,173.63773,0,0,1,78.53868,164.03335"/>
|
||||||
|
<path class="cls-4" d="M384.6971,326.51445a174.40847,174.40847,0,0,1-29.53382,36.16634"/>
|
||||||
|
<path class="cls-5" d="M376.28793,132.73488a174.0403,174.0403,0,0,1,30.9287,135.45127q-1.41985,7.53807-3.47937,14.83668"/>
|
||||||
|
<path class="cls-6" d="M213.06279,63.846a173.34913,173.34913,0,0,1,148.19464,51.16884"/>
|
||||||
|
<circle class="cls-7" cx="303.91311" cy="150.88991" r="29.17431"/>
|
||||||
|
<circle class="cls-8" cx="271.54614" cy="309.09174" r="14.97248"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
17
server/template/account.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends "sidebar.html" %}
|
||||||
|
{% block body%}
|
||||||
|
<div class="container profile">
|
||||||
|
<img class="picture" src={{ profile_img }} alt="Profile Picture"></img>
|
||||||
|
<p class="name"> {{ username }} </p>
|
||||||
|
</div>
|
||||||
|
<div class="sub container">
|
||||||
|
<p class=name>E-mail </p><p class=data> {{email}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="sub container">
|
||||||
|
<p class=data>0</p><p class=name> Devices </p>
|
||||||
|
</div>
|
||||||
|
<div class=container>
|
||||||
|
<a class="button logout" href="/logout">Logout</a>
|
||||||
|
<a class="button delete" href="/delete_account">Delete Account</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
22
server/template/base.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/css/main.css">
|
||||||
|
<link rel="shortcut icon" type="image/jpg" href="/static/res/favicon.ico"/>
|
||||||
|
<title>Longitude</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id=container>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
<div id=main>
|
||||||
|
</div>
|
||||||
|
{% include "footer.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
11
server/template/base_navigation.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
<div class="navigation" id="navigation">
|
||||||
|
<a href="/"><div class='primeblue'>Longitude</div></a>
|
||||||
|
<div class="navigation_rightside">
|
||||||
|
<a href="/login">Log In</a>
|
||||||
|
<a href="/register">Sign up</a>
|
||||||
|
</div>
|
||||||
|
<a href="javascript:void(0);" class="icon" onclick="menuBars()">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
@ -1,30 +1,29 @@
|
|||||||
{% extends "sidebar.html" %}
|
{% extends "sidebar.html" %}
|
||||||
{% block body%}
|
{% block body%}
|
||||||
|
|
||||||
<div style="height: 50px">
|
<div class="container">
|
||||||
<div style="width: 30%; float: left; margin-left: 10%">Calendar</div>
|
<div style="width: 15rem; margin: 1rem">Calendar</div>
|
||||||
<div style="width: 30%; float: left">Show on device</div>
|
<div style="width: 10rem; margin: 1rem; padding-right: 5rem">Show on device</div>
|
||||||
<div style="width: 30%; float: left">Color</div>
|
<div style="width: 2rem; margin: 1rem">Color</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
{% for item in calendars %}
|
{% for item in calendars %}
|
||||||
<div style="height: 30px">
|
<div class="container">
|
||||||
<!--Name-->
|
<!--Name-->
|
||||||
<div style="width: 30%; float: left; font-size: 10px; text-align: left; margin-left: 10%">{{ item.name }}</div>
|
<div style="width: 15rem; margin: 1rem;">{{ item.name }}</div>
|
||||||
|
|
||||||
<!--Toggle-->
|
<!--Toggle-->
|
||||||
<div style="width: 30%; float: left">
|
<div style="width: 10rem; margin: 1rem; padding-right: 5rem">
|
||||||
<!-- Rounded switch -->
|
<!-- Rounded switch -->
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
<input type="checkbox">
|
<input class="toggle" id={{item.calendarId}} type="checkbox" toggled={{item.toggle}} onclick="toggleReaction(this)">
|
||||||
<span id={{item.name}} class="slider round" onclick="toggleReaction(this)"></span>
|
<span class="slider on round"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--Color Selector-->
|
<!--Color Selector-->
|
||||||
<div style="width: 30%; float: left">
|
<div style="width: 2rem; margin: 1rem;">
|
||||||
<div class="colorPickSelector" id={{item.name}} defaultColor={{item.color}}></div>
|
<div class="colorPickSelector" id={{item.calendarId}} defaultColor={{item.color}}></div>
|
||||||
<!--svg height="20" width="20">
|
<!--svg height="20" width="20">
|
||||||
<circle cx="10" cy="10" r="10" stroke="black" stroke-width="0" fill={{ item.color }} />
|
<circle cx="10" cy="10" r="10" stroke="black" stroke-width="0" fill={{ item.color }} />
|
||||||
</svg-->
|
</svg-->
|
||||||
@ -32,12 +31,21 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<div id=calendars class="container">
|
||||||
</div>
|
<a class="button" href="login/google">Google Calendar</a>
|
||||||
|
<a class="button" href="#" >Nextcloud Calendar</a>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<a class="button addcalendar" href="/login/google" style="width: auto; margin: 4rem">Add Calendar</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
|
||||||
|
var init = false;
|
||||||
|
|
||||||
|
|
||||||
|
// initialize all DOM items
|
||||||
$(".colorPickSelector").colorPick({
|
$(".colorPickSelector").colorPick({
|
||||||
'initialColor': '#3498db',
|
'initialColor': '#3498db',
|
||||||
'allowRecent': true,
|
'allowRecent': true,
|
||||||
@ -45,27 +53,41 @@
|
|||||||
'allowCustomColor': false,
|
'allowCustomColor': false,
|
||||||
'palette': ["#1abc9c", "#16a085", "#2ecc71", "#27ae60", "#3498db", "#2980b9", "#9b59b6", "#8e44ad", "#34495e", "#2c3e50", "#f1c40f", "#f39c12", "#e67e22", "#d35400", "#e74c3c", "#c0392b", "#ecf0f1", "#bdc3c7", "#95a5a6", "#7f8c8d"],
|
'palette': ["#1abc9c", "#16a085", "#2ecc71", "#27ae60", "#3498db", "#2980b9", "#9b59b6", "#8e44ad", "#34495e", "#2c3e50", "#f1c40f", "#f39c12", "#e67e22", "#d35400", "#e74c3c", "#c0392b", "#ecf0f1", "#bdc3c7", "#95a5a6", "#7f8c8d"],
|
||||||
'onColorSelected': function() {
|
'onColorSelected': function() {
|
||||||
|
if(!init) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Todo getting the element id is currently done over [0] [#02]
|
// Todo getting the element id is currently done over [0] [#02]
|
||||||
this.element.css({'backgroundColor': this.color, 'color': this.color});
|
this.element.css({'backgroundColor': this.color, 'color': this.color});
|
||||||
|
|
||||||
post("color", this.element[0].id, this.color);
|
post("color", this.element[0].id, this.color);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
($(".colorPickSelector").each(function() {
|
($(".toggle").each(function() {
|
||||||
console.log($( this )[0].attributes.getNamedItem("style"));
|
var toggle = true;
|
||||||
|
console.log($(this).attr('toggled'));
|
||||||
|
if($(this).attr('toggled') == "True") {
|
||||||
|
toggle = false;
|
||||||
|
} else if ($(this).attr('toggled') == "False") {
|
||||||
|
toggle = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this).prop('checked', toggle);
|
||||||
|
}));
|
||||||
|
|
||||||
|
($(".colorPickSelector").each(function() {
|
||||||
|
// console.log($( this )[0].attributes.getNamedItem("style"));
|
||||||
var color = $( this )[0].attributes.getNamedItem("defaultcolor").nodeValue;
|
var color = $( this )[0].attributes.getNamedItem("defaultcolor").nodeValue;
|
||||||
var style = document.createAttribute("style");
|
var style = document.createAttribute("style");
|
||||||
style.value = 'background-color: ' + color + '; color: ' + color + ';';
|
style.value = 'background-color: ' + color + '; color: ' + color + ';';
|
||||||
$( this )[0].attributes.setNamedItem(style);
|
$( this )[0].attributes.setNamedItem(style);
|
||||||
|
|
||||||
console.log($( this )[0].attributes.getNamedItem("style"));
|
// console.log($( this )[0].attributes.getNamedItem("style"));
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function post(type, id, data) {
|
function post(type, id, data) {
|
||||||
var url = "https://192.168.68.103.xip.io:1234/calendar";
|
var url = "calendar";
|
||||||
var method = "POST";
|
var method = "POST";
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "color":
|
case "color":
|
||||||
@ -95,26 +117,15 @@
|
|||||||
|
|
||||||
function toggleReaction(self) {
|
function toggleReaction(self) {
|
||||||
// the slider used defaults to inverted information [#01]
|
// the slider used defaults to inverted information [#01]
|
||||||
post("toggle", self.id, !self.previousElementSibling.checked);
|
var data;
|
||||||
|
if(self.checked) {
|
||||||
/*console.log(self.id);
|
data = "False";
|
||||||
var url = "https://192.168.68.103.xip.io:1234/calendar";
|
} else {
|
||||||
var method = "POST";
|
data = "True";
|
||||||
var postData = JSON.stringify({"calendar_id": self.id.toString() });
|
|
||||||
console.log(postData);
|
|
||||||
var shouldBeAsync = true;
|
|
||||||
|
|
||||||
var request = new XMLHttpRequest();
|
|
||||||
request.onload = function () {
|
|
||||||
var status = request.status;
|
|
||||||
var data = request.responseText;
|
|
||||||
}
|
}
|
||||||
|
post("toggle", self.id, data);
|
||||||
request.open(method, url, shouldBeAsync);
|
|
||||||
// content type json makes app do error 400
|
|
||||||
request.setRequestHeader("Content-Type", "application/json");
|
|
||||||
request.send(postData);*/
|
|
||||||
}
|
}
|
||||||
|
init = true;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
46
server/template/devices.html
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{% extends "sidebar.html" %}
|
||||||
|
{% block body%}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div style="font-weight: bold;width: 15rem; margin: 1rem">Device ID</div>
|
||||||
|
<div style="font-weight: bold;width: 10rem; margin: 1rem; padding-right: 4rem">Link Status</div>
|
||||||
|
<div style="font-weight: bold;width: 4rem; margin: 1rem">Action</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for item in devices %}
|
||||||
|
<div class="container">
|
||||||
|
<!--device id-->
|
||||||
|
<div style="width: 15rem; margin: 1rem;">{{ item.deviceName }}</div>
|
||||||
|
|
||||||
|
<!--link status-->
|
||||||
|
<div style="width: 10rem; margin: 1rem; padding-right: 4rem">
|
||||||
|
{% if item.connection %}
|
||||||
|
Connected
|
||||||
|
{% else %}
|
||||||
|
Not Connected
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--action button-->
|
||||||
|
<div style="width: 4rem; margin: 1rem;">
|
||||||
|
<form action="" method="post">
|
||||||
|
<input type="hidden" name="device" value={{ item.deviceName }}>
|
||||||
|
<input type="submit" name="submit" value="Unlink">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<form action="" method="post">
|
||||||
|
<div class="container grey" style="margin-top: 3rem;">
|
||||||
|
<div>{{ form.hidden_tag() }}</div>
|
||||||
|
<div style="margin: 1rem">{{ form.deviceName.label }}</div>
|
||||||
|
<div style="margin: 1rem">{{ form.deviceName(size=24) }}</div>
|
||||||
|
<div style="with: 8rem; margin: 1rem">{{ form.submit() }}</div>
|
||||||
|
{% for error in form.deviceName.errors %}
|
||||||
|
<span style="color: red;">[{{ error }}]</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
19
server/template/emaillogin.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Sign In</h1>
|
||||||
|
<form action="" method="post" novalidate>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<p>
|
||||||
|
{{ form.username.label }}<br>
|
||||||
|
{{ form.username(size=32) }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ form.password.label }}<br>
|
||||||
|
{{ form.password(size=32) }}
|
||||||
|
</p>
|
||||||
|
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
|
||||||
|
<p>{{ form.submit() }}</p>
|
||||||
|
</form>
|
||||||
|
<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>
|
||||||
|
{% endblock %}
|
9
server/template/footer.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
<div id="footer">
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>made by Raphael Maenle </p>
|
||||||
|
<p><a href="mailto:raphael@maenle.net">raphael@maenle.net</a></p>
|
||||||
|
<p><a href="/privacy">privacy policy</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,16 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
{% block content %}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/css/main.css">
|
|
||||||
<title>Index</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
{% include "base_navigation.html" %}
|
||||||
<h1 style="color: blue">Login Page</h1>
|
|
||||||
|
<div class="banner">
|
||||||
|
<h1 class="title">Longitude</h1>
|
||||||
|
<h5> A calendar watchface</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!--Google Login-->
|
<!--Google Login-->
|
||||||
<div class="center-align">
|
<div class="center-align">
|
||||||
@ -25,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--Email Login-->
|
<!--Email Login-->
|
||||||
<div class="col s12 m6 offset-m3 center-align" style=" margin: 5px;">
|
<div class="col s12 m6 offset-m3 center-align" style=" margin: 2rem;">
|
||||||
<a class="oauth-container btn darken-4 white black-text" href="/login/email" style="text-transform:none">
|
<a class="oauth-container btn darken-4 white black-text" href="/login/email" style="text-transform:none">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<img width="20px" style="margin-top:7px; margin-right:8px" alt="E-mail sign-in"
|
<img width="20px" style="margin-top:7px; margin-right:8px" alt="E-mail sign-in"
|
||||||
@ -41,6 +38,5 @@
|
|||||||
|
|
||||||
<!-- Compiled and minified JavaScript -->
|
<!-- Compiled and minified JavaScript -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
{% endblock %}
|
52
server/template/privacy.html
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="banner">
|
||||||
|
<h1 class="title">Privacy</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-left:10rem">Summary</h3>
|
||||||
|
<div style="margin-left:10rem; margin-right:10rem;">This Privacy Statement descibes how Longitude handles your data and how the developer makes sure, that the users information remains as secure as possible.
|
||||||
|
This application does not share any user information with third parties and takes care to only save the minimum amount of information about the user.
|
||||||
|
The following chapters cover all essential points of interest about which information is saved and when it is removed from the server.
|
||||||
|
If you have any further questions or suggestions, please email us at <a href="mailto:raphael@maenle.net">raphael@maenle.net</a>.</div>
|
||||||
|
|
||||||
|
|
||||||
|
<h3 style="margin-left:10rem">What Information is saved?</h3>
|
||||||
|
<div style="margin-left:10rem; margin-right:10rem;">
|
||||||
|
Longitude Calendar saves as little information about their users as possible. The application handles sensitive information only when directly prompted by the user or a device associated with the user. The service only provides this information to the user or a device associated with the user. The data saved in the Longidute Databas is
|
||||||
|
<ul>
|
||||||
|
<li>Username and hashed password or alternatively</li>
|
||||||
|
<li>Google Username and Id with Google Login Token</li>
|
||||||
|
<li>Profile Picture</li>
|
||||||
|
<li>Email</li>
|
||||||
|
<li>Google Calendar read Token connected with this service</li>
|
||||||
|
<li>Names and Ids of calendars as well as color preferences</li>
|
||||||
|
<li>Device Fingerprints</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
All this information is erased as soon as the user deletes his account. Further, this information can be exported for the user to view if he so requests via email.
|
||||||
|
</div>
|
||||||
|
<h3 style="margin-left:10rem">How do you handle calendar information?</h3>
|
||||||
|
<div style="margin-left:10rem; margin-right:10rem;">
|
||||||
|
As previously stated, Longitude does not save calendar event information. Instead, any user or device request dynamically pulls only the neccessary information and
|
||||||
|
generates the response. The information is then immediately discarded.
|
||||||
|
</div>
|
||||||
|
<h3 style="margin-left:10rem">Are there any Cookies, and what does your javascript do?</h3>
|
||||||
|
<div style="margin-left:10rem; margin-right:10rem;">
|
||||||
|
longitudecalendar.com saves a session cookie on your device while you are on the website.
|
||||||
|
Javascript is used to send data to the server and is necessary for the color picker.
|
||||||
|
</div>
|
||||||
|
<h3 style="margin-left:10rem">Will there be Changes to these Policies?</h3>
|
||||||
|
<div style="margin-left:10rem; margin-right:10rem;">
|
||||||
|
This Privacy Policy statement may be upated at any time, if any material changes are made, the users of this service
|
||||||
|
will be notified in advance through the email provided with the creation of their user account. If a user continues to
|
||||||
|
use the service after changes in the privacy policy are in effect, he or she thereby agrees to the policy revisions.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-left:10rem">What do I do if I have further questions?</h3>
|
||||||
|
<div style="margin-left:10rem; margin-right:10rem;">
|
||||||
|
If you have any further questions about this policy, please do not hesitate to contact the developer of this service.
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
62
server/template/register.html
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% include "base_navigation.html" %}
|
||||||
|
|
||||||
|
<!--Google Login-->
|
||||||
|
<div class="center-align">
|
||||||
|
<div class="col s12 m6 offset-m3 center-align" style=" margin: 5px;">
|
||||||
|
<a class="oauth-container btn darken-4 white black-text" href="/login/google" style="text-transform:none">
|
||||||
|
<div class="left">
|
||||||
|
<img width="20px" style="margin-top:7px; margin-right:8px" alt="Google sign-in"
|
||||||
|
src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/512px-Google_%22G%22_Logo.svg.png" />
|
||||||
|
</div class="login_google">
|
||||||
|
<div class="login_google">Register with Google</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class=login_email>Register with Email</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<form action="" method="post">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<p>
|
||||||
|
{{ form.username.label }}<br>
|
||||||
|
{{ form.username(size=32) }}<br>
|
||||||
|
{% for error in form.username.errors %}
|
||||||
|
<span style="color: red;">[{{ error }}]</span>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ form.email.label }}<br>
|
||||||
|
{{ form.email(size=64) }}<br>
|
||||||
|
{% for error in form.email.errors %}
|
||||||
|
<span style="color: red;">[{{ error }}]</span>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ form.password.label }}<br>
|
||||||
|
{{ form.password(size=32) }}<br>
|
||||||
|
{% for error in form.password.errors %}
|
||||||
|
<span style="color: red;">[{{ error }}]</span>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ form.password2.label }}<br>
|
||||||
|
{{ form.password2(size=32) }}<br>
|
||||||
|
{% for error in form.password2.errors %}
|
||||||
|
<span style="color: red;">[{{ error }}]</span>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
<p>{{ form.submit() }}</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- Compiled and minified CSS -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/css/materialize.min.css">
|
||||||
|
|
||||||
|
<!-- Compiled and minified JavaScript -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
|
||||||
|
{% endblock %}
|
48
server/template/sidebar.html
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<link rel="shortcut icon" type="image/jpg" href="/static/res/favicon.ico"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/css/main.css">
|
||||||
|
<script src="static/js/jquery-3.5.0.min.js"></script>
|
||||||
|
<script type="text/javascript" src="static/js/index.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
|
<link rel="stylesheet" href="static/css/colorPick.css">
|
||||||
|
<!-- OPTIONAL DARK THEME -->
|
||||||
|
<link rel="stylesheet" href="static/css/colorPick.dark.theme.css">
|
||||||
|
<script src="static/js/colorPick.js"></script>
|
||||||
|
|
||||||
|
<title>Logitude</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="container">
|
||||||
|
<!-- Side navigation -->
|
||||||
|
<div class="navigation" id="navigation">
|
||||||
|
<a href="/view">View</a>
|
||||||
|
<a href="/calendar">Calendar</a>
|
||||||
|
<a href="/account">Account</a>
|
||||||
|
<a href="/devices">Devices</a>
|
||||||
|
<a href="javascript:void(0);" class="icon" onclick="menuBars()">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page content -->
|
||||||
|
<div id="main">
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
// content here
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% include "footer.html" %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
</html>
|
46
server/template/startpage.html
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% include "base_navigation.html" %}
|
||||||
|
|
||||||
|
<div class="banner">
|
||||||
|
<div class="title primeblue">Longitude Calendar</div>
|
||||||
|
<h4 class="subtitle">your day on your wrist</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grayblock horizontal">
|
||||||
|
<div class="content padded">
|
||||||
|
<div style='margin: 1rem'>
|
||||||
|
Connect your <img src='/static/res/googlelogo.png' style='height: 2.2rem; vertical-align:middle; padding-Bottom: 0.1rem'/> Calendar...
|
||||||
|
</div>
|
||||||
|
<img class="image" src='/static/res/calendar.svg'/>
|
||||||
|
</div>
|
||||||
|
<div class="content padded">
|
||||||
|
<div style='margin: 1rem'>
|
||||||
|
...with your <img src='/static/res/tizenlogo.png' style='height: 2rem; vertical-align:middle; padding-Bottom:0.3rem;'/> Watchface
|
||||||
|
</div>
|
||||||
|
<img class="image" src='/static/res/watchface.svg'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="whiteblock vertical">
|
||||||
|
<div class="content">
|
||||||
|
<img class="image" src='/static/res/connect_calendar.svg'/>
|
||||||
|
<div class="text">1, Connect your Calendar Service to Longitude</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<img class="image" src='/static/res/connect_device.svg'/>
|
||||||
|
<div class="text">2, Connect the Longitude Watch app to Longitude Web</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<img class="image" src='/static/res/personalize_view.svg'/>
|
||||||
|
<div class="text">3, Customize your Content and Style</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Compiled and minified CSS -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/css/materialize.min.css">
|
||||||
|
|
||||||
|
<!-- Compiled and minified JavaScript -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
|
||||||
|
|
||||||
|
{% endblock %}
|
43
server/template/view.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{% extends "sidebar.html" %}
|
||||||
|
{% block body%}
|
||||||
|
<div class="container profile">
|
||||||
|
<p class="name">Sun View</p>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<i class="fa fa-arrow-left" style="font-size: 4rem; color: #ddd; margin-bottom: 3rem"></i>
|
||||||
|
<img class="preview" src="/static/res/sunview.png" alt="Profile Picture"></img>
|
||||||
|
<i class="fa fa-arrow-right" style="font-size: 4rem; color: #ddd; margin-bottom: 3rem"></i>
|
||||||
|
</div>
|
||||||
|
<div class="sub container">
|
||||||
|
<p class=name>Sun</p>
|
||||||
|
<div style="flex-grow: 1"></div>
|
||||||
|
<label class="switch">
|
||||||
|
<input class="toggle" id=Sun type="checkbox" toggled="True" >
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="sub container">
|
||||||
|
<p class=name>Planet</p>
|
||||||
|
<div style="flex-grow: 1"></div>
|
||||||
|
<label class="switch">
|
||||||
|
<input class="toggle" id=Sun type="checkbox" toggled="True" >
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="sub container">
|
||||||
|
<p class=name>Dwarf</p>
|
||||||
|
<div style="flex-grow: 1"></div>
|
||||||
|
<label class="switch">
|
||||||
|
<input class="toggle" id=Sun type="checkbox" toggled="True" >
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="sub container">
|
||||||
|
<p class=name>Calendar</p>
|
||||||
|
<div style="flex-grow: 1"></div>
|
||||||
|
<label class="switch">
|
||||||
|
<input class="toggle" id=Sun type="checkbox" toggled="True" >
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -1,8 +0,0 @@
|
|||||||
{% extends "sidebar.html" %}
|
|
||||||
{% block body%}
|
|
||||||
<p>Hello, {{ username }}! You're logged in! Email: {{email}}</p>
|
|
||||||
<div><p>Google Profile Picture:</p>
|
|
||||||
<img src={{ profile_img }} alt="Google profile pic"></img></div>
|
|
||||||
<a class="button" href="/logout">Logout</a>
|
|
||||||
<a class="button" href="/test">test API</a>
|
|
||||||
{% endblock %}
|
|
@ -1,43 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/css/main.css">
|
|
||||||
<script src="static/js/jquery-3.5.0.min.js"></script>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="static/css/colorPick.css">
|
|
||||||
<!-- OPTIONAL DARK THEME -->
|
|
||||||
<link rel="stylesheet" href="static/css/colorPick.dark.theme.css">
|
|
||||||
<script src="static/js/colorPick.js"></script>
|
|
||||||
|
|
||||||
<title>Index</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Side navigation -->
|
|
||||||
<div class="sidenav">
|
|
||||||
<a href="/view">View</a>
|
|
||||||
<a href="/calendar">Calendar</a>
|
|
||||||
<a href="/account">Account</a>
|
|
||||||
<a href="/devices">Devices</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Page content -->
|
|
||||||
<div class="main">
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
// content here
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</html>
|
|
13
wsgi.ini
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# uwsgi --socket 0.0.0.0:8084 -w wsgi:application --protocol=http
|
||||||
|
|
||||||
|
[uwsgi]
|
||||||
|
module = wsgi:application
|
||||||
|
protocol = http
|
||||||
|
master = true
|
||||||
|
processes = 5
|
||||||
|
|
||||||
|
socket = 0.0.0.0:8084
|
||||||
|
|
||||||
|
die-on-term = true
|
||||||
|
|
||||||
|
|