Compare commits

..

59 Commits

Author SHA1 Message Date
ec4dfd1940 further updates for mobile experience 2020-08-26 23:02:46 +02:00
a83c28f873 updates calendar webpage flex container 2020-08-26 13:44:01 +02:00
2e81d53f9d fixes typo, adds flex divs for improved mobile view 2020-08-18 21:04:09 +02:00
f3ab6834fc fixes typo 2020-08-17 17:09:56 +02:00
e5df7c3cd6 adds empty main div to correct footer position 2020-08-17 14:32:05 +02:00
081888d1f6 uwsgi initialization uses the --lazy flag, to prevent connection overload
as described in https://serverfault.com/questions/407612/error-2006-mysql-server-has-gone-away
the uwsgi server usually forks its workers from the parent, which already has a connection
open to the mysql database. So the new child typically inherits the same connection.
Should both workers use the same connection at the same time, the server connection throws a
'2006, server has gone away error'.
The flag --lazy creates fresh forks for each child, each creating a new connection.
This prevents the overload and after testing, the '2006' error seems to not be reproduceable.
2020-08-17 13:21:33 +02:00
5391a4548b only shows remove calendar button on ical calendar 2020-08-02 00:10:14 +02:00
c3dcccb479 reloads page after successful form transmition in routes 2020-07-25 23:44:49 +02:00
5ec5ba488d adds remove button to all calendars 2020-07-25 19:45:37 +02:00
9ecaf0211f timezone fix to always use calendar timezone for event description 2020-07-25 19:28:30 +02:00
1f48689a32 Merge branch 'master' of git.maenle.net:raphael/calendarwatch_frontend 2020-07-25 18:10:04 +02:00
752c7c5577 adds ical input to calendar html templates and implements data handling
- the icalHandler in the backend is used in two instances. First, when
  the user adds a new ical calendar into the database, routes.py passes
  the information (name and url) to the ical Handler. The calendars are
  saved in the database as type 'ical'.

  Then, when a connected device pulls current events, all the calendars
  which are of type 'ical' are pulled and parsed for current events.
  See the backend commit for details on what happens there.

- All google Calendar related functions now have an additional check,
  to make sure that the calendar they are working on is of type 'Google'.
2020-07-25 18:05:55 +02:00
04c5410c41 migration scripts to newest database 2020-07-25 11:41:12 +02:00
524d2f1e1d adds calendar_type to calendar model and to calendar handling 2020-07-25 11:28:59 +02:00
2add28fa00 adds ical form and form visualization in calendar.html 2020-07-25 10:50:02 +02:00
38e16f92e8 device receives a timestamp at creation
no new devices are ever without a lastConnection timestamp
2020-07-17 10:42:27 +02:00
0284eb2fa8 adds cleanup functionality for old devices
- device now last field 'last connection' which gets updated
  at every server connection from the respective device
- manual cleanup script in database deletes all devices which
  have never been updated, or have a timestamp older than 30 days
2020-07-17 10:39:16 +02:00
15e68b88e8 adds uwsgi support instead of using flask dev server
- uses existing wsgi.py file

- adds wsgi.ini file
  - sets socket
  - defines application to be run, which it gets from wsgi.py file
  - uses http communication to nginx server
  - some random number of workers

- updates Dockerfile and docker-entrypoint
2020-07-10 11:47:49 +02:00
adb20dea14 code cleanup and comments 2020-07-10 10:55:01 +02:00
016e52f1e7 updates images, changes step description 2020-06-11 13:55:02 +02:00
6374c8d983 fixes routing bug and provides temporary fix for css overlap 2020-06-06 23:49:39 +02:00
39b899283c adds a landing page which gives an overview of the tool
- landing page shows two images of google calendar and tizen watch
  and how the calendars corelate
- routes redesigned to fit this landing page
- redesign of coloring in css
2020-06-06 20:02:18 +02:00
0f47ff15dd fixes bug where color hex values where not passed along (variable mixup) 2020-05-30 23:33:55 +02:00
70197ee393 Large changes in the seperation of backend and google handler
- backend now takes care of all the recoloring, and communication with database
- google handler takes care of the entire communication with google
- colors selected on the front-end are now translated to the watch

- Calendars in the database now directly save the color the user has set
- only if the event has a different color than the calendar (event color from google is not 0)
  is the event color from google used.
- No more passing around of google color ids, hex colors all the way
2020-05-30 23:05:46 +02:00
98b09bb778 update footer css 2020-05-29 20:32:44 +02:00
36c9b5015f adds privacy policy and page footer 2020-05-29 20:30:37 +02:00
87dedb8e02 adds device generation and connection
- fingerprint generated first time device connects
- saved to database and served to device
- connection status set to true, once device requests first package
- user can link device via online form
2020-05-28 15:43:51 +02:00
7b82086ff0 adds device handling functionality from the browser 2020-05-28 12:00:05 +02:00
5d1edbc6fc fixes account deletion without google user 2020-05-28 00:05:53 +02:00
722db5feae delete now uses orphan cascade deletion instead of manual delete 2020-05-27 20:56:23 +02:00
120931dc4c updated removal process for database elements 2020-05-27 20:45:19 +02:00
d17a76f4b8 fixes bugs in database calles 2020-05-27 20:36:05 +02:00
355ba99ca3 updates database design for mariadb 2020-05-27 20:06:43 +02:00
0cfc801f59 moves models to database folder 2020-05-24 13:33:48 +02:00
c9cbb53eea routing updated to be more variable and on demand
- calendar json file now generated on demand at download request
- device fingerprint route now added
2020-05-24 13:26:41 +02:00
98a78f2102 adds delete account function 2020-05-22 10:47:28 +02:00
cf9c4f0e85 adds minor login design update and device page update 2020-05-22 00:10:16 +02:00
3c6d950bbc adds device page and form for new device
- form added to push new device id to backend
- device added to db model (needs to be pushed still)
- form return right now just prints
- design for device list created, still needs some updates
2020-05-21 18:33:58 +02:00
46eece9b98 adds view mode design 2020-05-21 15:38:22 +02:00
b0f4e98513 updates account design 2020-05-21 11:37:52 +02:00
14670ae871 updated backend 2020-05-21 10:19:59 +02:00
934c4f2a1d adds favicon, changes menu 2020-05-21 10:18:40 +02:00
4a8ac52201 backend now uses calendar_id and this is the frontend upgrade
- calendar_id is unique and therefore creates less problems
- changing id of DOM from item.name to calendar_id
2020-05-18 23:52:18 +02:00
ee54dd5daa updates color generation, tries fixing credentials bug
- credentials still lost the refresh token after a certain event
  - this was now fixed by completely removing any saves past the first one
  - https://trello.com/c/iORYLYHl/44-1-check-on-credentials

- Color Coding is now updated
  - fixed an incorrect color mapping in the colors.json
  - updates some calls to functions to correctly get the natural colors
  - the incorrect red -> yellow mapping might have to do with the previous credentials error
2020-05-17 22:59:12 +02:00
1b7980e834 adds routines.python script 2020-05-15 16:05:45 +02:00
999ae069da db update 2020-05-15 16:01:38 +02:00
2ecb5a4b71 updates routines script and google certification handling 2020-05-15 16:00:54 +02:00
5e7080695d Merge branch 'master' into venv 2020-05-11 23:06:32 +02:00
ae773daad7 modifies entrypoint back to flask 2020-05-11 23:05:41 +02:00
a4265a44f5 adds static files to server 2020-05-11 21:00:00 +00:00
dbb6d170da adds structure for virtual environment uwsgi and http communication
- starts uwsgi and flask in virtual environment instead of docker
- sets oauth2 https error to false (OAUTHLIB_INSECURE_TRANSPORT=1)

- this setup works, but
  - moving it into docker
  -  removing the venv
  -  and allowing https connection internally?
would be better
2020-05-10 20:31:42 +02:00
76a8b97ae4 Merge branch 'master' of git.maenle.net:raphael/calendarwatch_frontend 2020-05-09 07:46:59 +00:00
908d64e0a9 hotfixes removing backend update script 2020-05-09 07:44:15 +00:00
02625299c7 adds backend as submodule 2020-05-08 13:21:42 +02:00
7952ff2c12 upates backend which now includes the calendar json file update script 2020-04-24 18:27:27 +00:00
c89ecd7134 adds backend script which can be run as a cronjob every n minutes to generate new json files for google calendars
- database updated to save google credentials
- database updated to save json calendar information
- json still saved as a json file under userinfo/<user.id>/calendarevents.json
2020-04-24 17:54:56 +00:00
a071193959 adds email login and registration forms
- using flask-wtf forms to create login and registration
- saves and compares input data with database
- generates user if need-be
- same user form as google user
2020-04-23 17:11:23 +00:00
f156d38739 adds more advanced database handling unsing sql alchemy
- moves app into package
- adds sql alchemy equipment
- moves templates into server package
- add app.db sqlite file
2020-04-22 20:15:21 +00:00
8f20be53e1 fixes communication conflicts between frontend and database through backend;
sets up visualization of DOMs in frontend through javascript
2020-04-17 16:54:35 +00:00
64 changed files with 2844 additions and 600 deletions

1
.gitignore vendored
View File

@ -146,4 +146,3 @@ cython_debug/
# static files generated from Django application using `collectstatic`
media
static

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "backend"]
path = backend
url = git@git.maenle.net:raphael/calenderwatch_server.git
url = git@git.maenle.net:raphael/calendarwatch_backend

View File

@ -1,4 +1,15 @@
### summary:
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
View File

@ -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)

Submodule backend updated: fed3fddb21...f939127a0c

7
config.py Normal file
View 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

14
database/__init__.py Normal file
View File

@ -0,0 +1,14 @@
from server import db
import time
from database.models import Device
def cleanDevices():
allDevs = db.session.query(Device)
devices = allDevs.filter(Device.lastConnection <= int(round(time.time())) - 60*60*24*30).all()
devices += allDevs.filter(Device.lastConnection == None)
for device in devices:
print(device.deviceName)
db.session.delete(device)
db.session.commit()

View File

@ -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)

View File

@ -0,0 +1 @@
Generic single-database configuration.

View 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

View 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()

View 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"}

View 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 ###

View File

@ -0,0 +1,30 @@
"""empty message
Revision ID: 9882522aafa9
Revises: e5ef5e4a807b
Create Date: 2020-07-25 09:34:07.987380
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9882522aafa9'
down_revision = 'e5ef5e4a807b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('calendar', sa.Column('calendar_type', sa.String(length=32), nullable=True))
op.create_index(op.f('ix_calendar_calendar_type'), 'calendar', ['calendar_type'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_calendar_calendar_type'), table_name='calendar')
op.drop_column('calendar', 'calendar_type')
# ### end Alembic commands ###

View 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 ###

View 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 ###

73
database/models.py Normal file
View File

@ -0,0 +1,73 @@
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)
lastConnection = db.Column(db.BigInteger)
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)
calendar_type = db.Column(db.String(32), index=True)
name = db.Column(db.String(256), index=True)
toggle = db.Column(db.String(8))
color = db.Column(db.String(16))

View File

@ -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
);

Binary file not shown.

View File

@ -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()

View File

@ -0,0 +1,11 @@
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 ics
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"]
# CMD tail -f /dev/null

View File

@ -0,0 +1,17 @@
#!/bin/sh
cd /home/calendarwatch
# use flasks own uwsgi server for debugging:
# export FLASK_APP=/home/calendarwatch/server.py
# python3 server.py
# the --lazy flag forks() a new instance of the server
# instead of forking from the parent and copying the same mysql
# connection. If you don't do that, then multiple forks will use
# the same connection at the same time, causing the server to throw
# a 'connection has gone away' error.
# more here: https://serverfault.com/questions/407612/error-2006-mysql-server-has-gone-away
uwsgi --ini wsgi.ini --lazy
echo "server has been started"

View 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
View 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
View 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

58
server/forms.py Normal file
View File

@ -0,0 +1,58 @@
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')
class CalendarForm(FlaskForm):
iCalURL = StringField('New ical URL', validators=[DataRequired()])
calName = StringField('Calendar Name', validators=[DataRequired()])
submit = SubmitField('Add URL')
def validate_iCalURL (self, iCalURL):
return None

218
server/googleHandler.py Normal file
View File

@ -0,0 +1,218 @@
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):
service = None
if user.google_token is not None:
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" and
calendar.calendar_type == "Google" and
service != None):
event_result = service.events().list(calendarId=calendar.calendar_id,
timeMin=startDate.isoformat(),
timeMax=endDate.isoformat(),
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)
if service != None:
colors = service.colors().get().execute()
else:
colors = None
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'],
calType="Google",
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}

294
server/routes.py Normal file
View File

@ -0,0 +1,294 @@
# Python standard libraries
import json
import os
import time
# 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 backend.icalHandler as ical
import server.googleHandler as google
from server import login_manager, app, db
from server.forms import LoginForm, RegistrationForm, DeviceForm, CalendarForm
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", methods=['GET', 'POST', 'DELETE'])
def calendar():
if not current_user.is_authenticated:
return flask.render_template('login.html')
calendars = backend.calendarsFromDb(current_user)
form = CalendarForm()
if request.method == 'POST':
if request.form.get("submit") == "Remove":
calendar = db.session.query(Calendar).filter(Calendar.calendar_id==request.form.get("calendar")).first()
db.session.delete(calendar)
db.session.commit()
return flask.redirect(url_for('calendar'))
elif form.validate_on_submit():
ical.icalToCalendarDb(form.iCalURL.data, form.calName.data, current_user)
return flask.redirect(url_for('calendar'))
# otherwise it is a javascript POST
else:
try:
calId = request.json.get('calendar_id')
color = request.json.get('color', None)
toggle = request.json.get('toggle', None)
except:
return flask.render_template('calendar.html', calendars=calendars, form=form)
if color != None:
current_user.updateCalendar(calId, color=color)
if toggle != None:
current_user.updateCalendar(calId, toggle=toggle)
# toggle specific calendar of user
try:
calId = request.json.get('calendar_id')
color = request.json.get('color', None)
toggle = request.json.get('toggle', None)
except:
return flask.render_template('calendar.html', calendars=calendars, form=form)
if color != None:
current_user.updateCalendar(calId, color=color)
if toggle != None:
current_user.updateCalendar(calId, toggle=toggle)
# toggle specific calendar of user
return flask.render_template('calendar.html', calendars=calendars, form=form)
# POST
@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.lastConnection=int(round(time.time()))
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)
events.extend(ical.fetchCalendarEvents(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)
device.lastConnection = int(round(time.time()))
db.session.add(device)
db.session.commit()
break;
# Send to Device
return jsonify(deviceName=fingerprint)

View 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); }

View 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
View 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}

380
server/static/css/main.css Normal file
View File

@ -0,0 +1,380 @@
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: 0rem;
}
.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 {
max-width: 90%;
width: 25rem;
}
.vertical .content .text {
max-width: 90%;
margin: auto;
width: 26rem;
}
.horizontal .content {
font-size: 2rem;
text-align: center;
}
.horizontal .image {
margin: 1rem;
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: block;
}
.navigation_rightside {
margin-left: auto;
}
.navigation a {
float: left;
display: block;
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: flex;
flex-wrap: wrap;
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 1rem 1rem 1rem;
color: black;
text-decoration: none;
}
.container .preview {
width: 20rem;
height: auto;
margin: 1rem 3rem 4rem 3rem;
max-width: 100%;
}
.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: 40%;
justify-content: left;
display: flex;
}
.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;
text-align: center;
}
.grey {
background-color: #ddd;
}
.sub.container .name {
margin-right: 1rem;
}
.sub.container .data {
background: #ddd;
padding: 5px;
border-radius: 5px;
margin-right: 1rem;
}
@media (max-width:800px) {
.sub.container {
justify-content: center;
}
}

View 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
View 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);

View 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

File diff suppressed because one or more lines are too long

12
server/static/res/arrow.svg Executable file
View 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
View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

61
server/static/res/watchface.svg Executable file
View 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

View 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
View 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>

View 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>

View File

@ -0,0 +1,161 @@
{% extends "sidebar.html" %}
{% block body%}
<div class="container">
<div style="width: 4rem;margin:1rem;"></div>
<div style="width: 10rem; margin: 1rem; font-weight: bold">Calendar</div>
<div style="display: inline-flex">
<div style="width: 5rem; margin: 1rem; padding-right: 1rem;font-weight: bold">Show on device</div>
<div style="width: 2rem; margin: 1rem;font-weight: bold">Color</div>
</div>
</div>
{% for item in calendars %}
<div class="container" style="margin-top: 1.5rem">
<!--action button-->
{% if "ical" == item.calType %}
<div style="width: 4rem; margin: 1rem;">
<form action="" method="post">
<input type="hidden" name="calendar" value={{ item.calendarId }}>
<input type="submit" name="submit" value="Remove">
</form>
</div>
{% else %}
<div style="width: 4rem; margin: 1rem;"></div>
{% endif %}
<!--Name-->
<div style="width: 10rem; margin-left: 1rem; margin-right: 1rem; margin-top: 0.5rem">{{ item.name }}</div>
<div style="display: inline-flex">
<!--Toggle-->
<div style="width: 5rem; margin-left: 1rem; margin-right: 1rem; margin-top: 0.5rem; padding-right: 1rem">
<!-- Rounded switch -->
<label class="switch">
<input class="toggle" id={{item.calendarId}} type="checkbox" toggled={{item.toggle}} onclick="toggleReaction(this)">
<span class="slider on round"></span>
</label>
</div>
<!--Color Selector-->
<div style="width: 2rem; margin-left: 1rem; margin-right: 1rem; margin-top: 0.5rem">
<div class="colorPickSelector" id={{item.calendarId}} defaultColor={{item.color}}></div>
<!--svg height="20" width="20">
<circle cx="10" cy="10" r="10" stroke="black" stroke-width="0" fill={{ item.color }} />
</svg-->
</div>
</div>
</div>
{% endfor %}
<div id=calendars class="container">
</div>
<form action="" method="post">
<div class="container grey" style="margin-top: 3rem;">
<div>{{ form.hidden_tag() }}</div>
<div style="display: flex">
<div style="margin: 1rem">{{ form.calName.label }}</div>
<div style="margin: 1rem">{{ form.calName(size=24) }}</div>
</div>
<div style="display: flex">
<div style="margin: 1rem">{{ form.iCalURL.label }}</div>
<div style="margin: 1rem">{{ form.iCalURL(size=24) }}</div>
</div>
<div style="with: 8rem; margin: 1rem">{{ form.submit() }}</div>
{% for error in form.iCalURL.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</div>
</form>
<!--div class="container">
<a class="button addcalendar" href="/login/google" style="width: auto; margin: 4rem">Add Calendar</a>
</div-->
<script type="text/javascript">
var init = false;
// initialize all DOM items
$(".colorPickSelector").colorPick({
'initialColor': '#3498db',
'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() {
if(!init) {
return;
}
// Todo getting the element id is currently done over [0] [#02]
this.element.css({'backgroundColor': this.color, 'color': this.color});
post("color", this.element[0].id, this.color);
}
});
($(".toggle").each(function() {
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 style = document.createAttribute("style");
style.value = 'background-color: ' + color + '; color: ' + color + ';';
$( this )[0].attributes.setNamedItem(style);
// console.log($( this )[0].attributes.getNamedItem("style"));
}));
function post(type, id, data) {
var url = "calendar";
var method = "POST";
switch (type) {
case "color":
var postData = JSON.stringify({"calendar_id": id.toString(), "color": data.toString()});
break;
case "toggle":
var postData = JSON.stringify({"calendar_id": id.toString(), "toggle": data.toString()})
break;
default:
break;
}
console.log(postData);
var shouldBeAsync = true;
var request = new XMLHttpRequest();
request.onload = function () {
var status = request.status;
var data = request.responseText;
}
request.open(method, url, shouldBeAsync);
// content type json makes app do error 400
request.setRequestHeader("Content-Type", "application/json");
request.send(postData);
}
function toggleReaction(self) {
// the slider used defaults to inverted information [#01]
var data;
if(self.checked) {
data = "False";
} else {
data = "True";
}
post("toggle", self.id, data);
}
init = true;
</script>
{% endblock %}

View 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 %}

View 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 %}

View File

@ -0,0 +1,9 @@
<div id="footer">
<div class="footer container">
<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>

View File

@ -1,16 +1,13 @@
<!DOCTYPE html>
<html lang="en">
{% extends "base.html" %}
<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">
<title>Index</title>
</head>
{% block content %}
<body>
<h1 style="color: blue">Login Page</h1>
{% include "base_navigation.html" %}
<div class="banner">
<h1 class="title">Longitude</h1>
<h5> A calendar watchface</h5>
</div>
<!--Google Login-->
<div class="center-align">
@ -25,7 +22,7 @@
</div>
<!--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">
<div class="left">
<img width="20px" style="margin-top:7px; margin-right:8px" alt="E-mail sign-in"
@ -41,6 +38,5 @@
<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
</body>
</html>
{% endblock %}

View 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 describes how Longitude handles your data and how the developer makes sure, that the user's 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 Database 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 %}

View 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 %}

View File

@ -0,0 +1,50 @@
<!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>
<div id="main">
</div>
{% include "footer.html" %}
</div>
</body>
</html>

View File

@ -0,0 +1,48 @@
{% 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: 3.2rem; vertical-align:middle; padding-Bottom: 0.1rem'/> Calendar...-->
Connect your 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-->
..with your Tizen 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
View 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 %}

View File

@ -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 %}

View File

@ -1,120 +0,0 @@
{% extends "sidebar.html" %}
{% block body%}
<div style="height: 50px">
<div style="width: 30%; float: left; margin-left: 10%">Calendar</div>
<div style="width: 30%; float: left">Show on device</div>
<div style="width: 30%; float: left">Color</div>
</div>
<div>
{% for item in calendars %}
<div style="height: 30px">
<!--Name-->
<div style="width: 30%; float: left; font-size: 10px; text-align: left; margin-left: 10%">{{ item.name }}</div>
<!--Toggle-->
<div style="width: 30%; float: left">
<!-- Rounded switch -->
<label class="switch">
<input type="checkbox">
<span id={{item.name}} class="slider round" onclick="toggleReaction(this)"></span>
</label>
</div>
<!--Color Selector-->
<div style="width: 30%; float: left">
<div class="colorPickSelector" id={{item.name}} defaultColor={{item.color}}></div>
<!--svg height="20" width="20">
<circle cx="10" cy="10" r="10" stroke="black" stroke-width="0" fill={{ item.color }} />
</svg-->
</div>
</div>
{% endfor %}
</div>
<script type="text/javascript">
$(".colorPickSelector").colorPick({
'initialColor': '#3498db',
'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() {
// Todo getting the element id is currently done over [0] [#02]
this.element.css({'backgroundColor': this.color, 'color': this.color});
post("color", this.element[0].id, this.color);
}
});
($(".colorPickSelector").each(function() {
console.log($( this )[0].attributes.getNamedItem("style"));
var color = $( this )[0].attributes.getNamedItem("defaultcolor").nodeValue;
var style = document.createAttribute("style");
style.value = 'background-color: ' + color + '; color: ' + color + ';';
$( this )[0].attributes.setNamedItem(style);
console.log($( this )[0].attributes.getNamedItem("style"));
}));
function post(type, id, data) {
var url = "https://192.168.68.103.xip.io:1234/calendar";
var method = "POST";
switch (type) {
case "color":
var postData = JSON.stringify({"calendar_id": id.toString(), "color": data.toString()});
break;
case "toggle":
var postData = JSON.stringify({"calendar_id": id.toString(), "toggle": data.toString()})
break;
default:
break;
}
console.log(postData);
var shouldBeAsync = true;
var request = new XMLHttpRequest();
request.onload = function () {
var status = request.status;
var data = request.responseText;
}
request.open(method, url, shouldBeAsync);
// content type json makes app do error 400
request.setRequestHeader("Content-Type", "application/json");
request.send(postData);
}
function toggleReaction(self) {
// the slider used defaults to inverted information [#01]
post("toggle", self.id, !self.previousElementSibling.checked);
/*console.log(self.id);
var url = "https://192.168.68.103.xip.io:1234/calendar";
var method = "POST";
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;
}
request.open(method, url, shouldBeAsync);
// content type json makes app do error 400
request.setRequestHeader("Content-Type", "application/json");
request.send(postData);*/
}
</script>
{% endblock %}

View File

@ -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
View 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

4
wsgi.py Normal file
View File

@ -0,0 +1,4 @@
from server import app as application
if __name__ == "__main__":
application.run()