Compare commits

...

17 Commits
dev ... master

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
15 changed files with 220 additions and 69 deletions

@ -1 +1 @@
Subproject commit 45cd71cc4bcddf23f46b1eddcffd8c7fda2b9d41 Subproject commit f939127a0c9099f9c22c39bfaaed33b70b006fa2

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

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

@ -61,11 +61,13 @@ class Device(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id')) user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
deviceName = db.Column(db.String(64), unique=True) deviceName = db.Column(db.String(64), unique=True)
connection = db.Column(db.Boolean) connection = db.Column(db.Boolean)
lastConnection = db.Column(db.BigInteger)
class Calendar(db.Model): class Calendar(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True, nullable=False) 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_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) name = db.Column(db.String(256), index=True)
toggle = db.Column(db.String(8)) toggle = db.Column(db.String(8))
color = db.Column(db.String(16)) color = db.Column(db.String(16))

View File

@ -2,9 +2,10 @@ FROM python:3.8-slim-buster
RUN apt-get update && apt-get upgrade RUN apt-get update && apt-get upgrade
RUN pip3 install flask Flask-SQLAlchemy flask_migrate flask_login flask_wtf python-dotenv RUN pip3 install flask Flask-SQLAlchemy flask_migrate flask_login flask_wtf python-dotenv
RUN apt-get install gcc libpcre3 libpcre3-dev libmariadbclient-dev -y RUN apt-get install gcc libpcre3 libpcre3-dev libmariadbclient-dev -y
RUN pip3 install uwsgi email-validator RandomWords RUN pip3 install uwsgi email-validator RandomWords ics
RUN pip3 install google google-oauth google-auth-oauthlib google-api-python-client mysqlclient RUN pip3 install google google-oauth google-auth-oauthlib google-api-python-client mysqlclient
COPY docker-entrypoint.sh /usr/local/bin/ COPY docker-entrypoint.sh /usr/local/bin/
EXPOSE 8084 EXPOSE 8084
EXPOSE 3001 EXPOSE 3001
ENTRYPOINT ["docker-entrypoint.sh"] ENTRYPOINT ["docker-entrypoint.sh"]
# CMD tail -f /dev/null

View File

@ -1,5 +1,17 @@
#!/bin/sh #!/bin/sh
cd /home/calendarwatch cd /home/calendarwatch
uwsgi --ini wsgi.ini
# 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" echo "server has been started"

View File

@ -46,3 +46,13 @@ class DeviceForm(FlaskForm):
device = Device.query.filter_by(deviceName=deviceName.data).first() device = Device.query.filter_by(deviceName=deviceName.data).first()
if device is None: if device is None:
raise ValidationError('Device not Found') 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

@ -122,6 +122,8 @@ def deleteAccount(user):
def fetchCalendarEvents(user, calendars, startDate, endDate): def fetchCalendarEvents(user, calendars, startDate, endDate):
service = None
if user.google_token is not None:
client_token = GC.build_credentials(user.google_token.token, client_token = GC.build_credentials(user.google_token.token,
user.google_token.refresh_token) user.google_token.refresh_token)
credentials = google.oauth2.credentials.Credentials(**client_token) credentials = google.oauth2.credentials.Credentials(**client_token)
@ -130,10 +132,12 @@ def fetchCalendarEvents(user, calendars, startDate, endDate):
all_events = [] all_events = []
for calendar in calendars: for calendar in calendars:
if calendar.toggle == "True": if (calendar.toggle == "True" and
calendar.calendar_type == "Google" and
service != None):
event_result = service.events().list(calendarId=calendar.calendar_id, event_result = service.events().list(calendarId=calendar.calendar_id,
timeMin=startDate, timeMin=startDate.isoformat(),
timeMax=endDate, timeMax=endDate.isoformat(),
maxResults=10, maxResults=10,
singleEvents=True, singleEvents=True,
orderBy='startTime').execute() orderBy='startTime').execute()
@ -156,7 +160,10 @@ def fetchCalendarEvents(user, calendars, startDate, endDate):
all_events.append(newEvent) all_events.append(newEvent)
if service != None:
colors = service.colors().get().execute() colors = service.colors().get().execute()
else:
colors = None
return all_events, colors return all_events, colors
@ -178,6 +185,7 @@ def fetchCalendars():
calendar_list = service.calendarList().list(pageToken=page_token).execute() calendar_list = service.calendarList().list(pageToken=page_token).execute()
for calendar in calendar_list['items']: for calendar in calendar_list['items']:
calendars.append(Calendar(name=calendar['summary'], calendars.append(Calendar(name=calendar['summary'],
calType="Google",
calendarId=calendar['id'], calendarId=calendar['id'],
color=calendar['colorId'])) color=calendar['colorId']))
page_token = calendar_list.get('nextPageToken') page_token = calendar_list.get('nextPageToken')

View File

@ -1,6 +1,7 @@
# Python standard libraries # Python standard libraries
import json import json
import os import os
import time
# Third-party libraries # Third-party libraries
import flask import flask
@ -16,10 +17,11 @@ from flask_login import (
from random_words import RandomWords from random_words import RandomWords
import requests import requests
import backend.icalHandler as ical
import server.googleHandler as google import server.googleHandler as google
from server import login_manager, app, db from server import login_manager, app, db
from server.forms import LoginForm, RegistrationForm, DeviceForm from server.forms import LoginForm, RegistrationForm, DeviceForm, CalendarForm
import backend import backend
from database.models import User, Calendar, Device, GoogleToken from database.models import User, Calendar, Device, GoogleToken
@ -93,11 +95,58 @@ def devices():
form=form) form=form)
@app.route("/calendar") @app.route("/calendar", methods=['GET', 'POST', 'DELETE'])
@login_required
def calendar(): def calendar():
if not current_user.is_authenticated:
return flask.render_template('login.html')
calendars = backend.calendarsFromDb(current_user) calendars = backend.calendarsFromDb(current_user)
return flask.render_template('calendar.html', calendars=calendars)
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']) @app.route('/login/email', methods=['GET', 'POST'])
def emaillogin(): def emaillogin():
@ -204,6 +253,7 @@ def downloader(device):
if request_device.user_id == None: if request_device.user_id == None:
return jsonify(kind="unregistered") return jsonify(kind="unregistered")
request_device.lastConnection=int(round(time.time()))
request_device.connection=True request_device.connection=True
db.session.commit() db.session.commit()
request_user = db.session.query(User).filter(User.id==request_device.user_id).first() request_user = db.session.query(User).filter(User.id==request_device.user_id).first()
@ -213,6 +263,7 @@ def downloader(device):
# TODO only pass along google calendars form user # TODO only pass along google calendars form user
startDate, endDate = backend.getTimeStamps() startDate, endDate = backend.getTimeStamps()
events, colors = google.fetchCalendarEvents(request_user, request_user.calendars, startDate, endDate) 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) calendarjson = backend.generateJsonFromCalendarEntries(events, colors)
return jsonify(calendarjson) return jsonify(calendarjson)
@ -233,6 +284,7 @@ def generateDeviceFingerprint():
if not db.session.query(Device).filter(Device.deviceName==fingerprint).first(): if not db.session.query(Device).filter(Device.deviceName==fingerprint).first():
# Save as new Device # Save as new Device
device = Device(deviceName=fingerprint, connection=False) device = Device(deviceName=fingerprint, connection=False)
device.lastConnection = int(round(time.time()))
db.session.add(device) db.session.add(device)
db.session.commit() db.session.commit()
break; break;
@ -240,27 +292,3 @@ def generateDeviceFingerprint():
# Send to Device # Send to Device
return jsonify(deviceName=fingerprint) return jsonify(deviceName=fingerprint)
# POST
@app.route('/calendar', methods = ['POST', 'DELETE'])
@login_required
def user():
if request.method == 'POST':
calId = request.json.get('calendar_id')
color = request.json.get('color', None)
toggle = request.json.get('toggle', None)
if color != None:
current_user.updateCalendar(calId, color=color)
if toggle != None:
current_user.updateCalendar(calId, toggle=toggle)
# toggle specific calendar of user
elif request.method == 'DELETE':
# do nothing
return 'NONE'
else:
# POST Error 405
print("405")
return 'OK'

View File

@ -69,7 +69,7 @@ body
} }
.grayblock .padded { .grayblock .padded {
padding: 3rem; padding: 0rem;
} }
.horizontal { .horizontal {
@ -91,11 +91,13 @@ body
} }
.vertical .content .image { .vertical .content .image {
max-width: 90%;
width: 25rem; width: 25rem;
} }
.vertical .content .text { .vertical .content .text {
margin-left: 2rem; max-width: 90%;
margin: auto;
width: 26rem; width: 26rem;
} }
@ -106,8 +108,6 @@ body
.horizontal .image { .horizontal .image {
margin: 1rem; margin: 1rem;
margin-left: 4rem;
margin-right: 4rem;
height: 20rem; height: 20rem;
border-radius: 1rem; border-radius: 1rem;
width: auto; width: auto;
@ -156,7 +156,7 @@ body
background-color: #eaeaea; background-color: #eaeaea;
overflow: hidden; overflow: hidden;
margin: auto; margin: auto;
display: flex; display: block;
} }
.navigation_rightside { .navigation_rightside {
@ -165,7 +165,7 @@ body
.navigation a { .navigation a {
float: left; float: left;
display: flex; display: block;
color: #333; color: #333;
text-align: center; text-align: center;
padding: 14px 16px; padding: 14px 16px;
@ -211,6 +211,7 @@ body
display: block; display: block;
text-align: left; text-align: left;
} }
} }
/* Style page content */ /* Style page content */
@ -289,25 +290,27 @@ body
} }
.container { .container {
display: inline-flex; display: flex;
flex-wrap: wrap;
justify-content: center; justify-content: center;
align-items:center; align-items:center;
flex-direction: row; /* flex-direction: row; */
padding: 0px 2rem 0px 2rem; /*padding: 0px 2rem 0px 2rem;*/
} }
.container .button { .container .button {
padding: 1rem 1.5rem 1rem 1.5rem; padding: 1rem 1.5rem 1rem 1.5rem;
font-size: 2rem; font-size: 2rem;
margin: 4rem; margin: 4rem 1rem 1rem 1rem;
color: black; color: black;
text-decoration: none; text-decoration: none;
} }
.container .preview { .container .preview {
width: 20rem; width: 20rem;
height: 20rem; height: auto;
margin: 1rem 3rem 4rem 3rem; margin: 1rem 3rem 4rem 3rem;
max-width: 100%;
} }
.container .button.logout { .container .button.logout {
@ -328,8 +331,9 @@ body
} }
.sub.container { .sub.container {
width: 20rem; width: 40%;
justify-content: left; justify-content: left;
display: flex;
} }
.profile { .profile {
@ -349,6 +353,7 @@ body
.profile .name { .profile .name {
font-size: 3rem; font-size: 3rem;
color: #333; color: #333;
text-align: center;
} }
.grey { .grey {
@ -366,3 +371,10 @@ body
border-radius: 5px; border-radius: 5px;
margin-right: 1rem; margin-right: 1rem;
} }
@media (max-width:800px) {
.sub.container {
justify-content: center;
}
}

View File

@ -2,18 +2,32 @@
{% block body%} {% block body%}
<div class="container"> <div class="container">
<div style="width: 15rem; margin: 1rem">Calendar</div> <div style="width: 4rem;margin:1rem;"></div>
<div style="width: 10rem; margin: 1rem; padding-right: 5rem">Show on device</div> <div style="width: 10rem; margin: 1rem; font-weight: bold">Calendar</div>
<div style="width: 2rem; margin: 1rem">Color</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> </div>
{% for item in calendars %} {% 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--> <!--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--> <!--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 --> <!-- Rounded switch -->
<label class="switch"> <label class="switch">
<input class="toggle" id={{item.calendarId}} type="checkbox" toggled={{item.toggle}} onclick="toggleReaction(this)"> <input class="toggle" id={{item.calendarId}} type="checkbox" toggled={{item.toggle}} onclick="toggleReaction(this)">
@ -22,22 +36,38 @@
</div> </div>
<!--Color Selector--> <!--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> <div class="colorPickSelector" id={{item.calendarId}} defaultColor={{item.color}}></div>
<!--svg height="20" width="20"> <!--svg height="20" width="20">
<circle cx="10" cy="10" r="10" stroke="black" stroke-width="0" fill={{ item.color }} /> <circle cx="10" cy="10" r="10" stroke="black" stroke-width="0" fill={{ item.color }} />
</svg--> </svg-->
</div> </div>
</div>
</div> </div>
{% endfor %} {% endfor %}
<div id=calendars class="container"> <div id=calendars class="container">
<a class="button" href="login/google">Google Calendar</a>
<a class="button" href="#" >Nextcloud Calendar</a>
</div> </div>
<div class="container"> <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> <a class="button addcalendar" href="/login/google" style="width: auto; margin: 4rem">Add Calendar</a>
</div> </div-->
<script type="text/javascript"> <script type="text/javascript">

View File

@ -1,7 +1,7 @@
<div id="footer"> <div id="footer">
<div class="footer"> <div class="footer container">
<p>made by Raphael Maenle </p> <p>made by Raphael Maenle </p>
<p><a href="mailto:raphael@maenle.net">raphael@maenle.net</a></p> <p><a href="mailto:raphael@maenle.net">raphael@maenle.net</a></p>
<p><a href="/privacy">privacy policy</a></p> <p><a href="/privacy">privacy policy</a></p>

View File

@ -7,7 +7,7 @@
</div> </div>
<h3 style="margin-left:10rem">Summary</h3> <h3 style="margin-left:10rem">Summary</h3>
<div style="margin-left:10rem; margin-right:10rem;">This Privacy Statement descibes how Longitude handles your data and how the developer makes sure, that the users information remains as secure as possible. <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. 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. 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> If you have any further questions or suggestions, please email us at <a href="mailto:raphael@maenle.net">raphael@maenle.net</a>.</div>
@ -15,7 +15,7 @@ If you have any further questions or suggestions, please email us at <a href="ma
<h3 style="margin-left:10rem">What Information is saved?</h3> <h3 style="margin-left:10rem">What Information is saved?</h3>
<div style="margin-left:10rem; margin-right:10rem;"> <div style="margin-left:10rem; margin-right:10rem;">
Longitude Calendar saves as little information about their users as possible. The application handles sensitive information only when directly prompted by the user or a device associated with the user. The service only provides this information to the user or a device associated with the user. The data saved in the Longidute Databas is 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> <ul>
<li>Username and hashed password or alternatively</li> <li>Username and hashed password or alternatively</li>
<li>Google Username and Id with Google Login Token</li> <li>Google Username and Id with Google Login Token</li>

View File

@ -39,6 +39,8 @@
// content here // content here
{% endblock %} {% endblock %}
</div>
<div id="main">
</div> </div>
{% include "footer.html" %} {% include "footer.html" %}
</div> </div>

View File

@ -12,13 +12,15 @@
<div class="grayblock horizontal"> <div class="grayblock horizontal">
<div class="content padded"> <div class="content padded">
<div style='margin: 1rem'> <div style='margin: 1rem'>
Connect your <img src='/static/res/googlelogo.png' style='height: 2.2rem; vertical-align:middle; padding-Bottom: 0.1rem'/> Calendar... <!--Connect your <img src='/static/res/googlelogo.png' style='height: 3.2rem; vertical-align:middle; padding-Bottom: 0.1rem'/> Calendar...-->
Connect your Calendar..
</div> </div>
<img class="image" src='/static/res/calendar.svg'/> <img class="image" src='/static/res/calendar.svg'/>
</div> </div>
<div class="content padded"> <div class="content padded">
<div style='margin: 1rem'> <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 <img src='/static/res/tizenlogo.png' style='height: 2rem; vertical-align:middle; padding-Bottom:0.3rem;'/> Watchface-->
..with your Tizen Watchface
</div> </div>
<img class="image" src='/static/res/watchface.svg'/> <img class="image" src='/static/res/watchface.svg'/>
</div> </div>