Merge pull request #33 from Casvt/Development

V1.1.0
This commit is contained in:
Casvt
2023-01-28 19:44:13 +01:00
committed by GitHub
15 changed files with 517 additions and 67 deletions

153
.dockerignore Normal file
View File

@@ -0,0 +1,153 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Database
*.db
# VS code
*.code-workspace
.vscode/
# Docker
Dockerfile
.dockerignore
docker-compose.yml
# Git(hub)
.gitignore
.git/
.github/
# Various files
*.md
LICENSE
# Tests
tests/

4
.gitignore vendored
View File

@@ -133,7 +133,3 @@ dmypy.json
# VS code
*.code-workspace
# Docker
Dockerfile
.dockerignore

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
# syntax=docker/dockerfile:1
FROM python:3.8-slim-buster
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY . .
CMD [ "python3", "/app/Noted.py" ]

View File

@@ -8,7 +8,7 @@ from typing import Union
from flask import g
__DATABASE_VERSION__ = 2
__DATABASE_VERSION__ = 3
class Singleton(type):
_instances = {}
@@ -79,7 +79,17 @@ def migrate_db(current_db_version: int) -> None:
for reminder in reminders:
new_reminders_append([round((datetime.fromtimestamp(reminder[0]) - utc_offset).timestamp()), reminder[1]])
cursor.executemany("UPDATE reminders SET time = ? WHERE id = ?;", new_reminders)
__DATABASE_VERSION__ = 2
current_db_version = 2
if current_db_version == 2:
# V2 -> V3
cursor.executescript("""
ALTER TABLE reminders
ADD color VARCHAR(7);
ALTER TABLE templates
ADD color VARCHAR(7);
""")
current_db_version = 3
return
@@ -115,6 +125,8 @@ def setup_db() -> None:
repeat_interval INTEGER,
original_time INTEGER,
color VARCHAR(7),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (notification_service) REFERENCES notification_services(id)
);
@@ -125,6 +137,8 @@ def setup_db() -> None:
text TEXT,
notification_service INTEGER NOT NULL,
color VARCHAR(7),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (notification_service) REFERENCES notification_services(id)
);

View File

@@ -156,7 +156,8 @@ class Reminder:
r.notification_service,
ns.title AS notification_service_title,
r.repeat_quantity,
r.repeat_interval
r.repeat_interval,
r.color
FROM
reminders r
INNER JOIN notification_services ns
@@ -176,7 +177,8 @@ class Reminder:
notification_service: int = None,
text: str = None,
repeat_quantity: Literal["year", "month", "week", "day", "hours", "minutes"] = None,
repeat_interval: int = None
repeat_interval: int = None,
color: str = None
) -> dict:
"""Edit the reminder
@@ -185,6 +187,9 @@ class Reminder:
time (int): The new UTC epoch timestamp the the reminder should be send. Defaults to None.
notification_service (int): The new id of the notification service to use to send the reminder. Defaults to None.
text (str, optional): The new body of the reminder. Defaults to None.
repeat_quantity (Literal["year", "month", "week", "day", "hours", "minutes"], optional): The new quantity of the repeat specified for the reminder. Defaults to None.
repeat_interval (int, optional): The new amount of repeat_quantity, like "5" (hours). Defaults to None.
color (str, optional): The new hex code of the color of the reminder, which is shown in the web-ui. Defaults to None.
Returns:
dict: The new reminder info
@@ -212,10 +217,11 @@ class Reminder:
'notification_service': notification_service,
'text': text,
'repeat_quantity': repeat_quantity,
'repeat_interval': repeat_interval
'repeat_interval': repeat_interval,
'color': color
}
for k, v in new_values.items():
if k in ('repeat_quantity', 'repeat_interval') or v is not None:
if k in ('repeat_quantity', 'repeat_interval', 'color') or v is not None:
data[k] = v
# Update database
@@ -224,7 +230,7 @@ class Reminder:
next_time = data["time"]
cursor.execute("""
UPDATE reminders
SET title=?, text=?, time=?, notification_service=?, repeat_quantity=?, repeat_interval=?
SET title=?, text=?, time=?, notification_service=?, repeat_quantity=?, repeat_interval=?, color=?
WHERE id = ?;
""", (
data["title"],
@@ -233,13 +239,14 @@ class Reminder:
data["notification_service"],
data["repeat_quantity"],
data["repeat_interval"],
data["color"],
self.id
))
else:
next_time = _find_next_time(data["time"], data["repeat_quantity"], data["repeat_interval"])
cursor.execute("""
UPDATE reminders
SET title=?, text=?, time=?, notification_service=?, repeat_quantity=?, repeat_interval=?, original_time=?
SET title=?, text=?, time=?, notification_service=?, repeat_quantity=?, repeat_interval=?, original_time=?, color=?
WHERE id = ?;
""", (
data["title"],
@@ -249,6 +256,7 @@ class Reminder:
data["repeat_quantity"],
data["repeat_interval"],
data["time"],
data["color"],
self.id
))
except IntegrityError:
@@ -284,7 +292,7 @@ class Reminders:
sort_by (Literal["time", "time_reversed", "title", "title_reversed"], optional): How to sort the result. Defaults to "time".
Returns:
List[dict]: The id, title, text, time, notification_service and notification_service_title of each reminder
List[dict]: The id, title, text, time, notification_service, notification_service_title and color of each reminder
"""
sort_function = self.sort_functions.get(
sort_by,
@@ -300,7 +308,8 @@ class Reminders:
r.notification_service,
ns.title AS notification_service_title,
r.repeat_quantity,
r.repeat_interval
r.repeat_interval,
r.color
FROM
reminders r
INNER JOIN notification_services ns
@@ -350,7 +359,8 @@ class Reminders:
notification_service: int,
text: str = '',
repeat_quantity: Literal["year", "month", "week", "day", "hours", "minutes"] = None,
repeat_interval: int = None
repeat_interval: int = None,
color: str = None
) -> Reminder:
"""Add a reminder
@@ -361,6 +371,7 @@ class Reminders:
text (str, optional): The body of the reminder. Defaults to ''.
repeat_quantity (Literal["year", "month", "week", "day", "hours", "minutes"], optional): The quantity of the repeat specified for the reminder. Defaults to None.
repeat_interval (int, optional): The amount of repeat_quantity, like "5" (hours). Defaults to None.
color (str, optional): The hex code of the color of the reminder, which is shown in the web-ui. Defaults to None.
Returns:
dict: The info about the reminder
@@ -377,15 +388,15 @@ class Reminders:
try:
if repeat_quantity is None and repeat_interval is None:
id = get_db().execute("""
INSERT INTO reminders(user_id, title, text, time, notification_service)
VALUES (?,?,?,?,?);
""", (self.user_id, title, text, time, notification_service)
INSERT INTO reminders(user_id, title, text, time, notification_service, color)
VALUES (?,?,?,?,?, ?);
""", (self.user_id, title, text, time, notification_service, color)
).lastrowid
else:
id = get_db().execute("""
INSERT INTO reminders(user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, original_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
""", (self.user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, time)
INSERT INTO reminders(user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, original_time, color)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
""", (self.user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, time, color)
).lastrowid
except IntegrityError:
raise NotificationServiceNotFound
@@ -393,3 +404,26 @@ class Reminders:
# Return info
return self.fetchone(id)
def test_reminder(
title: str,
notification_service: int,
text: str = ''
) -> None:
"""Test send a reminder draft
Args:
title (str): Title title of the entry
notification_service (int): The id of the notification service to use to send the reminder
text (str, optional): The body of the reminder. Defaults to ''.
"""
a = Apprise()
url = get_db(dict).execute(
"SELECT url FROM notification_services WHERE id = ?",
(notification_service,)
).fetchone()
if not url:
raise NotificationServiceNotFound
a.add(url[0])
a.notify(title=title, body=text)
return

View File

@@ -27,7 +27,8 @@ class Template:
SELECT
id,
title, text,
notification_service
notification_service,
color
FROM templates
WHERE id = ?;
""",
@@ -39,7 +40,8 @@ class Template:
def update(self,
title: str = None,
notification_service: int = None,
text: str = None
text: str = None,
color: str = None
) -> dict:
"""Edit the template
@@ -47,6 +49,7 @@ class Template:
title (str): The new title of the entry. Defaults to None.
notification_service (int): The new id of the notification service to use to send the reminder. Defaults to None.
text (str, optional): The new body of the template. Defaults to None.
color (str, optional): The new hex code of the color of the template, which is shown in the web-ui. Defaults to None.
Returns:
dict: The new template info
@@ -57,21 +60,23 @@ class Template:
new_values = {
'title': title,
'notification_service': notification_service,
'text': text
'text': text,
'color': color
}
for k, v in new_values.items():
if v is not None:
if k in ('color',) or v is not None:
data[k] = v
try:
cursor.execute("""
UPDATE templates
SET title=?, notification_service=?, text=?
SET title=?, notification_service=?, text=?, color=?
WHERE id = ?;
""", (
data['title'],
data['notification_service'],
data['text'],
data['color'],
self.id
))
except IntegrityError:
@@ -95,13 +100,14 @@ class Templates:
"""Get all templates
Returns:
List[dict]: The id, title, text and notification_service
List[dict]: The id, title, text, notification_service and color
"""
templates: list = list(map(dict, get_db(dict).execute("""
SELECT
id,
title, text,
notification_service
notification_service,
color
FROM templates
WHERE user_id = ?
ORDER BY title, id;
@@ -126,7 +132,8 @@ class Templates:
self,
title: str,
notification_service: int,
text: str = ''
text: str = '',
color: str = None
) -> Template:
"""Add a template
@@ -134,16 +141,17 @@ class Templates:
title (str): The title of the entry
notification_service (int): The id of the notification service to use to send the reminder.
text (str, optional): The body of the reminder. Defaults to ''.
color (str, optional): The hex code of the color of the template, which is shown in the web-ui. Defaults to None.
Returns:
Template: The info about the template
"""
try:
id = get_db().execute("""
INSERT INTO templates(user_id, title, text, notification_service)
VALUES (?,?,?,?);
INSERT INTO templates(user_id, title, text, notification_service, color)
VALUES (?,?,?,?,?);
""",
(self.user_id, title, text, notification_service)
(self.user_id, title, text, notification_service, color)
).lastrowid
except IntegrityError:
raise NotificationServiceNotFound

View File

@@ -3,6 +3,7 @@
from os import urandom
from time import time as epoch_time
from typing import Any, Tuple
from re import compile
from flask import Blueprint, g, request
@@ -14,12 +15,13 @@ from backend.custom_exceptions import (AccessUnauthorized, InvalidKeyValue,
UsernameTaken, UserNotFound)
from backend.notification_service import (NotificationService,
NotificationServices)
from backend.reminders import Reminders, reminder_handler
from backend.reminders import Reminders, reminder_handler, test_reminder
from backend.templates import Template, Templates
from backend.users import User, register_user
api = Blueprint('api', __name__)
api_key_map = {}
color_regex = compile(r'#[0-9a-f]{6}')
"""
AUTHENTICATION:
@@ -104,6 +106,10 @@ def extract_key(values: dict, key: str, check_existence: bool=True) -> Any:
'text', 'query'):
if not isinstance(value, str):
raise InvalidKeyValue(key, value)
elif key == 'color':
if not color_regex.search(value):
raise InvalidKeyValue(key, value)
else:
if key == 'sort_by':
@@ -127,7 +133,7 @@ def api_login():
Requires being logged in: No
Methods:
POST:
Parameters (body (content-type: application/json)):
Parameters (body):
username (required): the username of the user account
password (required): the password of the user account
Returns:
@@ -216,7 +222,7 @@ def api_add_user():
Requires being logged in: No
Methods:
POST:
Parameters (body (content-type: application/json)):
Parameters (body):
username (required): the username of the new user account
password (required): the password of the new user account
Returns:
@@ -248,7 +254,7 @@ def api_manage_user():
Methods:
PUT:
Description: Change the password of the user account
Parameters (body (content-type: application/json)):
Parameters (body):
new_password (required): the new password of the user account
Returns:
200:
@@ -297,7 +303,7 @@ def api_notification_services_list():
The id, title and url of every notification service
POST:
Description: Add a notification service
Parameters (body (content-type: application/json)):
Parameters (body):
title (required): the title of the notification service
url (required): the apprise url of the notification service
Returns:
@@ -340,7 +346,7 @@ def api_notification_service(n_id: int):
No notification service found with the given id
PUT:
Description: Edit the notification service
Parameters (body (content-type: application/json)):
Parameters (body):
title: The new title of the entry.
url: The new apprise url of the entry.
Returns:
@@ -396,16 +402,17 @@ def api_reminders_list():
sort_by: how to sort the result. Allowed values are 'title', 'title_reversed', 'time' and 'time_reversed'
Returns:
200:
The id, title, text, time, notification_service, notification_service_title, repeat_quantity and repeat_interval of each reminder
The id, title, text, time, notification_service, notification_service_title, repeat_quantity, repeat_interval and color of each reminder
POST:
Description: Add a reminder
Parameters (body (content-type: application/json)):
Parameters (body):
title (required): the title of the reminder
time (required): the UTC epoch timestamp that the reminder should be sent at
notification_service (required): the id of the notification service to use to send the notification
text: the body of the reminder
repeat_quantity ('year', 'month', 'week', 'day', 'hours', 'minutes'): The quantity of the repeat_interval
repeat_interval: The number of the interval
color: The hex code of the color of the reminder, which is shown in the web-ui
Returns:
200:
The info about the new reminder entry
@@ -427,13 +434,15 @@ def api_reminders_list():
text = extract_key(data, 'text', check_existence=False)
repeat_quantity = extract_key(data, 'repeat_quantity', check_existence=False)
repeat_interval = extract_key(data, 'repeat_interval', check_existence=False)
color = extract_key(data, 'color', check_existence=False)
result = reminders.add(title=title,
time=time,
notification_service=notification_service,
text=text,
repeat_quantity=repeat_quantity,
repeat_interval=repeat_interval)
repeat_interval=repeat_interval,
color=color)
return return_api(result.get(), code=201)
@api.route('/reminders/search', methods=['GET'])
@@ -459,6 +468,36 @@ def api_reminders_query():
result = g.user_data.reminders.search(query)
return return_api(result)
@api.route('/reminders/test', methods=['POST'])
@error_handler
@auth
def api_test_reminder():
"""
Endpoint: /reminders/test
Description: Test send a reminder draft
Requires being logged in: Yes
Methods:
GET:
Parameters (body):
title (required): The title of the entry.
notification_service (required): The new id of the notification service to use to send the reminder.
text: The body of the reminder.
Returns:
201:
The reminder is sent (doesn't mean it works, just that it was sent)
400:
KeyNotFound: One of the required parameters was not given
404:
NotificationServiceNotFound: The notification service given was not found
"""
data = request.get_json()
title = extract_key(data, 'title')
notification_service = extract_key(data, 'notification_service')
text = extract_key(data, 'text', check_existence=False)
test_reminder(title, notification_service, text)
return return_api({}, code=201)
@api.route('/reminders/<int:r_id>', methods=['GET','PUT','DELETE'])
@error_handler
@auth
@@ -479,13 +518,14 @@ def api_get_reminder(r_id: int):
No reminder found with the given id
PUT:
Description: Edit the reminder
Parameters (body (content-type: application/json)):
Parameters (body):
title: The new title of the entry.
time: The new UTC epoch timestamp the the reminder should be send.
notification_service: The new id of the notification service to use to send the reminder.
text: The new body of the reminder.
repeat_quantity ('year', 'month', 'week', 'day', 'hours', 'minutes'): The quantity of the repeat_interval
repeat_interval: The number of the interval
repeat_quantity ('year', 'month', 'week', 'day', 'hours', 'minutes'): The new quantity of the repeat_interval.
repeat_interval: The new number of the interval.
color: The new hex code of the color of the reminder, which is shown in the web-ui.
Returns:
200:
Reminder updated successfully
@@ -512,14 +552,15 @@ def api_get_reminder(r_id: int):
text = extract_key(data, 'text', check_existence=False)
repeat_quantity = extract_key(data, 'repeat_quantity', check_existence=False)
repeat_interval = extract_key(data, 'repeat_interval', check_existence=False)
color = extract_key(data, 'color', check_existence=False)
result = reminders.fetchone(r_id).update(title=title,
time=time,
notification_service=notification_service,
text=text,
repeat_quantity=repeat_quantity,
repeat_interval=repeat_interval)
repeat_interval=repeat_interval,
color=color)
return return_api(result)
elif request.method == 'DELETE':
@@ -543,13 +584,14 @@ def api_get_templates():
Description: Get a list of all templates
Returns:
200:
The id, title, notification_service and text of every template
The id, title, notification_service, text and color of every template
POST:
Description: Add a template
Parameters (body (content-type: application/json)):
Parameters (body):
title (required): the title of the template
notification_service (required): the id of the notification service to use to send the notification
text: the body of the template
color: the hex code of the color of the template, which is shown in the web-ui
Returns:
200:
The info about the new template entry
@@ -567,10 +609,12 @@ def api_get_templates():
title = extract_key(data, 'title')
notification_service = extract_key(data, 'notification_service')
text = extract_key(data, 'text', check_existence=False)
color = extract_key(data, 'color', check_existence=False)
result = templates.add(title=title,
notification_service=notification_service,
text=text)
text=text,
color=color)
return return_api(result.get(), code=201)
@api.route('/templates/<int:t_id>', methods=['GET', 'PUT', 'DELETE'])
@@ -593,10 +637,11 @@ def api_get_template(t_id: int):
No template found with the given id
PUT:
Description: Edit the template
Parameters (body (content-type: application/json)):
Parameters (body):
title: The new title of the entry.
notification_service: The new id of the notification service to use to send the reminder.
text: The new body of the template.
color: The new hex code of the color of the template.
Returns:
200:
Template updated successfully
@@ -621,10 +666,12 @@ def api_get_template(t_id: int):
title = extract_key(data, 'title', check_existence=False)
notification_service = extract_key(data, 'notification_service', check_existence=False)
text = extract_key(data, 'text', check_existence=False)
color = extract_key(data, 'color', check_existence=False)
result = template.update(title=title,
notification_service=notification_service,
text=text)
text=text,
color=color)
return return_api(result)
elif request.method == 'DELETE':

View File

@@ -33,6 +33,7 @@
width: calc(50% - (var(--gap) / 2));
}
.form-container > form > button,
.sub-inputs > button {
display: flex;
justify-content: center;
@@ -54,6 +55,32 @@
opacity: 0;
}
.color-list {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
padding: 1rem;
border: 2px solid var(--color-gray);
border-radius: 4px;
box-shadow: var(--default-shadow);
}
.color-list > button {
height: 1.5rem;
width: 1.5rem;
padding: 1rem;
border: 1px solid transparent;
background-color: var(--color);
}
.color-list > button[data-selected='true'] {
border-color: var(--color-white);
}
.repeat-bar,
.repeat-edit-bar {
display: flex;
@@ -112,9 +139,30 @@ div.options > button {
color: var(--color-error);
}
#test-reminder {
display: flex;
gap: 1rem;
overflow-x: hidden;
}
#test-reminder > div {
width: 100%;
flex: 0 0 auto;
font-size: inherit;
transition: transform .1s linear;
}
#test-reminder.show-sent > div {
transform: translateX(calc(-100% - 1rem));
}
@media (max-width: 460px) {
.sub-inputs > input,
.sub-inputs > select {
.sub-inputs > select,
.sub-inputs > button {
width: 100%;
}
}

View File

@@ -11,8 +11,8 @@
--color-gray: #3c3c3c;
--color-dark: #1b1b1b;
--color-error: rgb(219, 84, 97);
--color-success: rgb(84, 219, 104);
--color-error: #db5461;
--color-success: #54db68;
--header-height: 4.5rem;
--nav-width: 4rem;

View File

@@ -84,6 +84,7 @@
}
.entry {
--color: var(--color-gray);
width: var(--entry-width);
height: 6rem;
position: relative;
@@ -95,7 +96,7 @@
border-radius: 4px;
padding: .75rem;
background-color: var(--color-gray);
background-color: var(--color);
}
button.entry.fit {

View File

@@ -1,8 +1,10 @@
const colors = ["#3c3c3c", "#49191e", "#171a42", "#083b06", "#3b3506", "#300e40"];
const inputs = {
'title': document.getElementById('title-input'),
'time': document.getElementById('time-input'),
'notification_service': document.getElementById('notification-service-input'),
'text': document.getElementById('text-input')
'text': document.getElementById('text-input'),
'color': document.querySelector('#add .color-list')
};
const type_buttons = {
@@ -22,7 +24,11 @@ function addReminder() {
'title': inputs.title.value,
'time': (new Date(inputs.time.value) / 1000) + (new Date(inputs.time.value).getTimezoneOffset() * 60),
'notification_service': inputs.notification_service.value,
'text': inputs.text.value
'text': inputs.text.value,
'color': null
};
if (!inputs.color.classList.contains('hidden')) {
data['color'] = inputs.color.querySelector('button[data-selected="true"]').dataset.color;
};
if (type_buttons['repeat-button'].dataset.selected === 'true') {
data['repeat_quantity'] = type_buttons['repeat-quantity'].value;
@@ -69,14 +75,43 @@ function closeAdd() {
hideWindow();
setTimeout(() => {
document.getElementById('template-selection').value = document.querySelector('#template-selection option[selected]').value;
if (!inputs.color.classList.contains('hidden')) {
toggleColor(inputs.color);
};
inputs.title.value = '';
inputs.time.value = '';
inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value;
toggleNormal();
inputs.text.value = '';
document.getElementById('test-reminder').classList.remove('show-sent');
}, 500);
};
function loadColor() {
document.querySelectorAll('.color-list').forEach(list => {
colors.forEach(color => {
const entry = document.createElement('button');
entry.dataset.color = color;
entry.title = color;
entry.type = 'button';
entry.style.setProperty('--color', color);
entry.addEventListener('click', e => selectColor(list, color))
list.appendChild(entry);
});
});
};
function selectColor(list, color_code) {
list.querySelector(`button[data-color="${color_code}"]`).dataset.selected = 'true';
list.querySelectorAll(`button:not([data-color="${color_code}"])`).forEach(b => b.dataset.selected = 'false');
return;
}
function toggleColor(list) {
selectColor(list, colors[0]);
list.classList.toggle('hidden');
}
function toggleNormal() {
type_buttons['normal-button'].dataset.selected = 'true';
type_buttons['repeat-button'].dataset.selected = 'false';
@@ -89,15 +124,53 @@ function toggleNormal() {
function toggleRepeated() {
type_buttons['normal-button'].dataset.selected = 'false';
type_buttons['repeat-button'].dataset.selected = 'true';
type_buttons['repeat-bar'].classList.remove('hidden');
type_buttons['repeat-interval'].setAttribute('required', '');
};
function testReminder() {
const input = document.getElementById('test-reminder');
if (inputs.title.value === '') {
input.classList.add('error-input');
input.title = 'No title set';
return
} else {
input.classList.remove('error-input');
input.removeAttribute('title');
};
const data = {
'title': inputs.title.value,
'notification_service': inputs.notification_service.value,
'text': inputs.text.value
};
fetch(`/api/reminders/test?api_key=${api_key}`, {
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
})
.then(response => {
// catch errors
if (!response.ok) {
return Promise.reject(response.status);
};
input.classList.add('show-sent');
})
.catch(e => {
if (e === 401) {
window.location.href = '/';
};
});
};
// code run on load
document.getElementById('add-form').setAttribute('action', 'javascript:addReminder();');
document.getElementById('template-selection').addEventListener('change', e => loadTemplate());
document.getElementById('color-toggle').addEventListener('click', e => toggleColor(inputs.color));
document.getElementById('normal-button').addEventListener('click', e => toggleNormal());
document.getElementById('repeat-button').addEventListener('click', e => toggleRepeated());
document.getElementById('close-add').addEventListener('click', e => closeAdd());
document.getElementById('test-reminder').addEventListener('click', e => testReminder());
loadColor();

View File

@@ -2,7 +2,8 @@ const edit_inputs = {
'title': document.getElementById('title-edit-input'),
'time': document.getElementById('time-edit-input'),
'notification_service': document.getElementById('notification-service-edit-input'),
'text': document.getElementById('text-edit-input')
'text': document.getElementById('text-edit-input'),
'color': document.querySelector('#edit .color-list')
};
const edit_type_buttons = {
@@ -22,7 +23,11 @@ function editReminder() {
'notification_service': edit_inputs.notification_service.value,
'text': edit_inputs.text.value,
'repeat_quantity': null,
'repeat_interval': null
'repeat_interval': null,
'color': null
};
if (!edit_inputs.color.classList.contains('hidden')) {
data['color'] = edit_inputs.color.querySelector('button[data-selected="true"]').dataset.color;
};
if (edit_type_buttons['repeat-edit-button'].dataset.selected === 'true') {
data['repeat_quantity'] = edit_type_buttons['repeat-edit-quantity'].value;
@@ -65,6 +70,13 @@ function showEdit(id) {
return response.json();
})
.then(json => {
if (json.result['color'] !== null) {
if (edit_inputs.color.classList.contains('hidden')) {
toggleColor(edit_inputs.color);
};
selectColor(edit_inputs.color, json.result['color']);
};
edit_inputs.title.value = json.result.title;
var trigger_date = new Date(
@@ -142,6 +154,7 @@ function deleteReminder() {
// code run on load
document.getElementById('edit-form').setAttribute('action', 'javascript:editReminder();');
document.getElementById('color-edit-toggle').addEventListener('click', e => toggleColor(edit_inputs.color));
document.getElementById('normal-edit-button').addEventListener('click', e => toggleEditNormal());
document.getElementById('repeat-edit-button').addEventListener('click', e => toggleEditRepeated());
document.getElementById('close-edit').addEventListener('click', e => hideWindow());

View File

@@ -7,6 +7,9 @@ function fillTable(result) {
entry.classList.add('entry');
entry.dataset.id = reminder.id;
entry.addEventListener('click', e => showEdit(reminder.id));
if (reminder.color !== null) {
entry.style.setProperty('--color', reminder.color);
};
const title = document.createElement('h2');
title.innerText = reminder.title;

View File

@@ -1,13 +1,15 @@
const template_inputs = {
'title': document.getElementById('title-template-input'),
'notification-service': document.getElementById('notification-service-template-input'),
'text': document.getElementById('text-template-input')
'text': document.getElementById('text-template-input'),
'color': document.querySelector('#add-template .color-list')
};
const edit_template_inputs = {
'title': document.getElementById('title-template-edit-input'),
'notification-service': document.getElementById('notification-service-template-edit-input'),
'text': document.getElementById('text-template-edit-input')
'text': document.getElementById('text-template-edit-input'),
'color': document.querySelector('#edit-template .color-list')
};
function loadTemplates(force=true) {
@@ -68,6 +70,9 @@ function loadTemplate() {
inputs.title.value = '';
inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value;
inputs.text.value = '';
if (!inputs.color.classList.contains('hidden')) {
toggleColor(inputs.color);
};
} else {
fetch(`/api/templates/${id}?api_key=${api_key}`)
.then(response => {
@@ -81,6 +86,16 @@ function loadTemplate() {
inputs.title.value = json.result.title;
inputs.notification_service.value = json.result.notification_service;
inputs.text.value = json.result.text;
if (json.result.color !== null) {
if (inputs.color.classList.contains('hidden')) {
toggleColor(inputs.color);
};
selectColor(inputs.color, json.result.color);
} else {
if (!inputs.color.classList.contains('hidden')) {
toggleColor(inputs.color);
};
};
})
.catch(e => {
if (e === 401) {
@@ -96,7 +111,11 @@ function addTemplate() {
const data = {
'title': template_inputs.title.value,
'notification_service': template_inputs["notification-service"].value,
'text': template_inputs.text.value
'text': template_inputs.text.value,
'color': null
};
if (!template_inputs.color.classList.contains('hidden')) {
data['color'] = template_inputs.color.querySelector('button[data-selected="true"]').dataset.color;
};
fetch(`/api/templates?api_key=${api_key}`, {
'method': 'POST',
@@ -128,6 +147,9 @@ function closeAddTemplate() {
template_inputs.title.value = '';
template_inputs['notification-service'].value = document.querySelector('#notification-service-template-input option[selected]').value;
template_inputs.text.value = '';
if (!template_inputs.color.classList.contains('hidden')) {
toggleColor(inputs.color);
};
}, 500);
};
@@ -145,6 +167,12 @@ function showEditTemplate(id) {
edit_template_inputs.title.value = json.result.title;
edit_template_inputs['notification-service'].value = json.result.notification_service;
edit_template_inputs.text.value = json.result.text;
if (json.result.color !== null) {
if (edit_template_inputs.color.classList.contains('hidden')) {
toggleColor(edit_template_inputs.color);
};
selectColor(edit_template_inputs.color, json.result.color);
};
showWindow('edit-template');
})
.catch(e => {
@@ -161,7 +189,11 @@ function saveTemplate() {
const data = {
'title': edit_template_inputs.title.value,
'notification_service': edit_template_inputs['notification-service'].value,
'text': edit_template_inputs.text.value
'text': edit_template_inputs.text.value,
'color': null
};
if (!edit_template_inputs.color.classList.contains('hidden')) {
data['color'] = edit_template_inputs.color.querySelector('button[data-selected="true"]').dataset.color;
};
fetch(`/api/templates/${id}?api_key=${api_key}`, {
'method': 'PUT',
@@ -211,7 +243,9 @@ function deleteTemplate() {
// code run on load
document.getElementById('template-form').setAttribute('action', 'javascript:addTemplate();');
document.getElementById('color-template-toggle').addEventListener('click', e => toggleColor(template_inputs.color));
document.getElementById('close-template').addEventListener('click', e => closeAddTemplate());
document.getElementById('template-edit-form').setAttribute('action', 'javascript:saveTemplate()');
document.getElementById('color-template-edit-toggle').addEventListener('click', e => toggleColor(edit_template_inputs.color));
document.getElementById('close-edit-template').addEventListener('click', e => hideWindow());
document.getElementById('delete-template').addEventListener('click', e => deleteTemplate());

View File

@@ -137,9 +137,13 @@
<h2>Add a reminder</h2>
<div class="form-container">
<form id="add-form">
<select id="template-selection">
<option value="0" selected>No template</option>
</select>
<div class="sub-inputs">
<select id="template-selection">
<option value="0" selected>No template</option>
</select>
<button type="button" id="color-toggle">Color</button>
</div>
<div class="color-list hidden"></div>
<input type="text" id="title-input" placeholder="Title" required>
<div class="sub-inputs">
<input type="datetime-local" id="time-input" required>
@@ -178,6 +182,10 @@
<textarea id="text-input" cols="30" rows="10" placeholder="Text (optional)"></textarea>
<div class="options">
<button type="button" id="close-add">Cancel</button>
<button type="button" id="test-reminder">
<div>Test</div>
<div>Sent</div>
</button>
<button type="submit">Add</button>
</div>
</form>
@@ -187,6 +195,8 @@
<h2>Edit a reminder</h2>
<div class="form-container">
<form id="edit-form">
<button type="button" id="color-edit-toggle">Color</button>
<div class="color-list hidden"></div>
<input type="text" id="title-edit-input" placeholder="Title" required>
<div class="sub-inputs">
<input type="datetime-local" id="time-edit-input" required>
@@ -298,6 +308,8 @@
<h2>Add a template</h2>
<div class="form-container">
<form id="template-form">
<button type="button" id="color-template-toggle">Color</button>
<div class="color-list hidden"></div>
<input type="text" id="title-template-input" placeholder="Title" required>
<select id="notification-service-template-input" required></select>
<textarea id="text-template-input" cols="30" rows="10" placeholder="Text (optional)"></textarea>
@@ -312,6 +324,8 @@
<h2>Edit a template</h2>
<div class="form-container">
<form id="template-edit-form">
<button type="button" id="color-template-edit-toggle">Color</button>
<div class="color-list hidden"></div>
<input type="text" id="title-template-edit-input" placeholder="Title" required>
<select id="notification-service-template-edit-input" required></select>
<textarea id="text-template-edit-input" cols="30" rows="10" placeholder="Text (optional)"></textarea>