Compare commits

..

37 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
84 changed files with 1516 additions and 1241 deletions

BIN
app.db

Binary file not shown.

Binary file not shown.

Submodule backend updated: 060e506547...f939127a0c

View File

@ -1,84 +0,0 @@
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly
if [ "${BASH_SOURCE-}" = "$0" ]; then
echo "You must source this script: \$ source $0" >&2
exit 33
fi
deactivate () {
unset -f pydoc >/dev/null 2>&1
# reset old environment variables
# ! [ -z ${VAR+_} ] returns true if VAR is declared at all
if ! [ -z "${_OLD_VIRTUAL_PATH:+_}" ] ; then
PATH="$_OLD_VIRTUAL_PATH"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then
PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ] ; then
hash -r 2>/dev/null
fi
if ! [ -z "${_OLD_VIRTUAL_PS1+_}" ] ; then
PS1="$_OLD_VIRTUAL_PS1"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
if [ ! "${1-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
VIRTUAL_ENV='/home/raphael/dev/git/calendarwatch_frontend/calendarwatch'
export VIRTUAL_ENV
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH
# unset PYTHONHOME if set
if ! [ -z "${PYTHONHOME+_}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1-}"
if [ "x" != x ] ; then
PS1="${PS1-}"
else
PS1="(`basename \"$VIRTUAL_ENV\"`) ${PS1-}"
fi
export PS1
fi
# Make sure to unalias pydoc if it's already there
alias pydoc 2>/dev/null >/dev/null && unalias pydoc || true
pydoc () {
python -m pydoc "$@"
}
# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ] ; then
hash -r 2>/dev/null
fi

View File

@ -1,55 +0,0 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
set newline='\
'
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH:q" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT:q" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate && unalias pydoc'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV '/home/raphael/dev/git/calendarwatch_frontend/calendarwatch'
set _OLD_VIRTUAL_PATH="$PATH:q"
setenv PATH "$VIRTUAL_ENV:q/bin:$PATH:q"
if ('' != "") then
set env_name = ''
else
set env_name = '('"$VIRTUAL_ENV:t:q"') '
endif
if ( $?VIRTUAL_ENV_DISABLE_PROMPT ) then
if ( $VIRTUAL_ENV_DISABLE_PROMPT == "" ) then
set do_prompt = "1"
else
set do_prompt = "0"
endif
else
set do_prompt = "1"
endif
if ( $do_prompt == "1" ) then
# Could be in a non-interactive environment,
# in which case, $prompt is undefined and we wouldn't
# care about the prompt anyway.
if ( $?prompt ) then
set _OLD_VIRTUAL_PROMPT="$prompt:q"
if ( "$prompt:q" =~ *"$newline:q"* ) then
:
else
set prompt = "$env_name:q$prompt:q"
endif
endif
endif
unset env_name
unset do_prompt
alias pydoc python -m pydoc
rehash

View File

@ -1,100 +0,0 @@
# This file must be used using `source bin/activate.fish` *within a running fish ( http://fishshell.com ) session*.
# Do not run it directly.
function _bashify_path -d "Converts a fish path to something bash can recognize"
set fishy_path $argv
set bashy_path $fishy_path[1]
for path_part in $fishy_path[2..-1]
set bashy_path "$bashy_path:$path_part"
end
echo $bashy_path
end
function _fishify_path -d "Converts a bash path to something fish can recognize"
echo $argv | tr ':' '\n'
end
function deactivate -d 'Exit virtualenv mode and return to the normal environment.'
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
# https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling
if test (echo $FISH_VERSION | head -c 1) -lt 3
set -gx PATH (_fishify_path "$_OLD_VIRTUAL_PATH")
else
set -gx PATH "$_OLD_VIRTUAL_PATH"
end
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME "$_OLD_VIRTUAL_PYTHONHOME"
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
and functions -q _old_fish_prompt
# Set an empty local `$fish_function_path` to allow the removal of `fish_prompt` using `functions -e`.
set -l fish_function_path
# Erase virtualenv's `fish_prompt` and restore the original.
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
set -e _OLD_FISH_PROMPT_OVERRIDE
end
set -e VIRTUAL_ENV
if test "$argv[1]" != 'nondestructive'
# Self-destruct!
functions -e pydoc
functions -e deactivate
functions -e _bashify_path
functions -e _fishify_path
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV '/home/raphael/dev/git/calendarwatch_frontend/calendarwatch'
# https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling
if test (echo $FISH_VERSION | head -c 1) -lt 3
set -gx _OLD_VIRTUAL_PATH (_bashify_path $PATH)
else
set -gx _OLD_VIRTUAL_PATH "$PATH"
end
set -gx PATH "$VIRTUAL_ENV"'/bin' $PATH
# Unset `$PYTHONHOME` if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
function pydoc
python -m pydoc $argv
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# Copy the current `fish_prompt` function as `_old_fish_prompt`.
functions -c fish_prompt _old_fish_prompt
function fish_prompt
# Run the user's prompt first; it might depend on (pipe)status.
set -l prompt (_old_fish_prompt)
# Prompt override provided?
# If not, just prepend the environment name.
if test -n ''
printf '%s%s' '' (set_color normal)
else
printf '%s(%s) ' (set_color normal) (basename "$VIRTUAL_ENV")
end
string join -- \n $prompt # handle multi-line prompts
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
end

View File

@ -1,60 +0,0 @@
$script:THIS_PATH = $myinvocation.mycommand.path
$script:BASE_DIR = Split-Path (Resolve-Path "$THIS_PATH/..") -Parent
function global:deactivate([switch] $NonDestructive) {
if (Test-Path variable:_OLD_VIRTUAL_PATH) {
$env:PATH = $variable:_OLD_VIRTUAL_PATH
Remove-Variable "_OLD_VIRTUAL_PATH" -Scope global
}
if (Test-Path function:_old_virtual_prompt) {
$function:prompt = $function:_old_virtual_prompt
Remove-Item function:\_old_virtual_prompt
}
if ($env:VIRTUAL_ENV) {
Remove-Item env:VIRTUAL_ENV -ErrorAction SilentlyContinue
}
if (!$NonDestructive) {
# Self destruct!
Remove-Item function:deactivate
Remove-Item function:pydoc
}
}
function global:pydoc {
python -m pydoc $args
}
# unset irrelevant variables
deactivate -nondestructive
$VIRTUAL_ENV = $BASE_DIR
$env:VIRTUAL_ENV = $VIRTUAL_ENV
New-Variable -Scope global -Name _OLD_VIRTUAL_PATH -Value $env:PATH
$env:PATH = "$env:VIRTUAL_ENV/bin:" + $env:PATH
if (!$env:VIRTUAL_ENV_DISABLE_PROMPT) {
function global:_old_virtual_prompt {
""
}
$function:_old_virtual_prompt = $function:prompt
if ("" -ne "") {
function global:prompt {
# Add the custom prefix to the existing prompt
$previous_prompt_value = & $function:_old_virtual_prompt
("" + $previous_prompt_value)
}
}
else {
function global:prompt {
# Add a prefix to the current prompt, but don't discard it.
$previous_prompt_value = & $function:_old_virtual_prompt
$new_prompt_value = "($( Split-Path $env:VIRTUAL_ENV -Leaf )) "
($new_prompt_value + $previous_prompt_value)
}
}
}

View File

@ -1,46 +0,0 @@
"""Xonsh activate script for virtualenv"""
from xonsh.tools import get_sep as _get_sep
def _deactivate(args):
if "pydoc" in aliases:
del aliases["pydoc"]
if ${...}.get("_OLD_VIRTUAL_PATH", ""):
$PATH = $_OLD_VIRTUAL_PATH
del $_OLD_VIRTUAL_PATH
if ${...}.get("_OLD_VIRTUAL_PYTHONHOME", ""):
$PYTHONHOME = $_OLD_VIRTUAL_PYTHONHOME
del $_OLD_VIRTUAL_PYTHONHOME
if "VIRTUAL_ENV" in ${...}:
del $VIRTUAL_ENV
if "VIRTUAL_ENV_PROMPT" in ${...}:
del $VIRTUAL_ENV_PROMPT
if "nondestructive" not in args:
# Self destruct!
del aliases["deactivate"]
# unset irrelevant variables
_deactivate(["nondestructive"])
aliases["deactivate"] = _deactivate
$VIRTUAL_ENV = r"/home/raphael/dev/git/calendarwatch_frontend/calendarwatch"
$_OLD_VIRTUAL_PATH = $PATH
$PATH = $PATH[:]
$PATH.add($VIRTUAL_ENV + _get_sep() + "bin", front=True, replace=True)
if ${...}.get("PYTHONHOME", ""):
# unset PYTHONHOME if set
$_OLD_VIRTUAL_PYTHONHOME = $PYTHONHOME
del $PYTHONHOME
$VIRTUAL_ENV_PROMPT = ""
if not $VIRTUAL_ENV_PROMPT:
del $VIRTUAL_ENV_PROMPT
aliases["pydoc"] = ["python", "-m", "pydoc"]

View File

@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
"""Activate virtualenv for current interpreter:
Use exec(open(this_file).read(), {'__file__': this_file}).
This can be used when you must use an existing Python interpreter, not the virtualenv bin/python.
"""
import os
import site
import sys
try:
abs_file = os.path.abspath(__file__)
except NameError:
raise AssertionError("You must use exec(open(this_file).read(), {'__file__': this_file}))")
bin_dir = os.path.dirname(abs_file)
base = bin_dir[: -len("bin") - 1] # strip away the bin part from the __file__, plus the path separator
# prepend bin to PATH (this file is inside the bin directory)
os.environ["PATH"] = os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep))
os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory
# add the virtual environments libraries to the host python import mechanism
prev_length = len(sys.path)
for lib in "../lib/python3.7/site-packages".split(os.pathsep):
path = os.path.realpath(os.path.join(bin_dir, lib))
site.addsitedir(path.decode("utf-8") if "" else path)
sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length]
sys.real_prefix = sys.prefix
sys.prefix = base

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from alembic.config import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from chardet.cli.chardetect import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from dotenv.cli import cli
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from setuptools.command.easy_install import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from setuptools.command.easy_install import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from setuptools.command.easy_install import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from email_validator import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from flask.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,137 +0,0 @@
#!/usr/bin/python
# Python bindings to the Google search engine
# Copyright (c) 2009-2019, Mario Vilas
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice,this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import sys
from googlesearch import search, get_random_user_agent
# TODO port to argparse
from optparse import OptionParser, IndentedHelpFormatter
class BannerHelpFormatter(IndentedHelpFormatter):
"Just a small tweak to optparse to be able to print a banner."
def __init__(self, banner, *argv, **argd):
self.banner = banner
IndentedHelpFormatter.__init__(self, *argv, **argd)
def format_usage(self, usage):
msg = IndentedHelpFormatter.format_usage(self, usage)
return '%s\n%s' % (self.banner, msg)
def main():
# Parse the command line arguments.
formatter = BannerHelpFormatter(
"Python script to use the Google search engine\n"
"By Mario Vilas (mvilas at gmail dot com)\n"
"https://github.com/MarioVilas/googlesearch\n"
)
parser = OptionParser(formatter=formatter)
parser.set_usage("%prog [options] query")
parser.add_option(
'--tld', metavar='TLD', type='string', default='com',
help="top level domain to use [default: com]")
parser.add_option(
'--lang', metavar='LANGUAGE', type='string', default='en',
help="produce results in the given language [default: en]")
parser.add_option(
'--domains', metavar='DOMAINS', type='string', default='',
help="comma separated list of domains to constrain the search to")
parser.add_option(
'--tbs', metavar='TBS', type='string', default='0',
help="produce results from period [default: 0]")
parser.add_option(
'--safe', metavar='SAFE', type='string', default='off',
help="kids safe search [default: off]")
parser.add_option(
'--type', metavar='TYPE', type='string', default='search', dest='tpe',
help="search type (search, images, videos, news, shopping, books,"
" apps) [default: search]")
parser.add_option(
'--country', metavar='COUNTRY', type='string', default='',
help="region to restrict search on [default: not restricted]")
parser.add_option(
'--num', metavar='NUMBER', type='int', default=10,
help="number of results per page [default: 10]")
parser.add_option(
'--start', metavar='NUMBER', type='int', default=0,
help="first result to retrieve [default: 0]")
parser.add_option(
'--stop', metavar='NUMBER', type='int', default=0,
help="last result to retrieve [default: unlimited]")
parser.add_option(
'--pause', metavar='SECONDS', type='float', default=2.0,
help="pause between HTTP requests [default: 2.0]")
parser.add_option(
'--rua', metavar='USERAGENT', action='store_true', default=False,
help="Randomize the User-Agent [default: no]")
(options, args) = parser.parse_args()
query = ' '.join(args)
if not query:
parser.print_help()
sys.exit(2)
params = [
(k, v) for (k, v) in options.__dict__.items()
if not k.startswith('_')]
params = dict(params)
# Split the comma separated list of domains, if present.
if 'domains' in params:
params['domains'] = [x.strip() for x in params['domains'].split(',')]
# Use a special search type if requested.
if 'tpe' in params:
tpe = params['tpe']
if tpe and tpe not in (
'search', 'images', 'videos', 'news',
'shopping', 'books', 'apps'):
parser.error("invalid type: %r" % tpe)
if tpe == 'search':
params['tpe'] = ''
# Randomize the user agent if requested.
if 'rua' in params and params.pop('rua'):
params['user_agent'] = get_random_user_agent()
# Run the query.
for url in search(query, **params):
print(url)
try:
sys.stdout.flush()
except Exception:
pass
if __name__ == '__main__':
main()

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from google_auth_oauthlib.tool.__main__ import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from mako.cmd import cmdline
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cmdline())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from rsa.cli import decrypt
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(decrypt())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from rsa.cli import encrypt
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(encrypt())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from rsa.cli import keygen
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(keygen())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from rsa.util import private_to_public
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(private_to_public())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from rsa.cli import sign
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(sign())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from rsa.cli import verify
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(verify())

View File

@ -1 +0,0 @@
/usr/bin/python3

View File

@ -1 +0,0 @@
python

View File

@ -1 +0,0 @@
python

Binary file not shown.

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from wheel.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from wheel.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/raphael/dev/git/calendarwatch_frontend/calendarwatch/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from wheel.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
home = /usr
implementation = CPython
version_info = 3.7.3.final.0
virtualenv = 20.0.20
include-system-site-packages = false
base-prefix = /usr
base-exec-prefix = /usr
base-executable = /usr/bin/python3

View File

@ -3,6 +3,5 @@ basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
# ...
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app.db')
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,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

@ -1,8 +1,8 @@
"""empty message
Revision ID: af001b07177d
Revision ID: aeab4aff199b
Revises:
Create Date: 2020-05-16 13:53:02.747714
Create Date: 2020-05-27 15:23:20.611265
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'af001b07177d'
revision = 'aeab4aff199b'
down_revision = None
branch_labels = None
depends_on = None
@ -18,37 +18,55 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('calendar',
sa.Column('usr_id', sa.String(length=21), nullable=True),
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.PrimaryKeyConstraint('calendar_id')
)
op.create_index(op.f('ix_calendar_name'), 'calendar', ['name'], unique=False)
op.create_index(op.f('ix_calendar_usr_id'), 'calendar', ['usr_id'], unique=False)
op.create_table('user',
sa.Column('id', sa.String(length=64), nullable=False),
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.Column('calendarJson', sa.String(), nullable=True),
sa.Column('google_credentials', sa.String(), 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')
op.drop_index(op.f('ix_calendar_usr_id'), table_name='calendar')
op.drop_index(op.f('ix_calendar_name'), table_name='calendar')
op.drop_table('calendar')
# ### 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 TEXT,
color TEXT
);

View File

@ -1,92 +0,0 @@
from flask_login import UserMixin
from pathlib import Path
from server.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
return calendar
@staticmethod
def updateCalendar(user_id, calendar_name, toggle=None, color=None):
db = get_db()
print("updating")
if(toggle != None):
print(toggle)
db.execute(
"UPDATE calendar SET toggle = ? WHERE usr_id = ? AND name = ?", (toggle, user_id, calendar_name)
)
db.commit()
if(color != None):
db.execute(
"UPDATE calendar SET color = ? WHERE usr_id = ? AND name = ?", (color, user_id, calendar_name)
)
db.commit()
@staticmethod
def create(user_id, calendar_id, name, color, toggle = 'True'):
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()

Binary file not shown.

View File

@ -1,13 +1,11 @@
FROM python:3.8-slim-buster
RUN apt-get update && apt-get upgrade
RUN apt-get install -y cron
RUN pip3 install flask Flask-SQLAlchemy flask_migrate flask_login flask_wtf python-dotenv
RUN apt-get install gcc libpcre3 libpcre3-dev -y
RUN pip3 install uwsgi
RUN pip3 install email-validator
RUN pip3 install google google-oauth google-auth-oauthlib google-api-python-client
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

@ -1,7 +1,17 @@
#!/bin/sh
cd /home/calendarwatch
# uwsgi --http-socket 0.0.0.0:8084 -w wsgi --protocol=https
python3 server.py
# 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

@ -1,11 +1,29 @@
version: '3'
services:
calendarwatch:
build:
context: ./calendarwatch
image: calendarwatch:latest
container_name: calendarwatch
volumes:
- ../:/home/calendarwatch
ports:
- "0.0.0.0:8084:8084"
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

View File

@ -1,17 +0,0 @@
#!/usr/bin/env python3
from backend.Routine import Routine
import sched, time
s = sched.scheduler(time.time, time.sleep)
routine = Routine()
def run_routine(sc):
# do some stuff
routine.start()
#schedule next routine
s.enter(600, 1, run_routine, (sc,))
routine.start()
s.enter(600, 1, run_routine, (s, ))
s.run()

View File

@ -1,10 +1,5 @@
# Configuration
from server import app
from backend import routine
# routine.start()
if __name__ == "__main__":
context = ('certificate/xip.io.crt', 'certificate/xip.io.key')#certificate and key files
# app.run('0.0.0.0', 8084, ssl_context='adhoc', debug=True)
app.run(host='0.0.0.0', port=8084, debug=True)

View File

@ -13,7 +13,6 @@ 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)
@ -23,5 +22,5 @@ migrate = Migrate(app, db)
# https://flask-login.readthedocs.io/en/latest
login_manager = LoginManager(app)
from server import routes, models
from server import routes
from database import models

View File

@ -1,16 +1,24 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, ValidationError, Email, EqualTo
from server.models import User
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()])
@ -31,5 +39,20 @@ class RegistrationForm(FlaskForm):
raise ValidationError('Please use a different email address.')
class DeviceForm(FlaskForm):
deviceId=StringField('New Device ID', validators=[DataRequired()])
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

View File

@ -1,15 +1,14 @@
import google.oauth2.credentials
import google_auth_oauthlib.flow
import googleapiclient.discovery
from googleapiclient.discovery import build
import backend.caltojson as caltojson
from oauthlib.oauth2 import WebApplicationClient
import flask
# Python standard libraries
import json
import os
import sqlite3
# Third-party libraries
import flask
@ -23,37 +22,57 @@ from flask_login import (
)
import requests
from server.models import Calendar as dbCalendar
from database.models import Calendar as dbCalendar
from backend import Calendar, Event
# Configuration
CLIENT_SECRETS_FILE = "certificate/client_secret.json"
# 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"
# 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'
with open("/home/calendarwatch/certificate/google_client.json", encoding='utf-8') as json_file:
self.google_client = json.load(json_file)
GOOGLE_CLIENT_ID ="377787187748-shuvi4iq5bi4gdet6q3ioataimobs4lh.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET = "Hu_YWmKsVKUcLwyeINYzdKfZ"
GOOGLE_DISCOVERY_URL = (
"https://accounts.google.com/.well-known/openid-configuration"
)
self.SCOPES = self.google_client.get('scopes')
self.API_SERVICE_NAME = 'calendar'
self.API_VERSION = 'v3'
# OAuth 2 client setup
client = WebApplicationClient(GOOGLE_CLIENT_ID)
# 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(
CLIENT_SECRETS_FILE, scopes=SCOPES)
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 = request.base_url + "/callback"
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.
@ -72,8 +91,8 @@ def verifyResponse():
state = flask.session['state']
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
CLIENT_SECRETS_FILE, scopes=SCOPES, state=state)
flow.redirect_uri = flask.url_for('callback', _external=True)
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
@ -91,66 +110,101 @@ def verifyResponse():
def get_google_provider_cfg():
return requests.get(GOOGLE_DISCOVERY_URL).json()
return requests.get(GC.GOOGLE_DISCOVERY_URL).json()
class Calendar:
def __init__(self, name, calendarId, toggle='False', color="#000000"):
self.name = name
self.color = color
self.toggle=toggle
self.calendarId = calendarId
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 calendarsFromDb():
calendars = dbCalendar.getCalendars(dbCalendar, current_user.id)
pyCalendars = []
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:
name = (calendar.name[:16] + '..') if len(calendar.name)> 18 else calendar.name
calendarId = calendar.calendar_id
toggle = calendar.toggle
color = calendar.color
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()
pyCalendars.append(Calendar(name, calendarId, toggle, color))
for event in event_result.get('items', []):
return pyCalendars
# 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 getCalendarJson():
if 'credentials' not in flask.session:
return flask.redirect('login/google')
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)
# Load credentials from the session.
credentials = google.oauth2.credentials.Credentials(
**flask.session['credentials'])
service = build(GC.API_SERVICE_NAME, GC.API_VERSION, credentials=credentials)
with open('./userinfo/' + current_user.id + '/calendarevents.json', 'w') as outfile:
json.dump(todaysCal, outfile)
return todaysCal
# 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
def updateCalendars():
if 'credentials' not in flask.session:
return flask.redirect('login/google')
colors = service.colors().get().execute()
# Load credentials from the session.
# credentials = google.oauth2.credentials.Credentials(
# **flask.session['credentials'])
# a = flask.session['credentials']
# print(a, flush=True)
# print(current_user.getGoogleCredentials(), flush=True)
return calendars, colors, credentials.token
credentials = google.oauth2.credentials.Credentials(**current_user.getGoogleCredentials())
calendars = caltojson.getCalendarList(credentials)
for calendar in calendars:
if dbCalendar.getCalendar(dbCalendar, current_user.id, calendar.calendarId) == None:
dbCalendar.create(dbCalendar, 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)
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):

View File

@ -1,94 +0,0 @@
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.String(64), primary_key=True)
username = db.Column(db.String(64), index=True, unique=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))
calendarJson = db.Column(db.String)
google_credentials = db.Column(db.String)
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 setJson(self, jsonObject):
self.calendarJson = json.dumps(jsonObject)
db.session.commit()
def getJson(self):
return json.loads(self.calendarJson)
def setGoogleCredentials(self, credentials):
self.google_credentials = json.dumps(credentials)
db.session.commit()
def getGoogleCredentials(self):
if self.google_credentials is None:
print("no credentials", flush=True)
return None
return json.loads(self.google_credentials)
class Calendar(db.Model):
usr_id = db.Column(db.String(21), index=True)
calendar_id = db.Column(db.String(256), primary_key=True)
name = db.Column(db.String(256), index=True)
toggle = db.Column(db.String(8))
color = db.Column(db.String(16))
def getCalendars(self, user_id):
calendars = self.query.filter(Calendar.usr_id==user_id)
return calendars
def getCalendar(self, user_id, calendar_id):
calendars = self.query.filter(self.usr_id==user_id, self.calendar_id==calendar_id)
calendar = None
for c in calendars:
calendar = c
if not calendar:
return None
return calendar
@staticmethod
def updateCalendar(user_id, calendar_id, toggle=None, color=None):
calendar = Calendar.query.filter(Calendar.usr_id==user_id, Calendar.calendar_id==calendar_id).first()
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 create(self, user_id, calendar_id, name, color, toggle = 'True'):
newcal = Calendar(usr_id=user_id, calendar_id=calendar_id, name=name, toggle=toggle, color=color)
db.session.add(newcal)
db.session.commit()
class Device(db.Model):
id = db.Column(db.Integer, primary_key=True)
deviceId = db.Column(db.String(128), index=True)

View File

@ -1,12 +1,12 @@
# Python standard libraries
import json
import os
import sqlite3
import time
# Third-party libraries
import flask
from flask import render_template, flash
from flask import Flask, redirect, request, url_for
from flask import Flask, redirect, request, url_for, jsonify
from flask_login import (
LoginManager,
current_user,
@ -14,30 +14,53 @@ from flask_login import (
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
from server.models import User, Calendar, Device
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 account():
return flask.redirect('account')
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 index():
def account():
if current_user.is_authenticated:
google.updateCalendars()
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 flask.render_template('login.html')
return redirect(url_for("login"))
@app.route("/view")
def view():
@ -51,22 +74,79 @@ def devices():
if not current_user.is_authenticated:
return flask.render_template('login.html')
device = Device()
device.deviceId="Anthon-Mouse-Car"
devices = [device]
# if this is a post request from the 'unlink' submittion form
# delete this device from the list
form = DeviceForm()
if form.validate_on_submit():
print(form.deviceId.data, flush=True)
# TODO add device to database here
if request.method == 'POST':
return flask.render_template('devices.html', devices=devices, form=form)
if request.form.get("submit") == "Unlink":
device = db.session.query(Device).filter(Device.deviceName==request.form.get("device")).first()
db.session.delete(device)
db.session.commit()
# if this is part of the device form
elif form.validate_on_submit():
device = db.session.query(Device).filter(Device.deviceName==form.deviceName.data).first()
current_user.devices.append(device)
db.session.commit()
return flask.render_template('devices.html',
devices=current_user.devices,
form=form)
@app.route("/calendar")
@login_required
@app.route("/calendar", methods=['GET', 'POST', 'DELETE'])
def calendar():
calendars = google.calendarsFromDb()
return flask.render_template('calendar.html', calendars=calendars)
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():
@ -88,9 +168,12 @@ def emaillogin():
def register():
if current_user.is_authenticated:
return redirect(url_for('account'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(id=form.username.data,
user = User(userid=form.username.data,
username=form.username.data,
email=form.email.data)
user.setPassword(form.password.data)
@ -100,16 +183,24 @@ def register():
return redirect(url_for('emaillogin'))
return flask.render_template('register.html', title='Register', form=form)
@app.route("/test")
def testAPI():
if current_user.is_authenticated:
google.updateCalendars()
return redirect('/account')
@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)
@ -123,71 +214,81 @@ def callback():
# by Google
# Doesn't exist? Add it to the database.
if not User.query.get(userinfo['id']):
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(
id=userinfo['id'],
userid=userinfo['id'],
username=userinfo['name'],
email=userinfo['email'],
profile_pic=userinfo['picture'],
password_hash=""
password_hash="",
google_token = gc
)
db.session.add(newser)
db.session.commit()
user = User.query.get(userinfo['id'])
user = db.session.query(User).filter(User.userid==userinfo['id']).first()
# Begin user session by logging the user in
print("login:" + user.id)
login_user(user)
# TODO currently not using the credentials anymore
if user.getGoogleCredentials() is None:
user.setGoogleCredentials(credentials)
return flask.redirect(flask.url_for('index'))
return flask.redirect(flask.url_for('account'))
@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}
return redirect(url_for("startpage"))
@app.route("/userinfo/<path:user>/calendarevents.json")
def downloader(user):
path = "/home/calendarwatch/userinfo/" + user + "/"
print(path, flush=True)
return flask.send_from_directory(path, "calendarevents.json")
@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")
# POST
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()
@app.route('/calendar', methods = ['POST', 'DELETE'])
@login_required
def user():
if request.method == 'POST':
calId = request.json.get('calendar_id')
color = request.json.get('color', None)
toggle = request.json.get('toggle', None)
# 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)
print(request.json, flush=True)
if color != None:
Calendar.updateCalendar(current_user.id, calId, color=color)
if toggle != None:
Calendar.updateCalendar(current_user.id, calId, toggle=toggle)
# toggle specific calendar of user
@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 += "-"
elif request.method == 'DELETE':
# do nothing
return 'NONE'
else:
# POST Error 405
print("405")
# 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)
return 'OK'

View File

@ -1,7 +1,116 @@
body *
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 {
@ -18,60 +127,91 @@ body *
width: 200px;
}
/* top navigation */
.topnav {
background-color: #333;
overflow: hidden;
/* bot navigation */
.footer {
background-color: #d8d8d8;
display: flex;
justify-content:center;
align-items:center;
}
.topnav a {
float: left;
.footer p {
margin: 0px;
text-decoration: none;
display: flex;
color: #f2f2f2;
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;
}
.topnav a:hover {
.navigation a:hover {
background-color: #ddd;
color: black;
}
/* Add an active class to highlight the current page */
.topnav a.active {
.navigation a.active {
background-color: #4CAF50;
color: white;
}
/* Hide the link that should open and close the topnav on small screens */
.topnav .icon {
/* 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 topnav (.icon) */
/* 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) {
.topnav a:not(:first-child) {display: none;}
.topnav a.icon {
.navigation a:not(:first-child) {display: none;}
.navigation a.icon {
float: right;
display: block;
}
}
/* The "responsive" class is added to the topnav with JavaScript when the user clicks on the icon. This class makes the topnav look good on small screens (display the links vertically instead of horizontally) */
/* 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) {
.topnav.responsive {position: relative;}
.topnav.responsive a.icon {
.navigation.responsive {position: relative;}
.navigation.responsive a.icon {
position: absolute;
right: 0;
top: 0;
}
.topnav.responsive a {
.navigation.responsive a {
float: none;
display: block;
text-align: left;
}
}
/* Style page content */
@ -150,25 +290,27 @@ body *
}
.container {
display: inline-flex;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items:center;
flex-direction: row;
padding: 0px 2rem 0px 2rem;
/* flex-direction: row; */
/*padding: 0px 2rem 0px 2rem;*/
}
.container .button {
padding: 1rem 1.5rem 1rem 1.5rem;
font-size: 2rem;
margin: 4rem;
margin: 4rem 1rem 1rem 1rem;
color: black;
text-decoration: none;
}
.container .preview {
width: 20rem;
height: 20rem;
height: auto;
margin: 1rem 3rem 4rem 3rem;
max-width: 100%;
}
.container .button.logout {
@ -189,8 +331,9 @@ body *
}
.sub.container {
width: 20rem;
width: 40%;
justify-content: left;
display: flex;
}
.profile {
@ -210,6 +353,11 @@ body *
.profile .name {
font-size: 3rem;
color: #333;
text-align: center;
}
.grey {
background-color: #ddd;
}
.sub.container .name {
@ -223,3 +371,10 @@ body *
border-radius: 5px;
margin-right: 1rem;
}
@media (max-width:800px) {
.sub.container {
justify-content: center;
}
}

View File

@ -1,9 +1,9 @@
/* Toggle between adding and removing the "responsive" class to topnav when the user clicks on the icon */
/* Toggle between adding and removing the "responsive" class to navigation when the user clicks on the icon */
function menuBars() {
var x = document.getElementById("myTopnav");
if (x.className === "topnav") {
var x = document.getElementById("navigation");
if (x.className === "navigation") {
x.className += " responsive";
} else {
x.className = "topnav";
x.className = "navigation";
}
}

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

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

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

@ -11,8 +11,12 @@
</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

@ -2,18 +2,32 @@
{% block body%}
<div class="container">
<div style="width: 15rem; margin: 1rem">Calendar</div>
<div style="width: 10rem; margin: 1rem; padding-right: 5rem">Show on device</div>
<div style="width: 2rem; margin: 1rem">Color</div>
<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">
<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: 15rem; margin: 1rem;">{{ item.name }}</div>
<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: 10rem; margin: 1rem; padding-right: 5rem">
<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)">
@ -22,18 +36,38 @@
</div>
<!--Color Selector-->
<div style="width: 2rem; margin: 1rem;">
<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 class="container">
<div class="button addcalendar" style="width: auto; margin: 4rem">Add Calendar</div>
<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">

View File

@ -2,34 +2,44 @@
{% block body%}
<div class="container">
<div style="width: 15rem; margin: 1rem">Device ID</div>
<div style="width: 10rem; margin: 1rem; padding-right: 5rem">Link Status</div>
<div style="width: 2rem; margin: 1rem">Action</div>
<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">
<!--Name-->
<div style="width: 15rem; margin: 1rem;">{{ item.deviceId }}</div>
<!--device id-->
<div style="width: 15rem; margin: 1rem;">{{ item.deviceName }}</div>
<!--Toggle-->
<div style="width: 10rem; margin: 1rem; padding-right: 5rem">
Connected
<!--link status-->
<div style="width: 10rem; margin: 1rem; padding-right: 4rem">
{% if item.connection %}
Connected
{% else %}
Not Connected
{% endif %}
</div>
<!--Color Selector-->
<div style="width: 2rem; margin: 1rem;">
<button type="button">Unlink</button>
<!--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">
{{ form.hidden_tag() }}
{{ form.deviceId.label }}
{{ form.deviceId(size=32) }}
{{ form.submit() }}
<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>

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

@ -2,7 +2,12 @@
{% block content %}
<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">
@ -17,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"

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

@ -1,7 +1,25 @@
{% extends "base.html" %}
{% block content %}
<h1>Register</h1>
{% 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>
@ -34,4 +52,11 @@
</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

@ -20,30 +20,31 @@
</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">
<!-- Side navigation -->
<div class="topnav" id="myTopnav">
<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>
{% block body %}
// content here
{% endblock %}
</div>
<div id="main">
</div>
{% include "footer.html" %}
</div>
<!-- Page content -->
<div class="main">
{% block body %}
// content here
{% endblock %}
</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 %}

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