mirror of
https://github.com/Casvt/MIND.git
synced 2026-02-19 11:54:46 -05:00
14
MIND.py
14
MIND.py
@@ -7,7 +7,7 @@ from os.path import abspath, dirname, isfile, join
|
||||
from shutil import move
|
||||
from sys import version_info
|
||||
|
||||
from flask import Flask
|
||||
from flask import Flask, render_template, request
|
||||
from waitress.server import create_server
|
||||
from werkzeug.middleware.dispatcher import DispatcherMiddleware
|
||||
|
||||
@@ -57,6 +57,12 @@ def _create_app() -> Flask:
|
||||
@app.errorhandler(500)
|
||||
def internal_error(e):
|
||||
return {'error': 'Internal error', 'result': {}}, 500
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
if request.path.startswith('/api'):
|
||||
return {'error': 'Not Found', 'result': {}}, 404
|
||||
return render_template('page_not_found.html', url_prefix=logging.URL_PREFIX)
|
||||
|
||||
app.register_blueprint(ui)
|
||||
app.register_blueprint(api, url_prefix="/api")
|
||||
@@ -89,10 +95,7 @@ def MIND() -> None:
|
||||
makedirs(dirname(db_location), exist_ok=True)
|
||||
DBConnection.file = db_location
|
||||
setup_db()
|
||||
reminder_handler._find_next_reminder()
|
||||
|
||||
# Start thread
|
||||
reminder_handler.thread.start()
|
||||
reminder_handler.find_next_reminder()
|
||||
|
||||
# Create waitress server and run
|
||||
server = create_server(app, host=HOST, port=PORT, threads=THREADS)
|
||||
@@ -103,7 +106,6 @@ def MIND() -> None:
|
||||
# Stopping thread
|
||||
reminder_handler.stop_handling()
|
||||
|
||||
print('Bye')
|
||||
return
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
class UsernameTaken(Exception):
|
||||
"""The username is already taken"""
|
||||
api_response = {'error': 'UsernameTaken', 'result': {}, 'code': 400}
|
||||
|
||||
135
backend/db.py
135
backend/db.py
@@ -8,7 +8,7 @@ from typing import Union
|
||||
|
||||
from flask import g
|
||||
|
||||
__DATABASE_VERSION__ = 4
|
||||
__DATABASE_VERSION__ = 5
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
@@ -37,15 +37,15 @@ def get_db(output_type: Union[dict, tuple]=tuple):
|
||||
Cursor: The Cursor instance to use
|
||||
"""
|
||||
try:
|
||||
cursor = g.cursor
|
||||
cursor = g.cursor
|
||||
except AttributeError:
|
||||
db = DBConnection(timeout=20.0)
|
||||
cursor = g.cursor = db.cursor()
|
||||
db = DBConnection(timeout=20.0)
|
||||
cursor = g.cursor = db.cursor()
|
||||
|
||||
if output_type is dict:
|
||||
cursor.row_factory = Row
|
||||
cursor.row_factory = Row
|
||||
else:
|
||||
cursor.row_factory = None
|
||||
cursor.row_factory = None
|
||||
|
||||
return g.cursor
|
||||
|
||||
@@ -73,10 +73,10 @@ def migrate_db(current_db_version: int) -> None:
|
||||
# V1 -> V2
|
||||
t = time()
|
||||
utc_offset = datetime.fromtimestamp(t) - datetime.utcfromtimestamp(t)
|
||||
reminders = cursor.execute("SELECT time, id FROM reminders;").fetchall()
|
||||
cursor.execute("SELECT time, id FROM reminders;")
|
||||
new_reminders = []
|
||||
new_reminders_append = new_reminders.append
|
||||
for reminder in reminders:
|
||||
for reminder in cursor:
|
||||
new_reminders_append([round((datetime.fromtimestamp(reminder[0]) - utc_offset).timestamp()), reminder[1]])
|
||||
cursor.executemany("UPDATE reminders SET time = ? WHERE id = ?;", new_reminders)
|
||||
current_db_version = 2
|
||||
@@ -100,6 +100,87 @@ def migrate_db(current_db_version: int) -> None:
|
||||
""")
|
||||
current_db_version = 4
|
||||
|
||||
if current_db_version == 4:
|
||||
# V4 -> V5
|
||||
cursor.executescript("""
|
||||
BEGIN TRANSACTION;
|
||||
PRAGMA defer_foreign_keys = ON;
|
||||
|
||||
-- Reminders
|
||||
INSERT INTO reminder_services(reminder_id, notification_service_id)
|
||||
SELECT id, notification_service
|
||||
FROM reminders;
|
||||
|
||||
CREATE TEMPORARY TABLE temp_reminders AS
|
||||
SELECT id, user_id, title, text, time, repeat_quantity, repeat_interval, original_time, color
|
||||
FROM reminders;
|
||||
DROP TABLE reminders;
|
||||
CREATE TABLE reminders(
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
text TEXT,
|
||||
time INTEGER NOT NULL,
|
||||
|
||||
repeat_quantity VARCHAR(15),
|
||||
repeat_interval INTEGER,
|
||||
original_time INTEGER,
|
||||
|
||||
color VARCHAR(7),
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
INSERT INTO reminders
|
||||
SELECT * FROM temp_reminders;
|
||||
|
||||
-- Static reminders
|
||||
INSERT INTO reminder_services(static_reminder_id, notification_service_id)
|
||||
SELECT id, notification_service
|
||||
FROM static_reminders;
|
||||
|
||||
CREATE TEMPORARY TABLE temp_static_reminders AS
|
||||
SELECT id, user_id, title, text, color
|
||||
FROM static_reminders;
|
||||
DROP TABLE static_reminders;
|
||||
CREATE TABLE static_reminders(
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
text TEXT,
|
||||
|
||||
color VARCHAR(7),
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
INSERT INTO static_reminders
|
||||
SELECT * FROM temp_static_reminders;
|
||||
|
||||
-- Templates
|
||||
INSERT INTO reminder_services(template_id, notification_service_id)
|
||||
SELECT id, notification_service
|
||||
FROM templates;
|
||||
|
||||
CREATE TEMPORARY TABLE temp_templates AS
|
||||
SELECT id, user_id, title, text, color
|
||||
FROM templates;
|
||||
DROP TABLE templates;
|
||||
CREATE TABLE templates(
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
text TEXT,
|
||||
|
||||
color VARCHAR(7),
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
INSERT INTO templates
|
||||
SELECT * FROM temp_templates;
|
||||
|
||||
COMMIT;
|
||||
""")
|
||||
current_db_version = 5
|
||||
|
||||
return
|
||||
|
||||
def setup_db() -> None:
|
||||
@@ -128,7 +209,6 @@ def setup_db() -> None:
|
||||
title VARCHAR(255) NOT NULL,
|
||||
text TEXT,
|
||||
time INTEGER NOT NULL,
|
||||
notification_service INTEGER NOT NULL,
|
||||
|
||||
repeat_quantity VARCHAR(15),
|
||||
repeat_interval INTEGER,
|
||||
@@ -136,20 +216,41 @@ def setup_db() -> None:
|
||||
|
||||
color VARCHAR(7),
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (notification_service) REFERENCES notification_services(id)
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS templates(
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
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)
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS static_reminders(
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
text TEXT,
|
||||
|
||||
color VARCHAR(7),
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS reminder_services(
|
||||
reminder_id INTEGER,
|
||||
static_reminder_id INTEGER,
|
||||
template_id INTEGER,
|
||||
notification_service_id INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY (reminder_id) REFERENCES reminders(id)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY (static_reminder_id) REFERENCES static_reminders(id)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY (template_id) REFERENCES templates(id)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY (notification_service_id) REFERENCES notification_services(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS config(
|
||||
key VARCHAR(255) PRIMARY KEY,
|
||||
@@ -163,12 +264,14 @@ def setup_db() -> None:
|
||||
""",
|
||||
(__DATABASE_VERSION__,)
|
||||
)
|
||||
current_db_version = int(cursor.execute("SELECT value FROM config WHERE key = 'database_version' LIMIT 1;").fetchone()[0])
|
||||
current_db_version = int(cursor.execute(
|
||||
"SELECT value FROM config WHERE key = 'database_version' LIMIT 1;"
|
||||
).fetchone()[0])
|
||||
|
||||
if current_db_version < __DATABASE_VERSION__:
|
||||
migrate_db(current_db_version)
|
||||
cursor.execute(
|
||||
"UPDATE config SET value = ? WHERE key = 'database_version' LIMIT 1;",
|
||||
"UPDATE config SET value = ? WHERE key = 'database_version';",
|
||||
(__DATABASE_VERSION__,)
|
||||
)
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ from backend.custom_exceptions import (InvalidURL, NotificationServiceInUse,
|
||||
NotificationServiceNotFound)
|
||||
from backend.db import get_db
|
||||
|
||||
|
||||
class NotificationService:
|
||||
def __init__(self, notification_service_id: int) -> None:
|
||||
self.id = notification_service_id
|
||||
|
||||
if not get_db().execute(
|
||||
"SELECT 1 FROM notification_services WHERE id = ? LIMIT 1",
|
||||
"SELECT 1 FROM notification_services WHERE id = ? LIMIT 1;",
|
||||
(self.id,)
|
||||
).fetchone():
|
||||
raise NotificationServiceNotFound
|
||||
@@ -61,13 +62,15 @@ class NotificationService:
|
||||
# Update database
|
||||
get_db().execute("""
|
||||
UPDATE notification_services
|
||||
SET title=?, url=?
|
||||
SET title = ?, url = ?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
""",
|
||||
(
|
||||
data["title"],
|
||||
data["url"],
|
||||
self.id
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
return self.get()
|
||||
|
||||
@@ -79,21 +82,44 @@ class NotificationService:
|
||||
"""
|
||||
# Check if no reminders exist with this service
|
||||
cursor = get_db()
|
||||
cursor.execute(
|
||||
"SELECT 1 FROM reminders WHERE notification_service = ? LIMIT 1;",
|
||||
cursor.execute("""
|
||||
SELECT 1
|
||||
FROM reminder_services
|
||||
WHERE notification_service_id = ?
|
||||
AND reminder_id IS NOT NULL
|
||||
LIMIT 1;
|
||||
""",
|
||||
(self.id,)
|
||||
)
|
||||
if cursor.fetchone():
|
||||
raise NotificationServiceInUse('reminder')
|
||||
|
||||
# Check if no templates exist with this service
|
||||
cursor.execute(
|
||||
"SELECT 1 FROM templates WHERE notification_service = ? LIMIT 1;",
|
||||
cursor.execute("""
|
||||
SELECT 1
|
||||
FROM reminder_services
|
||||
WHERE notification_service_id = ?
|
||||
AND template_id IS NOT NULL
|
||||
LIMIT 1;
|
||||
""",
|
||||
(self.id,)
|
||||
)
|
||||
if cursor.fetchone():
|
||||
raise NotificationServiceInUse('template')
|
||||
|
||||
|
||||
# Check if no static reminders exist with this service
|
||||
cursor.execute("""
|
||||
SELECT 1
|
||||
FROM reminder_services
|
||||
WHERE notification_service_id = ?
|
||||
AND static_reminder_id IS NOT NULL
|
||||
LIMIT 1;
|
||||
""",
|
||||
(self.id,)
|
||||
)
|
||||
if cursor.fetchone():
|
||||
raise NotificationServiceInUse('static reminder')
|
||||
|
||||
cursor.execute(
|
||||
"DELETE FROM notification_services WHERE id = ?",
|
||||
(self.id,)
|
||||
@@ -103,17 +129,22 @@ class NotificationService:
|
||||
class NotificationServices:
|
||||
def __init__(self, user_id: int) -> None:
|
||||
self.user_id = user_id
|
||||
|
||||
|
||||
def fetchall(self) -> List[dict]:
|
||||
"""Get a list of all notification services
|
||||
|
||||
Returns:
|
||||
List[dict]: The list of all notification services
|
||||
"""
|
||||
result = list(map(dict, get_db(dict).execute(
|
||||
"SELECT id, title, url FROM notification_services WHERE user_id = ? ORDER BY title, id",
|
||||
result = list(map(dict, get_db(dict).execute("""
|
||||
SELECT
|
||||
id, title, url
|
||||
FROM notification_services
|
||||
WHERE user_id = ?
|
||||
ORDER BY title, id;
|
||||
""",
|
||||
(self.user_id,)
|
||||
).fetchall()))
|
||||
)))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
from datetime import datetime
|
||||
from sqlite3 import IntegrityError
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
from threading import Timer
|
||||
from typing import List, Literal
|
||||
|
||||
from apprise import Apprise
|
||||
@@ -33,98 +32,108 @@ def _find_next_time(
|
||||
new_time += td
|
||||
return int(new_time.timestamp())
|
||||
|
||||
class ReminderHandler():
|
||||
"""Run in a thread to handle the set reminders
|
||||
"""
|
||||
class ReminderHandler:
|
||||
"""Handle set reminders
|
||||
"""
|
||||
def __init__(self, context) -> None:
|
||||
self.context = context
|
||||
self.thread = Thread(target=self._handle, name='Reminder Handler')
|
||||
self.stop = False
|
||||
self.next_trigger = {
|
||||
'thread': None,
|
||||
'time': None
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
def _find_next_reminder(self) -> None:
|
||||
"""Note when next reminder is (could be in the past) or otherwise None
|
||||
def __trigger_reminders(self, time: int) -> None:
|
||||
"""Trigger all reminders that are set for a certain time
|
||||
|
||||
Args:
|
||||
time (int): The time of the reminders to trigger
|
||||
"""
|
||||
with self.context():
|
||||
next_timestamp = get_db().execute(
|
||||
"SELECT time FROM reminders ORDER BY time LIMIT 1;"
|
||||
).fetchone()
|
||||
cursor = get_db(dict)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
r.id,
|
||||
r.title, r.text,
|
||||
r.repeat_quantity, r.repeat_interval, r.original_time
|
||||
FROM reminders r
|
||||
WHERE time = ?;
|
||||
""", (time,))
|
||||
reminders = list(map(dict, cursor))
|
||||
|
||||
for reminder in reminders:
|
||||
cursor.execute("""
|
||||
SELECT url
|
||||
FROM reminder_services rs
|
||||
INNER JOIN notification_services ns
|
||||
ON rs.notification_service_id = ns.id
|
||||
WHERE rs.reminder_id = ?;
|
||||
""", (reminder['id'],))
|
||||
|
||||
# Send of reminder
|
||||
a = Apprise()
|
||||
for url in cursor:
|
||||
a.add(url['url'])
|
||||
a.notify(title=reminder["title"], body=reminder["text"])
|
||||
|
||||
if next_timestamp is None:
|
||||
self.next_reminder: None = next_timestamp
|
||||
else:
|
||||
self.next_reminder: int = next_timestamp[0]
|
||||
if reminder['repeat_quantity'] is None:
|
||||
# Delete the reminder from the database
|
||||
cursor.execute(
|
||||
"DELETE FROM reminders WHERE id = ?;",
|
||||
(reminder['id'],)
|
||||
)
|
||||
else:
|
||||
# Set next time
|
||||
new_time = _find_next_time(
|
||||
reminder['original_time'],
|
||||
reminder['repeat_quantity'],
|
||||
reminder['repeat_interval']
|
||||
)
|
||||
cursor.execute(
|
||||
"UPDATE reminders SET time = ? WHERE id = ?;",
|
||||
(new_time, reminder['id'])
|
||||
)
|
||||
|
||||
return
|
||||
self.next_trigger.update({
|
||||
'thread': None,
|
||||
'time': None
|
||||
})
|
||||
self.find_next_reminder()
|
||||
|
||||
def submit_next_reminder(self, timestamp: int=None) -> bool:
|
||||
if timestamp is None:
|
||||
self._find_next_reminder()
|
||||
return False
|
||||
|
||||
if self.next_reminder is None:
|
||||
self.next_reminder = timestamp
|
||||
return True
|
||||
def find_next_reminder(self, time: int=None) -> None:
|
||||
"""Determine when the soonest reminder is and set the timer to that time
|
||||
|
||||
if timestamp < self.next_reminder:
|
||||
self.next_reminder = timestamp
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _handle(self) -> None:
|
||||
while not self.stop:
|
||||
if self.next_reminder and self.next_reminder <= datetime.utcnow().timestamp():
|
||||
with self.context():
|
||||
cursor = get_db(dict)
|
||||
# Get all reminders for the timestamp
|
||||
reminders = cursor.execute("""
|
||||
SELECT
|
||||
id,
|
||||
notification_service, title, text,
|
||||
repeat_quantity, repeat_interval, original_time
|
||||
FROM reminders
|
||||
WHERE time = ?;
|
||||
""",
|
||||
(self.next_reminder,)
|
||||
).fetchall()
|
||||
|
||||
for reminder in reminders:
|
||||
# Send of reminder
|
||||
a = Apprise()
|
||||
url = cursor.execute(
|
||||
"SELECT url FROM notification_services WHERE id = ?",
|
||||
(reminder["notification_service"],)
|
||||
).fetchone()["url"]
|
||||
a.add(url)
|
||||
a.notify(title=reminder["title"], body=reminder["text"])
|
||||
|
||||
if reminder['repeat_quantity'] is None:
|
||||
# Delete the reminders from the database
|
||||
cursor.execute("DELETE FROM reminders WHERE id = ?", (reminder['id'],))
|
||||
else:
|
||||
# Set next time
|
||||
new_time = _find_next_time(
|
||||
reminder['original_time'],
|
||||
reminder['repeat_quantity'],
|
||||
reminder['repeat_interval']
|
||||
)
|
||||
self.submit_next_reminder(new_time)
|
||||
cursor.execute(
|
||||
"UPDATE reminders SET time = ? WHERE id = ?;",
|
||||
(new_time, reminder['id'])
|
||||
)
|
||||
|
||||
# Note when next reminder is (could be in the past) or otherwise None
|
||||
self._find_next_reminder()
|
||||
|
||||
sleep(5)
|
||||
return
|
||||
Args:
|
||||
time (int, optional): The timestamp to check for. Otherwise check soonest in database. Defaults to None.
|
||||
"""
|
||||
if not time:
|
||||
with self.context():
|
||||
time = get_db().execute("""
|
||||
SELECT DISTINCT r1.time
|
||||
FROM reminders r1
|
||||
LEFT JOIN reminders r2
|
||||
ON r1.time > r2.time
|
||||
WHERE r2.id IS NULL;
|
||||
""").fetchone()
|
||||
if time is None:
|
||||
return
|
||||
time = time[0]
|
||||
|
||||
if (self.next_trigger['thread'] is None
|
||||
or time < self.next_trigger['time']):
|
||||
if self.next_trigger['thread'] is not None:
|
||||
self.next_trigger['thread'].cancel()
|
||||
t = time - datetime.utcnow().timestamp()
|
||||
self.next_trigger['thread'] = Timer(t, self.__trigger_reminders, (time,))
|
||||
self.next_trigger['thread'].start()
|
||||
self.next_trigger['time'] = time
|
||||
|
||||
def stop_handling(self) -> None:
|
||||
self.stop = True
|
||||
self.thread.join()
|
||||
"""Stop the timer if it's active
|
||||
"""
|
||||
if self.next_trigger['thread'] is not None:
|
||||
self.next_trigger['thread'].cancel()
|
||||
return
|
||||
|
||||
handler_context = Flask('handler')
|
||||
@@ -138,7 +147,10 @@ class Reminder:
|
||||
self.id = reminder_id
|
||||
|
||||
# Check if reminder exists
|
||||
if not get_db().execute("SELECT 1 FROM reminders WHERE id = ? LIMIT 1", (self.id,)).fetchone():
|
||||
if not get_db().execute(
|
||||
"SELECT 1 FROM reminders WHERE id = ? LIMIT 1",
|
||||
(self.id,)
|
||||
).fetchone():
|
||||
raise ReminderNotFound
|
||||
|
||||
def get(self) -> dict:
|
||||
@@ -147,33 +159,35 @@ class Reminder:
|
||||
Returns:
|
||||
dict: The info about the reminder
|
||||
"""
|
||||
reminder: dict = get_db(dict).execute("""
|
||||
reminder = get_db(dict).execute("""
|
||||
SELECT
|
||||
r.id,
|
||||
r.title, r.text,
|
||||
r.time,
|
||||
r.notification_service,
|
||||
ns.title AS notification_service_title,
|
||||
r.repeat_quantity,
|
||||
r.repeat_interval,
|
||||
r.color
|
||||
FROM
|
||||
reminders r
|
||||
INNER JOIN notification_services ns
|
||||
ON
|
||||
r.notification_service = ns.id
|
||||
AND r.id = ?;
|
||||
id,
|
||||
title, text,
|
||||
time,
|
||||
repeat_quantity,
|
||||
repeat_interval,
|
||||
color
|
||||
FROM reminders
|
||||
WHERE id = ?
|
||||
LIMIT 1;
|
||||
""",
|
||||
(self.id,)
|
||||
).fetchone()
|
||||
reminder = dict(reminder)
|
||||
|
||||
return dict(reminder)
|
||||
reminder['notification_services'] = list(map(lambda r: r[0], get_db().execute("""
|
||||
SELECT notification_service_id
|
||||
FROM reminder_services
|
||||
WHERE reminder_id = ?;
|
||||
""", (self.id,))))
|
||||
|
||||
return reminder
|
||||
|
||||
def update(
|
||||
self,
|
||||
title: str = None,
|
||||
time: int = None,
|
||||
notification_service: int = None,
|
||||
notification_services: List[int] = None,
|
||||
text: str = None,
|
||||
repeat_quantity: Literal["years", "months", "weeks", "days", "hours", "minutes"] = None,
|
||||
repeat_interval: int = None,
|
||||
@@ -184,12 +198,15 @@ class Reminder:
|
||||
Args:
|
||||
title (str): The new title of the entry. Defaults to None.
|
||||
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.
|
||||
notification_services (List[int]): The new list of id's of the notification services to use to send the reminder. Defaults to None.
|
||||
text (str, optional): The new body of the reminder. Defaults to None.
|
||||
repeat_quantity (Literal["years", "months", "weeks", "days", "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.
|
||||
|
||||
Raises:
|
||||
NotificationServiceNotFound: One of the notification services was not found
|
||||
|
||||
Returns:
|
||||
dict: The new reminder info
|
||||
"""
|
||||
@@ -213,7 +230,6 @@ class Reminder:
|
||||
new_values = {
|
||||
'title': title,
|
||||
'time': time,
|
||||
'notification_service': notification_service,
|
||||
'text': text,
|
||||
'repeat_quantity': repeat_quantity,
|
||||
'repeat_interval': repeat_interval,
|
||||
@@ -224,51 +240,71 @@ class Reminder:
|
||||
data[k] = v
|
||||
|
||||
# Update database
|
||||
try:
|
||||
if not repeated_reminder:
|
||||
next_time = data["time"]
|
||||
cursor.execute("""
|
||||
UPDATE reminders
|
||||
SET title=?, text=?, time=?, notification_service=?, repeat_quantity=?, repeat_interval=?, color=?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
data["title"],
|
||||
data["text"],
|
||||
data["time"],
|
||||
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=?, color=?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
data["title"],
|
||||
data["text"],
|
||||
next_time,
|
||||
data["notification_service"],
|
||||
data["repeat_quantity"],
|
||||
data["repeat_interval"],
|
||||
data["time"],
|
||||
data["color"],
|
||||
self.id
|
||||
))
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
reminder_handler.submit_next_reminder(next_time)
|
||||
if not repeated_reminder:
|
||||
next_time = data["time"]
|
||||
cursor.execute("""
|
||||
UPDATE reminders
|
||||
SET
|
||||
title=?, text=?,
|
||||
time=?,
|
||||
repeat_quantity=?, repeat_interval=?,
|
||||
color=?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
data["title"],
|
||||
data["text"],
|
||||
data["time"],
|
||||
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=?,
|
||||
repeat_quantity=?, repeat_interval=?, original_time=?,
|
||||
color=?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
data["title"],
|
||||
data["text"],
|
||||
next_time,
|
||||
data["repeat_quantity"],
|
||||
data["repeat_interval"],
|
||||
data["time"],
|
||||
data["color"],
|
||||
self.id
|
||||
))
|
||||
|
||||
if notification_services:
|
||||
cursor.connection.isolation_level = None
|
||||
cursor.execute("BEGIN TRANSACTION;")
|
||||
cursor.execute("DELETE FROM reminder_services WHERE reminder_id = ?", (self.id,))
|
||||
try:
|
||||
cursor.executemany(
|
||||
"INSERT INTO reminder_services(reminder_id, notification_service_id) VALUES (?,?)",
|
||||
((self.id, s) for s in notification_services)
|
||||
)
|
||||
cursor.execute("COMMIT;")
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
cursor.connection.isolation_level = ""
|
||||
|
||||
reminder_handler.find_next_reminder(next_time)
|
||||
return self.get()
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Delete the reminder
|
||||
"""
|
||||
get_db().execute("DELETE FROM reminders WHERE id = ?", (self.id,))
|
||||
reminder_handler.submit_next_reminder(None)
|
||||
reminder_handler.find_next_reminder()
|
||||
return
|
||||
|
||||
class Reminders:
|
||||
@@ -291,7 +327,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, notification_service_title and color of each reminder
|
||||
List[dict]: The id, title, text, time and color of each reminder
|
||||
"""
|
||||
sort_function = self.sort_functions.get(
|
||||
sort_by,
|
||||
@@ -301,23 +337,17 @@ class Reminders:
|
||||
# Fetch all reminders
|
||||
reminders: list = list(map(dict, get_db(dict).execute("""
|
||||
SELECT
|
||||
r.id,
|
||||
r.title, r.text,
|
||||
r.time,
|
||||
r.notification_service,
|
||||
ns.title AS notification_service_title,
|
||||
r.repeat_quantity,
|
||||
r.repeat_interval,
|
||||
r.color
|
||||
FROM
|
||||
reminders r
|
||||
INNER JOIN notification_services ns
|
||||
ON
|
||||
r.notification_service = ns.id
|
||||
AND r.user_id = ?;
|
||||
id,
|
||||
title, text,
|
||||
time,
|
||||
repeat_quantity,
|
||||
repeat_interval,
|
||||
color
|
||||
FROM reminders
|
||||
WHERE user_id = ?;
|
||||
""",
|
||||
(self.user_id,)
|
||||
).fetchall()))
|
||||
)))
|
||||
|
||||
# Sort result
|
||||
reminders.sort(key=sort_function[0], reverse=sort_function[1])
|
||||
@@ -355,7 +385,7 @@ class Reminders:
|
||||
self,
|
||||
title: str,
|
||||
time: int,
|
||||
notification_service: int,
|
||||
notification_services: List[int],
|
||||
text: str = '',
|
||||
repeat_quantity: Literal["years", "months", "weeks", "days", "hours", "minutes"] = None,
|
||||
repeat_interval: int = None,
|
||||
@@ -366,12 +396,15 @@ class Reminders:
|
||||
Args:
|
||||
title (str): The title of the entry
|
||||
time (int): The UTC epoch timestamp the the reminder should be send.
|
||||
notification_service (int): The id of the notification service to use to send the reminder.
|
||||
notification_services (List[int]): The id's of the notification services to use to send the reminder.
|
||||
text (str, optional): The body of the reminder. Defaults to ''.
|
||||
repeat_quantity (Literal["years", "months", "weeks", "days", "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.
|
||||
|
||||
Raises:
|
||||
NotificationServiceNotFound: One of the notification services was not found
|
||||
|
||||
Returns:
|
||||
dict: The info about the reminder
|
||||
"""
|
||||
@@ -384,29 +417,36 @@ class Reminders:
|
||||
elif repeat_quantity is not None and repeat_interval is None:
|
||||
raise InvalidKeyValue('repeat_interval', repeat_interval)
|
||||
|
||||
cursor = get_db()
|
||||
if repeat_quantity is None and repeat_interval is None:
|
||||
id = cursor.execute("""
|
||||
INSERT INTO reminders(user_id, title, text, time, color)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
""", (self.user_id, title, text, time, color)
|
||||
).lastrowid
|
||||
else:
|
||||
id = cursor.execute("""
|
||||
INSERT INTO reminders(user_id, title, text, time, repeat_quantity, repeat_interval, original_time, color)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
""", (self.user_id, title, text, time, repeat_quantity, repeat_interval, time, color)
|
||||
).lastrowid
|
||||
|
||||
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, 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, color)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
""", (self.user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, time, color)
|
||||
).lastrowid
|
||||
cursor.executemany(
|
||||
"INSERT INTO reminder_services(reminder_id, notification_service_id) VALUES (?, ?);",
|
||||
((id, service) for service in notification_services)
|
||||
)
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
reminder_handler.submit_next_reminder(time)
|
||||
|
||||
reminder_handler.find_next_reminder(time)
|
||||
|
||||
# Return info
|
||||
return self.fetchone(id)
|
||||
|
||||
def test_reminder(
|
||||
title: str,
|
||||
notification_service: int,
|
||||
notification_services: List[int],
|
||||
text: str = ''
|
||||
) -> None:
|
||||
"""Test send a reminder draft
|
||||
@@ -417,12 +457,14 @@ def test_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])
|
||||
cursor = get_db(dict)
|
||||
for service in notification_services:
|
||||
url = cursor.execute(
|
||||
"SELECT url FROM notification_services WHERE id = ? LIMIT 1;",
|
||||
(service,)
|
||||
).fetchone()
|
||||
if not url:
|
||||
raise NotificationServiceNotFound
|
||||
a.add(url[0])
|
||||
a.notify(title=title, body=text)
|
||||
return
|
||||
|
||||
@@ -5,6 +5,7 @@ from hashlib import pbkdf2_hmac
|
||||
from secrets import token_bytes
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def get_hash(salt: bytes, data: str) -> bytes:
|
||||
"""Hash a string using the supplied salt
|
||||
|
||||
|
||||
232
backend/static_reminders.py
Normal file
232
backend/static_reminders.py
Normal file
@@ -0,0 +1,232 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
from sqlite3 import IntegrityError
|
||||
from typing import List
|
||||
|
||||
from apprise import Apprise
|
||||
|
||||
from backend.custom_exceptions import (NotificationServiceNotFound,
|
||||
ReminderNotFound)
|
||||
from backend.db import get_db
|
||||
|
||||
|
||||
class StaticReminder:
|
||||
"""Represents a static reminder
|
||||
"""
|
||||
def __init__(self, reminder_id: int) -> None:
|
||||
self.id = reminder_id
|
||||
|
||||
# Check if reminder exists
|
||||
if not get_db().execute(
|
||||
"SELECT 1 FROM static_reminders WHERE id = ? LIMIT 1;",
|
||||
(self.id,)
|
||||
).fetchone():
|
||||
raise ReminderNotFound
|
||||
|
||||
def get(self) -> dict:
|
||||
"""Get info about the static reminder
|
||||
|
||||
Returns:
|
||||
dict: The info about the reminder
|
||||
"""
|
||||
reminder = get_db(dict).execute("""
|
||||
SELECT
|
||||
id,
|
||||
title, text,
|
||||
color
|
||||
FROM static_reminders
|
||||
WHERE id = ?
|
||||
LIMIT 1;
|
||||
""",
|
||||
(self.id,)
|
||||
).fetchone()
|
||||
reminder = dict(reminder)
|
||||
|
||||
reminder['notification_services'] = list(map(lambda r: r[0], get_db().execute("""
|
||||
SELECT notification_service_id
|
||||
FROM reminder_services
|
||||
WHERE static_reminder_id = ?;
|
||||
""", (self.id,))))
|
||||
|
||||
return reminder
|
||||
|
||||
def update(
|
||||
self,
|
||||
title: str = None,
|
||||
notification_services: List[int] = None,
|
||||
text: str = None,
|
||||
color: str = None
|
||||
) -> dict:
|
||||
"""Edit the static reminder
|
||||
|
||||
Args:
|
||||
title (str, optional): The new title of the entry. Defaults to None.
|
||||
notification_services (List[int], optional): The new id's of the notification services to use to send the reminder. Defaults to None.
|
||||
text (str, optional): The new body of the reminder. 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.
|
||||
|
||||
Raises:
|
||||
NotificationServiceNotFound: One of the notification services was not found
|
||||
|
||||
Returns:
|
||||
dict: The new static reminder info
|
||||
"""
|
||||
# Get current data and update it with new values
|
||||
data = self.get()
|
||||
new_values = {
|
||||
'title': title,
|
||||
'text': text,
|
||||
'color': color
|
||||
}
|
||||
for k, v in new_values.items():
|
||||
if k == 'color' or v is not None:
|
||||
data[k] = v
|
||||
|
||||
# Update database
|
||||
cursor = get_db()
|
||||
cursor.execute("""
|
||||
UPDATE static_reminders
|
||||
SET
|
||||
title = ?, text = ?,
|
||||
color = ?
|
||||
WHERE id = ?;
|
||||
""",
|
||||
(data['title'], data['text'],
|
||||
data['color'],
|
||||
self.id)
|
||||
)
|
||||
|
||||
if notification_services:
|
||||
cursor.connection.isolation_level = None
|
||||
cursor.execute("BEGIN TRANSACTION;")
|
||||
cursor.execute("DELETE FROM reminder_services WHERE static_reminder_id = ?", (self.id,))
|
||||
try:
|
||||
cursor.executemany(
|
||||
"INSERT INTO reminder_services(static_reminder_id, notification_service_id) VALUES (?,?)",
|
||||
((self.id, s) for s in notification_services)
|
||||
)
|
||||
cursor.execute("COMMIT;")
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
cursor.connection.isolation_level = ""
|
||||
|
||||
return self.get()
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Delete the static reminder
|
||||
"""
|
||||
get_db().execute("DELETE FROM static_reminders WHERE id = ?", (self.id,))
|
||||
return
|
||||
|
||||
class StaticReminders:
|
||||
"""Represents the static reminder library of the user account
|
||||
"""
|
||||
|
||||
def __init__(self, user_id: int) -> None:
|
||||
self.user_id = user_id
|
||||
|
||||
def fetchall(self) -> List[dict]:
|
||||
"""Get all static reminders
|
||||
|
||||
Returns:
|
||||
List[dict]: The id, title, text and color of each static reminder
|
||||
"""
|
||||
reminders: list = list(map(
|
||||
dict,
|
||||
get_db(dict).execute("""
|
||||
SELECT
|
||||
id,
|
||||
title, text,
|
||||
color
|
||||
FROM static_reminders
|
||||
WHERE user_id = ?
|
||||
ORDER BY title, id;
|
||||
""",
|
||||
(self.user_id,)
|
||||
)
|
||||
))
|
||||
|
||||
return reminders
|
||||
|
||||
def fetchone(self, id: int) -> StaticReminder:
|
||||
"""Get one static reminder
|
||||
|
||||
Args:
|
||||
id (int): The id of the static reminder to fetch
|
||||
|
||||
Returns:
|
||||
StaticReminder: A StaticReminder instance
|
||||
"""
|
||||
return StaticReminder(id)
|
||||
|
||||
def add(
|
||||
self,
|
||||
title: str,
|
||||
notification_services: List[int],
|
||||
text: str = '',
|
||||
color: str = None
|
||||
) -> StaticReminder:
|
||||
"""Add a static reminder
|
||||
|
||||
Args:
|
||||
title (str): The title of the entry
|
||||
notification_services (List[int]): The id's of the notification services 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 reminder, which is shown in the web-ui. Defaults to None.
|
||||
|
||||
Raises:
|
||||
NotificationServiceNotFound: One of the notification services was not found
|
||||
|
||||
Returns:
|
||||
StaticReminder: A StaticReminder instance representing the newly created static reminder
|
||||
"""
|
||||
cursor = get_db()
|
||||
id = cursor.execute("""
|
||||
INSERT INTO static_reminders(user_id, title, text, color)
|
||||
VALUES (?,?,?,?);
|
||||
""",
|
||||
(self.user_id, title, text, color)
|
||||
).lastrowid
|
||||
|
||||
try:
|
||||
cursor.executemany(
|
||||
"INSERT INTO reminder_services(static_reminder_id, notification_service_id) VALUES (?, ?);",
|
||||
((id, service) for service in notification_services)
|
||||
)
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
|
||||
return self.fetchone(id)
|
||||
|
||||
def trigger_reminder(self, id: int) -> None:
|
||||
"""Trigger a static reminder to send it's reminder
|
||||
|
||||
Args:
|
||||
id (int): The id of the static reminder to trigger
|
||||
|
||||
Raises:
|
||||
ReminderNotFound: The static reminder with the given id was not found
|
||||
"""
|
||||
cursor = get_db(dict)
|
||||
reminder = cursor.execute("""
|
||||
SELECT title, text
|
||||
FROM static_reminders
|
||||
WHERE id = ?
|
||||
LIMIT 1;
|
||||
""", (id,)).fetchone()
|
||||
if not reminder:
|
||||
raise ReminderNotFound
|
||||
reminder = dict(reminder)
|
||||
|
||||
a = Apprise()
|
||||
cursor.execute("""
|
||||
SELECT url
|
||||
FROM reminder_services rs
|
||||
INNER JOIN notification_services ns
|
||||
ON rs.notification_service_id = ns.id
|
||||
WHERE rs.static_reminder_id = ?;
|
||||
""", (id,))
|
||||
for url in cursor:
|
||||
a.add(url['url'])
|
||||
a.notify(title=reminder['title'], body=reminder['text'])
|
||||
return
|
||||
@@ -7,13 +7,17 @@ from backend.custom_exceptions import (NotificationServiceNotFound,
|
||||
TemplateNotFound)
|
||||
from backend.db import get_db
|
||||
|
||||
|
||||
class Template:
|
||||
"""Represents a template
|
||||
"""
|
||||
def __init__(self, template_id: int):
|
||||
self.id = template_id
|
||||
|
||||
exists = get_db().execute("SELECT 1 FROM templates WHERE id = ? LIMIT 1;", (self.id,)).fetchone()
|
||||
exists = get_db().execute(
|
||||
"SELECT 1 FROM templates WHERE id = ? LIMIT 1;",
|
||||
(self.id,)
|
||||
).fetchone()
|
||||
if not exists:
|
||||
raise TemplateNotFound
|
||||
|
||||
@@ -27,19 +31,26 @@ class Template:
|
||||
SELECT
|
||||
id,
|
||||
title, text,
|
||||
notification_service,
|
||||
color
|
||||
FROM templates
|
||||
WHERE id = ?;
|
||||
WHERE id = ?
|
||||
LIMIT 1;
|
||||
""",
|
||||
(self.id,)
|
||||
).fetchone()
|
||||
template = dict(template)
|
||||
|
||||
return dict(template)
|
||||
template['notification_services'] = list(map(lambda r: r[0], get_db().execute("""
|
||||
SELECT notification_service_id
|
||||
FROM reminder_services
|
||||
WHERE template_id = ?;
|
||||
""", (self.id,))))
|
||||
|
||||
return template
|
||||
|
||||
def update(self,
|
||||
title: str = None,
|
||||
notification_service: int = None,
|
||||
notification_services: List[int] = None,
|
||||
text: str = None,
|
||||
color: str = None
|
||||
) -> dict:
|
||||
@@ -47,7 +58,7 @@ class Template:
|
||||
|
||||
Args:
|
||||
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.
|
||||
notification_services (List[int]): The new id's of the notification services 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.
|
||||
|
||||
@@ -59,7 +70,6 @@ class Template:
|
||||
data = self.get()
|
||||
new_values = {
|
||||
'title': title,
|
||||
'notification_service': notification_service,
|
||||
'text': text,
|
||||
'color': color
|
||||
}
|
||||
@@ -67,20 +77,30 @@ class Template:
|
||||
if k in ('color',) or v is not None:
|
||||
data[k] = v
|
||||
|
||||
try:
|
||||
cursor.execute("""
|
||||
UPDATE templates
|
||||
SET title=?, notification_service=?, text=?, color=?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
data['title'],
|
||||
data['notification_service'],
|
||||
data['text'],
|
||||
data['color'],
|
||||
self.id
|
||||
))
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
cursor.execute("""
|
||||
UPDATE templates
|
||||
SET title=?, text=?, color=?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
data['title'],
|
||||
data['text'],
|
||||
data['color'],
|
||||
self.id
|
||||
))
|
||||
|
||||
if notification_services:
|
||||
cursor.connection.isolation_level = None
|
||||
cursor.execute("BEGIN TRANSACTION;")
|
||||
cursor.execute("DELETE FROM reminder_services WHERE template_id = ?", (self.id,))
|
||||
try:
|
||||
cursor.executemany(
|
||||
"INSERT INTO reminder_services(template_id, notification_service_id) VALUES (?,?)",
|
||||
((self.id, s) for s in notification_services)
|
||||
)
|
||||
cursor.execute("COMMIT;")
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
cursor.connection.isolation_level = ""
|
||||
|
||||
return self.get()
|
||||
|
||||
@@ -100,20 +120,19 @@ class Templates:
|
||||
"""Get all templates
|
||||
|
||||
Returns:
|
||||
List[dict]: The id, title, text, notification_service and color
|
||||
List[dict]: The id, title, text and color
|
||||
"""
|
||||
templates: list = list(map(dict, get_db(dict).execute("""
|
||||
SELECT
|
||||
id,
|
||||
title, text,
|
||||
notification_service,
|
||||
color
|
||||
FROM templates
|
||||
WHERE user_id = ?
|
||||
ORDER BY title, id;
|
||||
""",
|
||||
(self.user_id,)
|
||||
).fetchall()))
|
||||
)))
|
||||
|
||||
return templates
|
||||
|
||||
@@ -131,7 +150,7 @@ class Templates:
|
||||
def add(
|
||||
self,
|
||||
title: str,
|
||||
notification_service: int,
|
||||
notification_services: List[int],
|
||||
text: str = '',
|
||||
color: str = None
|
||||
) -> Template:
|
||||
@@ -139,20 +158,26 @@ class Templates:
|
||||
|
||||
Args:
|
||||
title (str): The title of the entry
|
||||
notification_service (int): The id of the notification service to use to send the reminder.
|
||||
notification_services (List[int]): The id's of the notification services 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
|
||||
"""
|
||||
cursor = get_db()
|
||||
id = cursor.execute("""
|
||||
INSERT INTO templates(user_id, title, text, color)
|
||||
VALUES (?,?,?,?);
|
||||
""",
|
||||
(self.user_id, title, text, color)
|
||||
).lastrowid
|
||||
|
||||
try:
|
||||
id = get_db().execute("""
|
||||
INSERT INTO templates(user_id, title, text, notification_service, color)
|
||||
VALUES (?,?,?,?,?);
|
||||
""",
|
||||
(self.user_id, title, text, notification_service, color)
|
||||
).lastrowid
|
||||
cursor.executemany(
|
||||
"INSERT INTO reminder_services(template_id, notification_service_id) VALUES (?, ?);",
|
||||
((id, service) for service in notification_services)
|
||||
)
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from backend.db import get_db
|
||||
from backend.notification_service import NotificationServices
|
||||
from backend.reminders import Reminders
|
||||
from backend.security import generate_salt_hash, get_hash
|
||||
from backend.static_reminders import StaticReminders
|
||||
from backend.templates import Templates
|
||||
|
||||
ONEPASS_USERNAME_CHARACTERS = 'abcedfghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.!@$'
|
||||
@@ -17,7 +18,7 @@ class User:
|
||||
def __init__(self, username: str, password: str):
|
||||
# Fetch data of user to check if user exists and to check if password is correct
|
||||
result = get_db(dict).execute(
|
||||
"SELECT id, salt, hash FROM users WHERE username = ?",
|
||||
"SELECT id, salt, hash FROM users WHERE username = ? LIMIT 1;",
|
||||
(username,)
|
||||
).fetchone()
|
||||
if not result:
|
||||
@@ -64,6 +65,17 @@ class User:
|
||||
self.templates_instance = Templates(self.user_id)
|
||||
return self.templates_instance
|
||||
|
||||
@property
|
||||
def static_reminders(self) -> StaticReminders:
|
||||
"""Get access to the static reminders of the user account
|
||||
|
||||
Returns:
|
||||
StaticReminders: StaticReminders instance that can be used to access the static reminders of the user account
|
||||
"""
|
||||
if not hasattr(self, 'static_reminders_instance'):
|
||||
self.static_reminders_instance = StaticReminders(self.user_id)
|
||||
return self.static_reminders_instance
|
||||
|
||||
def edit_password(self, new_password: str) -> None:
|
||||
"""Change the password of the account
|
||||
|
||||
@@ -85,6 +97,8 @@ class User:
|
||||
"""
|
||||
cursor = get_db()
|
||||
cursor.execute("DELETE FROM reminders WHERE user_id = ?", (self.user_id,))
|
||||
cursor.execute("DELETE FROM templates WHERE user_id = ?", (self.user_id,))
|
||||
cursor.execute("DELETE FROM static_reminders WHERE user_id = ?", (self.user_id,))
|
||||
cursor.execute("DELETE FROM notification_services WHERE user_id = ?", (self.user_id,))
|
||||
cursor.execute("DELETE FROM users WHERE id = ?", (self.user_id,))
|
||||
return
|
||||
|
||||
192
frontend/api.py
192
frontend/api.py
@@ -1,9 +1,9 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
from os import urandom
|
||||
from re import compile
|
||||
from time import time as epoch_time
|
||||
from typing import Any, Tuple
|
||||
from re import compile
|
||||
|
||||
from flask import Blueprint, g, request
|
||||
|
||||
@@ -16,6 +16,7 @@ from backend.custom_exceptions import (AccessUnauthorized, InvalidKeyValue,
|
||||
from backend.notification_service import (NotificationService,
|
||||
NotificationServices)
|
||||
from backend.reminders import Reminders, reminder_handler, test_reminder
|
||||
from backend.static_reminders import StaticReminders
|
||||
from backend.templates import Template, Templates
|
||||
from backend.users import User, register_user
|
||||
|
||||
@@ -80,7 +81,7 @@ def extract_key(values: dict, key: str, check_existence: bool=True) -> Any:
|
||||
|
||||
if value is not None:
|
||||
# Check value and optionally convert
|
||||
if key in ('time', 'notification_service'):
|
||||
if key == 'time':
|
||||
try:
|
||||
value = int(value)
|
||||
except (ValueError, TypeError):
|
||||
@@ -111,6 +112,15 @@ def extract_key(values: dict, key: str, check_existence: bool=True) -> Any:
|
||||
if not color_regex.search(value):
|
||||
raise InvalidKeyValue(key, value)
|
||||
|
||||
elif key == 'notification_services':
|
||||
if not value:
|
||||
raise KeyNotFound(key)
|
||||
if not isinstance(value, list):
|
||||
raise InvalidKeyValue(key, value)
|
||||
for v in value:
|
||||
if not isinstance(v, int):
|
||||
raise InvalidKeyValue(key, value)
|
||||
|
||||
else:
|
||||
if key == 'sort_by':
|
||||
value = 'time'
|
||||
@@ -120,10 +130,6 @@ def extract_key(values: dict, key: str, check_existence: bool=True) -> Any:
|
||||
|
||||
return value
|
||||
|
||||
@api.errorhandler(404)
|
||||
def not_found(e):
|
||||
return return_api({}, 'Not Found', 404)
|
||||
|
||||
#===================
|
||||
# Authentication endpoints
|
||||
#===================
|
||||
@@ -247,7 +253,7 @@ def api_add_user():
|
||||
user_id = register_user(username, password)
|
||||
return return_api({'user_id': user_id}, code=201)
|
||||
|
||||
@api.route('/user', methods=['PUT','DELETE'])
|
||||
@api.route('/user', methods=['PUT', 'DELETE'])
|
||||
@error_handler
|
||||
@auth
|
||||
def api_manage_user():
|
||||
@@ -291,7 +297,7 @@ def api_manage_user():
|
||||
# Notification service endpoints
|
||||
#===================
|
||||
|
||||
@api.route('/notificationservices', methods=['GET','POST'])
|
||||
@api.route('/notificationservices', methods=['GET', 'POST'])
|
||||
@error_handler
|
||||
@auth
|
||||
def api_notification_services_list():
|
||||
@@ -391,7 +397,7 @@ def api_notification_service(n_id: int):
|
||||
# Library endpoints
|
||||
#===================
|
||||
|
||||
@api.route('/reminders', methods=['GET','POST'])
|
||||
@api.route('/reminders', methods=['GET', 'POST'])
|
||||
@error_handler
|
||||
@auth
|
||||
def api_reminders_list():
|
||||
@@ -406,13 +412,13 @@ 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, repeat_interval and color of each reminder
|
||||
The id, title, text, time, repeat_quantity, repeat_interval and color of each reminder
|
||||
POST:
|
||||
Description: Add a reminder
|
||||
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
|
||||
notification_services (required): array of the id's of the notification services to use to send the notification
|
||||
text: the body of the reminder
|
||||
repeat_quantity ('years', 'months', 'weeks', 'days', 'hours', 'minutes'): The quantity of the repeat_interval
|
||||
repeat_interval: The number of the interval
|
||||
@@ -422,6 +428,8 @@ def api_reminders_list():
|
||||
The info about the new reminder entry
|
||||
400:
|
||||
KeyNotFound: One of the required parameters was not given
|
||||
404:
|
||||
NotificationServiceNotFound: One of the notification services was not found
|
||||
"""
|
||||
reminders: Reminders = g.user_data.reminders
|
||||
|
||||
@@ -434,7 +442,7 @@ def api_reminders_list():
|
||||
data = request.get_json()
|
||||
title = extract_key(data, 'title')
|
||||
time = extract_key(data, 'time')
|
||||
notification_service = extract_key(data, 'notification_service')
|
||||
notification_services = extract_key(data, 'notification_services')
|
||||
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)
|
||||
@@ -442,7 +450,7 @@ def api_reminders_list():
|
||||
|
||||
result = reminders.add(title=title,
|
||||
time=time,
|
||||
notification_service=notification_service,
|
||||
notification_services=notification_services,
|
||||
text=text,
|
||||
repeat_quantity=repeat_quantity,
|
||||
repeat_interval=repeat_interval,
|
||||
@@ -496,13 +504,13 @@ def api_test_reminder():
|
||||
"""
|
||||
data = request.get_json()
|
||||
title = extract_key(data, 'title')
|
||||
notification_service = extract_key(data, 'notification_service')
|
||||
notification_services = extract_key(data, 'notification_services')
|
||||
text = extract_key(data, 'text', check_existence=False)
|
||||
|
||||
test_reminder(title, notification_service, text)
|
||||
test_reminder(title, notification_services, text)
|
||||
return return_api({}, code=201)
|
||||
|
||||
@api.route('/reminders/<int:r_id>', methods=['GET','PUT','DELETE'])
|
||||
@api.route('/reminders/<int:r_id>', methods=['GET', 'PUT', 'DELETE'])
|
||||
@error_handler
|
||||
@auth
|
||||
def api_get_reminder(r_id: int):
|
||||
@@ -525,7 +533,7 @@ def api_get_reminder(r_id: int):
|
||||
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.
|
||||
notification_services: Array of the new id's of the notification services to use to send the reminder.
|
||||
text: The new body of the reminder.
|
||||
repeat_quantity ('years', 'months', 'weeks', 'days', 'hours', 'minutes'): The new quantity of the repeat_interval.
|
||||
repeat_interval: The new number of the interval.
|
||||
@@ -534,7 +542,8 @@ def api_get_reminder(r_id: int):
|
||||
200:
|
||||
Reminder updated successfully
|
||||
404:
|
||||
No reminder found with the given id
|
||||
ReminderNotFound: No reminder found with the given id
|
||||
NotificationServiceNotFound: One of the notification services was not found
|
||||
DELETE:
|
||||
Description: Delete the reminder
|
||||
Returns:
|
||||
@@ -552,15 +561,15 @@ def api_get_reminder(r_id: int):
|
||||
data = request.get_json()
|
||||
title = extract_key(data, 'title', check_existence=False)
|
||||
time = extract_key(data, 'time', check_existence=False)
|
||||
notification_service = extract_key(data, 'notification_service', check_existence=False)
|
||||
notification_services = extract_key(data, 'notification_services', check_existence=False)
|
||||
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,
|
||||
notification_services=notification_services,
|
||||
text=text,
|
||||
repeat_quantity=repeat_quantity,
|
||||
repeat_interval=repeat_interval,
|
||||
@@ -588,12 +597,12 @@ def api_get_templates():
|
||||
Description: Get a list of all templates
|
||||
Returns:
|
||||
200:
|
||||
The id, title, notification_service, text and color of every template
|
||||
The id, title, text and color of every template
|
||||
POST:
|
||||
Description: Add a template
|
||||
Parameters (body):
|
||||
title (required): the title of the template
|
||||
notification_service (required): the id of the notification service to use to send the notification
|
||||
notification_services (required): array of the id's of the notification services 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:
|
||||
@@ -601,6 +610,8 @@ def api_get_templates():
|
||||
The info about the new template entry
|
||||
400:
|
||||
KeyNotFound: One of the required parameters was not given
|
||||
404:
|
||||
NotificationServiceNotFound: One of the notification services was not found
|
||||
"""
|
||||
templates: Templates = g.user_data.templates
|
||||
|
||||
@@ -611,12 +622,12 @@ def api_get_templates():
|
||||
elif request.method == 'POST':
|
||||
data = request.get_json()
|
||||
title = extract_key(data, 'title')
|
||||
notification_service = extract_key(data, 'notification_service')
|
||||
notification_services = extract_key(data, 'notification_services')
|
||||
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,
|
||||
notification_services=notification_services,
|
||||
text=text,
|
||||
color=color)
|
||||
return return_api(result.get(), code=201)
|
||||
@@ -643,14 +654,15 @@ def api_get_template(t_id: int):
|
||||
Description: Edit the template
|
||||
Parameters (body):
|
||||
title: The new title of the entry.
|
||||
notification_service: The new id of the notification service to use to send the reminder.
|
||||
notification_services: The new array of id's of the notification services 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
|
||||
404:
|
||||
No template found with the given id
|
||||
TemplateNotFound: No template found with the given id
|
||||
NotificationServiceNotFound: One of the notification services was not found
|
||||
DELETE:
|
||||
Description: Delete the template
|
||||
Returns:
|
||||
@@ -668,12 +680,12 @@ def api_get_template(t_id: int):
|
||||
elif request.method == 'PUT':
|
||||
data = request.get_json()
|
||||
title = extract_key(data, 'title', check_existence=False)
|
||||
notification_service = extract_key(data, 'notification_service', check_existence=False)
|
||||
notification_services = extract_key(data, 'notification_services', 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,
|
||||
notification_services=notification_services,
|
||||
text=text,
|
||||
color=color)
|
||||
return return_api(result)
|
||||
@@ -681,3 +693,125 @@ def api_get_template(t_id: int):
|
||||
elif request.method == 'DELETE':
|
||||
template.delete()
|
||||
return return_api({})
|
||||
|
||||
#===================
|
||||
# Static reminder endpoints
|
||||
#===================
|
||||
|
||||
@api.route('/staticreminders', methods=['GET', 'POST'])
|
||||
@error_handler
|
||||
@auth
|
||||
def api_static_reminders_list():
|
||||
"""
|
||||
Endpoint: /staticreminders
|
||||
Description: Manage the static reminders
|
||||
Requires being logged in: Yes
|
||||
Methods:
|
||||
GET:
|
||||
Description: Get a list of all static reminders
|
||||
Returns:
|
||||
200:
|
||||
The id, title, text and color of each static reminder
|
||||
POST:
|
||||
Description: Add a static reminder
|
||||
Parameters (body):
|
||||
title (required): the title of the static reminder
|
||||
notification_services (required): array of the id's of the notification services to use to send the notification
|
||||
text: the body of the static reminder
|
||||
color: The hex code of the color of the static reminder, which is shown in the web-ui
|
||||
Returns:
|
||||
200:
|
||||
The info about the new static reminder entry
|
||||
400:
|
||||
KeyNotFound: One of the required parameters was not given
|
||||
404:
|
||||
NotificationServiceNotFound: One of the notification services was not found
|
||||
"""
|
||||
reminders: StaticReminders = g.user_data.static_reminders
|
||||
|
||||
if request.method == 'GET':
|
||||
result = reminders.fetchall()
|
||||
return return_api(result)
|
||||
|
||||
elif request.method == 'POST':
|
||||
data = request.get_json()
|
||||
title = extract_key(data, 'title')
|
||||
notification_services = extract_key(data, 'notification_services')
|
||||
text = extract_key(data, 'text', check_existence=False)
|
||||
color = extract_key(data, 'color', check_existence=False)
|
||||
|
||||
result = reminders.add(title=title,
|
||||
notification_services=notification_services,
|
||||
text=text,
|
||||
color=color)
|
||||
return return_api(result.get(), code=201)
|
||||
|
||||
@api.route('/staticreminders/<int:r_id>', methods=['GET', 'POST', 'PUT', 'DELETE'])
|
||||
@error_handler
|
||||
@auth
|
||||
def api_get_static_reminder(r_id: int):
|
||||
"""
|
||||
Endpoint: /staticreminders/<r_id>
|
||||
Description: Manage a specific static reminder
|
||||
Requires being logged in: Yes
|
||||
URL Parameters:
|
||||
<r_id>:
|
||||
The id of the static reminder
|
||||
Methods:
|
||||
GET:
|
||||
Returns:
|
||||
200:
|
||||
All info about the static reminder
|
||||
404:
|
||||
No static reminder found with the given id
|
||||
POST:
|
||||
Description: Trigger the static reminder
|
||||
Returns:
|
||||
200:
|
||||
Static reminder triggered successfully
|
||||
PUT:
|
||||
Description: Edit the static reminder
|
||||
Parameters (body):
|
||||
title: The new title of the static reminder.
|
||||
notification_services: The new array of id's of the notification services to use to send the reminder.
|
||||
text: The new body of the static reminder.
|
||||
color: The new hex code of the color of the static reminder, which is shown in the web-ui.
|
||||
Returns:
|
||||
200:
|
||||
Static reminder updated successfully
|
||||
404:
|
||||
ReminderNotFound: No static reminder found with the given id
|
||||
NotificationServiceNotFound: One of the notification services was not found
|
||||
DELETE:
|
||||
Description: Delete the static reminder
|
||||
Returns:
|
||||
200:
|
||||
Static reminder deleted successfully
|
||||
404:
|
||||
No static reminder found with the given id
|
||||
"""
|
||||
reminders: StaticReminders = g.user_data.static_reminders
|
||||
if request.method == 'GET':
|
||||
result = reminders.fetchone(r_id).get()
|
||||
return return_api(result)
|
||||
|
||||
elif request.method == 'POST':
|
||||
reminders.trigger_reminder(r_id)
|
||||
return return_api({})
|
||||
|
||||
elif request.method == 'PUT':
|
||||
data = request.get_json()
|
||||
title = extract_key(data, 'title', check_existence=False)
|
||||
notification_services = extract_key(data, 'notification_services', check_existence=False)
|
||||
text = extract_key(data, 'text', check_existence=False)
|
||||
color = extract_key(data, 'color', check_existence=False)
|
||||
|
||||
result = reminders.fetchone(r_id).update(title=title,
|
||||
notification_services=notification_services,
|
||||
text=text,
|
||||
color=color)
|
||||
return return_api(result)
|
||||
|
||||
elif request.method == 'DELETE':
|
||||
reminders.fetchone(r_id).delete()
|
||||
return return_api({})
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
--default-shadow: 0 1px 2px 0 rgb(0 0 0 / 60%), 0 2px 6px 2px rgb(0 0 0 / 30%);
|
||||
}
|
||||
|
||||
/* */
|
||||
/* Default properties */
|
||||
/* */
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -66,7 +68,9 @@ svg rect {
|
||||
fill: var(--color-light);
|
||||
}
|
||||
|
||||
/* */
|
||||
/* Utility classes */
|
||||
/* */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -90,7 +94,9 @@ svg rect {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
/* */
|
||||
/* General styling */
|
||||
/* */
|
||||
body {
|
||||
height: 100vh;
|
||||
overflow-x: hidden;
|
||||
@@ -99,6 +105,21 @@ body {
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
noscript {
|
||||
display: block;
|
||||
max-width: 95%;
|
||||
margin-inline: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
background-color: var(--color-error);
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
/* */
|
||||
/* Header */
|
||||
/* */
|
||||
header {
|
||||
width: 100%;
|
||||
height: var(--header-height);
|
||||
@@ -138,18 +159,9 @@ h1 {
|
||||
font-size: clamp(1.3rem, 7vw, 2rem);
|
||||
}
|
||||
|
||||
noscript {
|
||||
display: block;
|
||||
max-width: 95%;
|
||||
margin-inline: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
background-color: var(--color-error);
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
/* */
|
||||
/* Nav */
|
||||
/* */
|
||||
.nav-divider {
|
||||
position: relative;
|
||||
height: calc(100% - var(--header-height));
|
||||
@@ -222,6 +234,9 @@ nav > div > button svg {
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
/* */
|
||||
/* Main window */
|
||||
/* */
|
||||
.window-container {
|
||||
margin-left: calc(4rem + var(--rem-clamp));
|
||||
width: 100%;
|
||||
|
||||
@@ -81,6 +81,34 @@
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.notification-service-list {
|
||||
width: 100%;
|
||||
max-height: 10rem;
|
||||
overflow-y: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
border: 2px solid var(--color-gray);
|
||||
border-radius: 4px;
|
||||
|
||||
box-shadow: var(--default-shadow);
|
||||
}
|
||||
|
||||
.notification-service-list > div {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: .5rem .75rem;
|
||||
}
|
||||
|
||||
.notification-service-list > div:not(:first-child) {
|
||||
border-top: 1px solid var(--color-gray);
|
||||
}
|
||||
|
||||
.notification-service-list > div > input {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.repeat-bar,
|
||||
.repeat-edit-bar {
|
||||
display: flex;
|
||||
@@ -134,7 +162,7 @@ div.options > button {
|
||||
background-color: var(--color-gray);
|
||||
}
|
||||
|
||||
#delete-reminder {
|
||||
#delete-info {
|
||||
border-color: var(--color-error);
|
||||
color: var(--color-error);
|
||||
}
|
||||
@@ -165,4 +193,95 @@ div.options > button {
|
||||
.sub-inputs > button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* */
|
||||
/* Adding */
|
||||
/* */
|
||||
#info.show-add-reminder #delete-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#info.show-add-static-reminder #time-input,
|
||||
#info.show-add-static-reminder #normal-button,
|
||||
#info.show-add-static-reminder #repeat-button,
|
||||
#info.show-add-static-reminder .repeat-bar,
|
||||
#info.show-add-static-reminder #delete-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#info.show-add-static-reminder #text-input {
|
||||
margin-top: -1rem;
|
||||
}
|
||||
|
||||
#info.show-add-static-reminder #template-selection,
|
||||
#info.show-add-static-reminder #color-toggle,
|
||||
#info.show-add-static-reminder #toggle-notification-service-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#info.show-add-template #template-selection,
|
||||
#info.show-add-template #time-input,
|
||||
#info.show-add-template #normal-button,
|
||||
#info.show-add-template #repeat-button,
|
||||
#info.show-add-template .repeat-bar,
|
||||
#info.show-add-template #test-reminder,
|
||||
#info.show-add-template #delete-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#info.show-add-template #text-input {
|
||||
margin-top: -1rem;
|
||||
}
|
||||
|
||||
#info.show-add-template #color-toggle,
|
||||
#info.show-add-template #toggle-notification-service-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* */
|
||||
/* Editing */
|
||||
/* */
|
||||
#info.show-edit-reminder #template-selection,
|
||||
#info.show-edit-reminder #test-reminder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#info.show-edit-reminder #color-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#info.show-edit-static-reminder #template-selection,
|
||||
#info.show-edit-static-reminder #time-input,
|
||||
#info.show-edit-static-reminder #normal-button,
|
||||
#info.show-edit-static-reminder #repeat-button,
|
||||
#info.show-edit-static-reminder .repeat-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#info.show-edit-static-reminder #text-input {
|
||||
margin-top: -1rem;
|
||||
}
|
||||
|
||||
#info.show-edit-static-reminder #color-toggle,
|
||||
#info.show-edit-static-reminder #toggle-notification-service-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#info.show-edit-template #template-selection,
|
||||
#info.show-edit-template #time-input,
|
||||
#info.show-edit-template #normal-button,
|
||||
#info.show-edit-template #repeat-button,
|
||||
#info.show-edit-template .repeat-bar,
|
||||
#info.show-edit-template #test-reminder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#info.show-edit-template #text-input {
|
||||
margin-top: -1rem;
|
||||
}
|
||||
|
||||
#info.show-edit-template #color-toggle,
|
||||
#info.show-edit-template #toggle-notification-service-list {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -47,6 +47,7 @@
|
||||
|
||||
/* REMINDER LIST */
|
||||
#reminder-list,
|
||||
#static-reminder-list,
|
||||
#template-list {
|
||||
--gap: 1rem;
|
||||
--entry-width: 13rem;
|
||||
@@ -61,24 +62,22 @@
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#add-entry,
|
||||
#add-template {
|
||||
.entry.add-entry {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
#add-entry svg,
|
||||
#add-template svg {
|
||||
.entry.add-entry svg {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
#add-entry:not(.error.error-icon) p {
|
||||
.entry.add-entry:not(.error.error-icon) p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#add-entry.error.error-icon p {
|
||||
.entry.add-entry.error.error-icon p {
|
||||
display: block;
|
||||
font-size: .8rem !important;
|
||||
}
|
||||
@@ -117,11 +116,6 @@ button.entry.fit {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#delete-template {
|
||||
border-color: var(--color-error);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
@media (max-width: 543px) {
|
||||
header > div {
|
||||
transform: translateX(0);
|
||||
@@ -1,6 +1,6 @@
|
||||
main {
|
||||
height: calc(100vh - var(--header-height));
|
||||
|
||||
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ form input {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
form button[type="submit"] {
|
||||
button[type="submit"] {
|
||||
padding: .5rem 1rem;
|
||||
|
||||
font-size: 1.1rem;
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
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'),
|
||||
'color': document.querySelector('#add .color-list')
|
||||
};
|
||||
|
||||
const type_buttons = {
|
||||
'normal-button': document.getElementById('normal-button'),
|
||||
'repeat-button': document.getElementById('repeat-button'),
|
||||
|
||||
'repeat-bar': document.querySelector('.repeat-bar'),
|
||||
'repeat-interval': document.getElementById('repeat-interval'),
|
||||
'repeat-quantity': document.getElementById('repeat-quantity')
|
||||
};
|
||||
|
||||
function addReminder() {
|
||||
inputs.time.classList.remove('error-input');
|
||||
inputs.time.removeAttribute('title');
|
||||
|
||||
const data = {
|
||||
'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,
|
||||
'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;
|
||||
data['repeat_interval'] = type_buttons['repeat-interval'].value
|
||||
};
|
||||
|
||||
fetch(`${url_prefix}/api/reminders?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);
|
||||
};
|
||||
|
||||
fillList();
|
||||
closeAdd();
|
||||
return
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else if (e === 400) {
|
||||
inputs.time.classList.add('error-input');
|
||||
inputs.time.title = 'Time is in the past';
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function showAdd() {
|
||||
if (!document.getElementById('add-entry').classList.contains('error')) {
|
||||
loadTemplates(force=false);
|
||||
showWindow('add');
|
||||
} else {
|
||||
showWindow('notification');
|
||||
};
|
||||
};
|
||||
|
||||
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';
|
||||
|
||||
type_buttons['repeat-bar'].classList.add('hidden');
|
||||
type_buttons['repeat-interval'].removeAttribute('required');
|
||||
type_buttons['repeat-interval'].value = '';
|
||||
};
|
||||
|
||||
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(`${url_prefix}/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 = url_prefix;
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 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();
|
||||
@@ -1,161 +0,0 @@
|
||||
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'),
|
||||
'color': document.querySelector('#edit .color-list')
|
||||
};
|
||||
|
||||
const edit_type_buttons = {
|
||||
'normal-edit-button': document.getElementById('normal-edit-button'),
|
||||
'repeat-edit-button': document.getElementById('repeat-edit-button'),
|
||||
|
||||
'repeat-edit-bar': document.querySelector('.repeat-edit-bar'),
|
||||
'repeat-edit-interval': document.getElementById('repeat-edit-interval'),
|
||||
'repeat-edit-quantity': document.getElementById('repeat-edit-quantity')
|
||||
};
|
||||
|
||||
function editReminder() {
|
||||
const id = document.getElementById('edit-form').dataset.id;
|
||||
const data = {
|
||||
'title': edit_inputs.title.value,
|
||||
'time': (new Date(edit_inputs.time.value) / 1000) + (new Date(edit_inputs.time.value).getTimezoneOffset() * 60),
|
||||
'notification_service': edit_inputs.notification_service.value,
|
||||
'text': edit_inputs.text.value,
|
||||
'repeat_quantity': 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;
|
||||
data['repeat_interval'] = edit_type_buttons['repeat-edit-interval'].value;
|
||||
};
|
||||
|
||||
fetch(`${url_prefix}/api/reminders/${id}?api_key=${api_key}`, {
|
||||
'method': 'PUT',
|
||||
'headers': {'Content-Type': 'application/json'},
|
||||
'body': JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
|
||||
fillList();
|
||||
hideWindow();
|
||||
return
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function showEdit(id) {
|
||||
document.getElementById('edit-form').dataset.id = id;
|
||||
fetch(`${url_prefix}/api/reminders/${id}?api_key=${api_key}`)
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
|
||||
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(
|
||||
(json.result.time + new Date(json.result.time * 1000).getTimezoneOffset() * -60) * 1000
|
||||
);
|
||||
edit_inputs.time.value = trigger_date.toLocaleString('en-CA').slice(0,10) + 'T' + trigger_date.toTimeString().slice(0,5);
|
||||
edit_inputs.notification_service.value = json.result.notification_service;
|
||||
|
||||
if (json.result['repeat_interval'] === null) {
|
||||
toggleEditNormal();
|
||||
} else {
|
||||
toggleEditRepeated();
|
||||
edit_type_buttons['repeat-edit-interval'].value = json.result['repeat_interval'];
|
||||
edit_type_buttons['repeat-edit-quantity'].value = json.result['repeat_quantity'];
|
||||
};
|
||||
|
||||
edit_inputs.text.value = json.result.text !== null ? json.result.text : '';
|
||||
|
||||
showWindow('edit');
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else if (e === 404) {
|
||||
fillList();
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function toggleEditNormal() {
|
||||
edit_type_buttons['normal-edit-button'].dataset.selected = 'true';
|
||||
edit_type_buttons['repeat-edit-button'].dataset.selected = 'false';
|
||||
|
||||
edit_type_buttons['repeat-edit-bar'].classList.add('hidden');
|
||||
edit_type_buttons['repeat-edit-interval'].removeAttribute('required');
|
||||
edit_type_buttons['repeat-edit-interval'].value = '';
|
||||
};
|
||||
|
||||
function toggleEditRepeated() {
|
||||
edit_type_buttons['normal-edit-button'].dataset.selected = 'false';
|
||||
edit_type_buttons['repeat-edit-button'].dataset.selected = 'true';
|
||||
|
||||
edit_type_buttons['repeat-edit-bar'].classList.remove('hidden');
|
||||
edit_type_buttons['repeat-edit-interval'].setAttribute('required', '');
|
||||
};
|
||||
|
||||
function deleteReminder() {
|
||||
const id = document.getElementById('edit-form').dataset.id;
|
||||
fetch(`${url_prefix}/api/reminders/${id}?api_key=${api_key}`, {
|
||||
'method': 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
|
||||
fillList();
|
||||
hideWindow();
|
||||
return;
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else if (e === 404) {
|
||||
fillList();
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 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());
|
||||
document.getElementById('delete-reminder').addEventListener('click', e => deleteReminder());
|
||||
@@ -1,19 +1,26 @@
|
||||
function logout() {
|
||||
fetch(`${url_prefix}/api/auth/logout?api_key=${api_key}`, {
|
||||
'method': 'POST'
|
||||
})
|
||||
.then(response => {
|
||||
sessionStorage.removeItem('api_key');
|
||||
window.location.href = url_prefix || '/';
|
||||
});
|
||||
const types = {
|
||||
'reminder': document.getElementById('reminder-list'),
|
||||
'static_reminder': document.getElementById('static-reminder-list'),
|
||||
'template': document.getElementById('template-list')
|
||||
};
|
||||
|
||||
const icons = {
|
||||
'save': '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><path d="M12,10a4,4,0,1,0,4,4A4,4,0,0,0,12,10Zm0,6a2,2,0,1,1,2-2A2,2,0,0,1,12,16Z"></path><path d="M22.536,4.122,19.878,1.464A4.966,4.966,0,0,0,16.343,0H5A5.006,5.006,0,0,0,0,5V19a5.006,5.006,0,0,0,5,5H19a5.006,5.006,0,0,0,5-5V7.657A4.966,4.966,0,0,0,22.536,4.122ZM17,2.08V3a3,3,0,0,1-3,3H10A3,3,0,0,1,7,3V2h9.343A2.953,2.953,0,0,1,17,2.08ZM22,19a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2V3a5.006,5.006,0,0,0,5,5h4a4.991,4.991,0,0,0,4.962-4.624l2.16,2.16A3.02,3.02,0,0,1,22,7.657Z"></path></g></svg>',
|
||||
'edit': '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><g id="_01_align_center" data-name="01 align center"><path d="M22.94,1.06a3.626,3.626,0,0,0-5.124,0L0,18.876V24H5.124L22.94,6.184A3.627,3.627,0,0,0,22.94,1.06ZM4.3,22H2V19.7L15.31,6.4l2.3,2.3ZM21.526,4.77,19.019,7.277l-2.295-2.3L19.23,2.474a1.624,1.624,0,0,1,2.3,2.3Z"></path></g></g></svg>',
|
||||
'delete': '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><g id="_01_align_center" data-name="01 align center"><path d="M22,4H17V2a2,2,0,0,0-2-2H9A2,2,0,0,0,7,2V4H2V6H4V21a3,3,0,0,0,3,3H17a3,3,0,0,0,3-3V6h2ZM9,2h6V4H9Zm9,19a1,1,0,0,1-1,1H7a1,1,0,0,1-1-1V6H18Z"></path><rect x="9" y="10" width="2" height="8"></rect><rect x="13" y="10" width="2" height="8"></rect></g></g></svg>'
|
||||
};
|
||||
|
||||
const info_classes = [
|
||||
'show-add-reminder', 'show-add-template', 'show-add-static-reminder',
|
||||
'show-edit-reminder', 'show-edit-template', 'show-edit-static-reminder'
|
||||
];
|
||||
|
||||
function toggleNav() {
|
||||
document.querySelector('.nav-divider').classList.toggle('show-nav');
|
||||
};
|
||||
|
||||
function showWindow(id) {
|
||||
document.querySelectorAll(`.window-container > div`).forEach(e => {
|
||||
document.querySelectorAll('.window-container > div').forEach(e => {
|
||||
if (e.id === id || e.id === 'home') {
|
||||
e.classList.remove('hidden');
|
||||
setTimeout(() => e.classList.add('show-window'), 0);
|
||||
@@ -28,23 +35,21 @@ function hideWindow() {
|
||||
document.querySelectorAll('.show-window').forEach(e => {
|
||||
e.classList.remove('show-window');
|
||||
});
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('.window-container > div:not(#home)').forEach(
|
||||
e => e.classList.add('hidden')
|
||||
);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
function showTab(tab_id, button_id, load_function=null) {
|
||||
document.querySelectorAll(`.tab-selector > button`).forEach(e => {
|
||||
if (e.id === button_id) {
|
||||
e.dataset.selected = "true"
|
||||
} else {
|
||||
e.dataset.selected = "false"
|
||||
};
|
||||
function logout() {
|
||||
fetch(`${url_prefix}/api/auth/logout?api_key=${api_key}`, {
|
||||
'method': 'POST'
|
||||
})
|
||||
.then(response => {
|
||||
sessionStorage.removeItem('api_key');
|
||||
window.location.href = `${url_prefix}/`;
|
||||
});
|
||||
document.querySelectorAll(`#home > div:not(.tab-selector)`).forEach(e => {
|
||||
e.classList.add('hidden');
|
||||
});
|
||||
document.getElementById(tab_id).classList.remove('hidden');
|
||||
if (load_function !== null) {
|
||||
load_function(force=false);
|
||||
};
|
||||
};
|
||||
|
||||
// code run on load
|
||||
@@ -52,7 +57,7 @@ function showTab(tab_id, button_id, load_function=null) {
|
||||
const url_prefix = document.getElementById('url_prefix').dataset.value;
|
||||
const api_key = sessionStorage.getItem('api_key');
|
||||
if (api_key === null) {
|
||||
window.location.href = url_prefix;
|
||||
window.location.href = `${url_prefix}/`;
|
||||
};
|
||||
|
||||
document.getElementById('toggle-nav').addEventListener('click', e => toggleNav());
|
||||
@@ -61,10 +66,3 @@ document.getElementById('home-button').addEventListener('click', e => hideWindow
|
||||
document.getElementById('notification-services-button').addEventListener('click', e => showWindow('notification'));
|
||||
document.getElementById('settings-button').addEventListener('click', e => showWindow('settings'));
|
||||
document.getElementById('logout-button').addEventListener('click', e => logout());
|
||||
|
||||
document.getElementById('reminders-selector').addEventListener('click', e => showTab('reminder-tab', 'reminders-selector'));
|
||||
document.getElementById('templates-selector').addEventListener('click', e => showTab('template-tab', 'templates-selector', loadTemplates));
|
||||
|
||||
const edit_icon = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><g id="_01_align_center" data-name="01 align center"><path d="M22.94,1.06a3.626,3.626,0,0,0-5.124,0L0,18.876V24H5.124L22.94,6.184A3.627,3.627,0,0,0,22.94,1.06ZM4.3,22H2V19.7L15.31,6.4l2.3,2.3ZM21.526,4.77,19.019,7.277l-2.295-2.3L19.23,2.474a1.624,1.624,0,0,1,2.3,2.3Z"></path></g></g></svg>';
|
||||
const delete_icon = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><g id="_01_align_center" data-name="01 align center"><path d="M22,4H17V2a2,2,0,0,0-2-2H9A2,2,0,0,0,7,2V4H2V6H4V21a3,3,0,0,0,3,3H17a3,3,0,0,0,3-3V6h2ZM9,2h6V4H9Zm9,19a1,1,0,0,1-1,1H7a1,1,0,0,1-1-1V6H18Z"></path><rect x="9" y="10" width="2" height="8"></rect><rect x="13" y="10" width="2" height="8"></rect></g></g></svg>';
|
||||
const save_icon = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><path d="M12,10a4,4,0,1,0,4,4A4,4,0,0,0,12,10Zm0,6a2,2,0,1,1,2-2A2,2,0,0,1,12,16Z"></path><path d="M22.536,4.122,19.878,1.464A4.966,4.966,0,0,0,16.343,0H5A5.006,5.006,0,0,0,0,5V19a5.006,5.006,0,0,0,5,5H19a5.006,5.006,0,0,0,5-5V7.657A4.966,4.966,0,0,0,22.536,4.122ZM17,2.08V3a3,3,0,0,1-3,3H10A3,3,0,0,1,7,3V2h9.343A2.953,2.953,0,0,1,17,2.08ZM22,19a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2V3a5.006,5.006,0,0,0,5,5h4a4.991,4.991,0,0,0,4.962-4.624l2.16,2.16A3.02,3.02,0,0,1,22,7.657Z"></path></g></svg>';
|
||||
|
||||
122
frontend/static/js/library.js
Normal file
122
frontend/static/js/library.js
Normal file
@@ -0,0 +1,122 @@
|
||||
function showTab(button) {
|
||||
// Apply styling to selected button
|
||||
document.querySelectorAll('.tab-selector > button').forEach(
|
||||
b => b.dataset.selected = b === button ? 'true' : 'false'
|
||||
);
|
||||
|
||||
// Show desired tab and hide all others
|
||||
document.querySelectorAll('#home > div:not(.tab-selector)').forEach(
|
||||
e => e.classList.add('hidden')
|
||||
);
|
||||
document.getElementById(button.dataset.target).classList.remove('hidden');
|
||||
};
|
||||
|
||||
//
|
||||
// Filling library
|
||||
//
|
||||
function fillTable(table, results) {
|
||||
table.querySelectorAll('button.entry:not(.add-entry)').forEach(e => e.remove());
|
||||
|
||||
results.forEach(r => {
|
||||
const entry = document.createElement('button');
|
||||
entry.classList.add('entry');
|
||||
entry.dataset.id = r.id;
|
||||
entry.addEventListener('click', e => showEdit(r.id, table));
|
||||
if (r.color !== null)
|
||||
entry.style.setProperty('--color', r.color);
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.innerText = r.title;
|
||||
entry.appendChild(title);
|
||||
|
||||
if (table === types.reminder) {
|
||||
const time = document.createElement('p');
|
||||
var offset = new Date(r.time * 1000).getTimezoneOffset() * -60;
|
||||
var d = new Date((r.time + offset) * 1000);
|
||||
var formatted_date = d.toLocaleString('en-CA').slice(0,10) + ' ' + d.toTimeString().slice(0,5);
|
||||
if (r.repeat_interval !== null) {
|
||||
if (r.repeat_interval === 1) {
|
||||
var quantity = r.repeat_quantity.endsWith('s') ? r.repeat_quantity.slice(0, -1) : r.repeat_quantity;
|
||||
var interval_text = ` (each ${quantity})`;
|
||||
} else {
|
||||
var quantity = r.repeat_quantity.endsWith('s') ? r.repeat_quantity : r.repeat_quantity + 's';
|
||||
var interval_text = ` (every ${r.repeat_interval} ${quantity})`;
|
||||
};
|
||||
formatted_date += interval_text;
|
||||
};
|
||||
time.innerText = formatted_date;
|
||||
entry.appendChild(time);
|
||||
};
|
||||
|
||||
table.appendChild(entry);
|
||||
|
||||
if (title.clientHeight < title.scrollHeight)
|
||||
entry.classList.add('expand');
|
||||
});
|
||||
table.querySelectorAll('button.entry:not(.add-entry)').forEach(r => r.classList.add('fit'));
|
||||
};
|
||||
|
||||
function fillLibrary(url, type) {
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(json => fillTable(type, json.result))
|
||||
.catch(e => {
|
||||
if (e === 401)
|
||||
window.location.href = `${url_prefix}/`;
|
||||
else
|
||||
console.log(e);
|
||||
});
|
||||
};
|
||||
|
||||
function fillReminders() {
|
||||
fillLibrary(`/api/reminders?api_key=${api_key}`, types.reminder);
|
||||
};
|
||||
|
||||
function fillStaticReminders() {
|
||||
fillLibrary(`/api/staticreminders?api_key=${api_key}`, types.static_reminder);
|
||||
}
|
||||
|
||||
function fillTemplates() {
|
||||
fillLibrary(`/api/templates?api_key=${api_key}`, types.template);
|
||||
};
|
||||
|
||||
//
|
||||
// Library search
|
||||
//
|
||||
function searchLibrary() {
|
||||
const query = document.querySelector('#search-input').value;
|
||||
fetch(`${url_prefix}/api/reminders/search?api_key=${api_key}&query=${query}`)
|
||||
.then(response => {
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(json => fillTable(types.reminder, json.result))
|
||||
.catch(e => {
|
||||
if (e === 401)
|
||||
window.location.href = `${url_prefix}/`;
|
||||
else
|
||||
console.log(e);
|
||||
});
|
||||
};
|
||||
|
||||
function clearSearchLibrary() {
|
||||
document.querySelector('#search-input').value = '';
|
||||
fillReminders();
|
||||
};
|
||||
|
||||
// code run on load
|
||||
|
||||
document.querySelectorAll('.tab-selector > button').forEach(b => {
|
||||
b.addEventListener('click', e => showTab(b));
|
||||
});
|
||||
|
||||
fillReminders();
|
||||
fillStaticReminders();
|
||||
fillTemplates();
|
||||
setInterval(fillReminders, 60000);
|
||||
|
||||
document.querySelector('#search-form').setAttribute('action', 'javascript:searchLibrary();');
|
||||
document.querySelector('#clear-button').addEventListener('click', e => clearSearchLibrary());
|
||||
@@ -1,26 +1,46 @@
|
||||
function login(data=null) {
|
||||
document.getElementById('username-error').classList.add('hidden');
|
||||
document.getElementById('username-input').classList.remove('error-input');
|
||||
document.getElementById('password-error').classList.add('hidden');
|
||||
document.getElementById('password-input').classList.remove('error-input');
|
||||
const login_inputs = {
|
||||
'username': document.querySelector('#login-form > input[type="text"]'),
|
||||
'password': document.querySelector('#login-form > input[type="password"]')
|
||||
};
|
||||
|
||||
if (data === null) {
|
||||
const login_errors = {
|
||||
'username': document.getElementById('username-error'),
|
||||
'password': document.getElementById('password-error')
|
||||
};
|
||||
|
||||
const create_inputs = {
|
||||
'username': document.querySelector('#create-form > input[type="text"]'),
|
||||
'password': document.querySelector('#create-form > input[type="password"]')
|
||||
}
|
||||
|
||||
const create_errors = {
|
||||
'username_invalid': document.getElementById('new-username-error'),
|
||||
'username_taken': document.getElementById('taken-username-error'),
|
||||
};
|
||||
|
||||
function toggleWindow() {
|
||||
document.querySelector('main').classList.toggle('show-create');
|
||||
};
|
||||
|
||||
function login(data=null) {
|
||||
login_inputs.username.classList.remove('error-input');
|
||||
login_errors.username.classList.add('hidden');
|
||||
login_inputs.password.classList.remove('error-input');
|
||||
login_errors.password.classList.add('hidden');
|
||||
|
||||
if (data === null)
|
||||
data = {
|
||||
'username': document.getElementById('username-input').value,
|
||||
'password': document.getElementById('password-input').value
|
||||
'username': login_inputs.username.value,
|
||||
'password': login_inputs.password.value
|
||||
};
|
||||
};
|
||||
|
||||
fetch(`${url_prefix}/api/auth/login`, {
|
||||
'method': 'POST',
|
||||
'headers': {'Content-Type': 'application/json'},
|
||||
'body': JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
@@ -29,11 +49,11 @@ function login(data=null) {
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
document.getElementById('password-error').classList.remove('hidden');
|
||||
document.getElementById('password-input').classList.add('error-input');
|
||||
login_inputs.password.classList.add('error-input');
|
||||
login_errors.password.classList.remove('hidden');
|
||||
} else if (e === 404) {
|
||||
document.getElementById('username-error').classList.remove('hidden');
|
||||
document.getElementById('username-input').classList.add('error-input');
|
||||
login_inputs.username.classList.add('error-input');
|
||||
login_errors.username.classList.remove('hidden');
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
@@ -41,42 +61,35 @@ function login(data=null) {
|
||||
};
|
||||
|
||||
function create() {
|
||||
document.getElementById('new-username-error').classList.add('hidden');
|
||||
document.getElementById('new-username-input').classList.remove('error-input');
|
||||
document.getElementById('taken-username-error').classList.add('hidden');
|
||||
create_inputs.username.classList.remove('error-input');
|
||||
create_errors.username_invalid.classList.add('hidden');
|
||||
create_errors.username_taken.classList.add('hidden');
|
||||
|
||||
const data = {
|
||||
'username': document.getElementById('new-username-input').value,
|
||||
'password': document.getElementById('new-password-input').value
|
||||
'username': create_inputs.username.value,
|
||||
'password': create_inputs.password.value
|
||||
};
|
||||
fetch(`${url_prefix}/api/user/add`, {
|
||||
'method': 'POST',
|
||||
'headers': {'Content-Type': 'application/json'},
|
||||
'body': JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json() )
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
// catch errors
|
||||
if (json.error !== null) {
|
||||
return Promise.reject(json.error);
|
||||
};
|
||||
if (json.error !== null) return Promise.reject(json.error);
|
||||
login(data);
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 'UsernameInvalid') {
|
||||
document.getElementById('new-username-error').classList.remove('hidden');
|
||||
document.getElementById('new-username-input').classList.add('error-input');
|
||||
create_inputs.username.classList.add('error-input');
|
||||
create_errors.username_invalid.classList.remove('hidden');
|
||||
} else if (e === 'UsernameTaken') {
|
||||
document.getElementById('taken-username-error').classList.remove('hidden');
|
||||
document.getElementById('new-username-input').classList.add('error-input');
|
||||
create_inputs.username.classList.add('error-input');
|
||||
create_errors.username_taken.classList.remove('hidden');
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function toggleWindow() {
|
||||
document.querySelector('main').classList.toggle('show-create');
|
||||
};
|
||||
|
||||
// code run on load
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
const inputs_buttons = {
|
||||
'save_button': document.querySelector('#add-row button[data-type="save"]'),
|
||||
'title': document.querySelector('#add-row td.title-column input'),
|
||||
'url': document.querySelector('#add-row td.url-column input')
|
||||
};
|
||||
|
||||
function fillNotificationSelection() {
|
||||
fetch(`${url_prefix}/api/notificationservices?api_key=${api_key}`)
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
if (json.result.length) {
|
||||
document.getElementById('add-entry').classList.remove('error', 'error-icon');
|
||||
[
|
||||
document.getElementById('notification-service-input'),
|
||||
document.getElementById('notification-service-edit-input'),
|
||||
document.getElementById('notification-service-template-input'),
|
||||
document.getElementById('notification-service-template-edit-input')
|
||||
].forEach(options => {
|
||||
options.innerHTML = '';
|
||||
json.result.forEach(service => {
|
||||
const entry = document.createElement('option');
|
||||
entry.value = service.id;
|
||||
entry.innerText = service.title;
|
||||
options.appendChild(entry);
|
||||
});
|
||||
options.querySelector(':nth-child(1)').setAttribute('selected', '');
|
||||
document.getElementById('add-reminder').classList.remove('error', 'error-icon');
|
||||
|
||||
inputs.notification_service.innerHTML = '';
|
||||
json.result.forEach(service => {
|
||||
const entry = document.createElement('div');
|
||||
|
||||
const select = document.createElement('input');
|
||||
select.dataset.id = service.id;
|
||||
select.type = 'checkbox';
|
||||
entry.appendChild(select);
|
||||
|
||||
const title = document.createElement('p');
|
||||
title.innerText = service.title;
|
||||
entry.appendChild(title);
|
||||
|
||||
inputs.notification_service.appendChild(entry);
|
||||
});
|
||||
inputs.notification_service.querySelector(':first-child input').checked = true;
|
||||
|
||||
const table = document.getElementById('services-list');
|
||||
table.querySelectorAll('tr:not(#add-row)').forEach(e => e.remove());
|
||||
@@ -60,7 +64,7 @@ function fillNotificationSelection() {
|
||||
edit_button.addEventListener('click', e => editService(service.id));
|
||||
edit_button.title = 'Edit';
|
||||
edit_button.setAttribute('aria-label', 'Edit');
|
||||
edit_button.innerHTML = edit_icon;
|
||||
edit_button.innerHTML = icons.edit;
|
||||
actions.appendChild(edit_button);
|
||||
|
||||
const save_button = document.createElement('button');
|
||||
@@ -68,7 +72,7 @@ function fillNotificationSelection() {
|
||||
save_button.addEventListener('click', e => saveService(service.id));
|
||||
save_button.title = 'Save Edits';
|
||||
save_button.setAttribute('aria-label', 'Save Edits');
|
||||
save_button.innerHTML = save_icon;
|
||||
save_button.innerHTML = icons.save;
|
||||
actions.appendChild(save_button);
|
||||
|
||||
const delete_button = document.createElement('button');
|
||||
@@ -76,51 +80,20 @@ function fillNotificationSelection() {
|
||||
delete_button.addEventListener('click', e => deleteService(service.id));
|
||||
delete_button.title = 'Delete';
|
||||
delete_button.setAttribute('aria-label', 'Delete');
|
||||
delete_button.innerHTML = delete_icon;
|
||||
delete_button.innerHTML = icons.delete;
|
||||
actions.appendChild(delete_button);
|
||||
|
||||
table.appendChild(entry);
|
||||
});
|
||||
} else {
|
||||
document.getElementById('add-entry').classList.add('error', 'error-icon');
|
||||
document.getElementById('add-reminder').classList.add('error', 'error-icon');
|
||||
};
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else {
|
||||
if (e === 401)
|
||||
window.location.href = `${url_prefix}/`;
|
||||
else
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function deleteService(id) {
|
||||
const row = document.querySelector(`tr[data-id="${id}"]`);
|
||||
fetch(`${url_prefix}/api/notificationservices/${id}?api_key=${api_key}`, {
|
||||
'method': 'DELETE'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
// catch errors
|
||||
if (json.error !== null) {
|
||||
return Promise.reject(json);
|
||||
};
|
||||
|
||||
row.remove();
|
||||
if (document.querySelectorAll('#services-list > tr:not(#add-row)').length === 0) {
|
||||
document.getElementById('add-entry').classList.add('error', 'error-icon');
|
||||
};
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.error === 'ApiKeyExpired' || e.error === 'ApiKeyInvalid') {
|
||||
window.location.href = url_prefix;
|
||||
} else if (e.error === 'NotificationServiceInUse') {
|
||||
const delete_button = row.querySelector('button[title="Delete"]');
|
||||
delete_button.classList.add('error-icon');
|
||||
delete_button.title = `The notification service is still in use by a ${e.result.type}`;
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -142,23 +115,44 @@ function saveService(id) {
|
||||
'body': JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
|
||||
fillNotificationSelection();
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else if (e === 400) {
|
||||
if (e === 401)
|
||||
window.location.href = `${url_prefix}/`;
|
||||
else if (e === 400) {
|
||||
save_button.classList.add('error-icon');
|
||||
save_button.title = 'Invalid Apprise URL';
|
||||
save_button.setAttribute('aria-label', 'Invalid Apprise URL');
|
||||
} else {
|
||||
} else
|
||||
console.log(e);
|
||||
});
|
||||
};
|
||||
|
||||
function deleteService(id) {
|
||||
const row = document.querySelector(`tr[data-id="${id}"]`);
|
||||
fetch(`${url_prefix}/api/notificationservices/${id}?api_key=${api_key}`, {
|
||||
'method': 'DELETE'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
if (json.error !== null) return Promise.reject(json);
|
||||
|
||||
row.remove();
|
||||
if (document.querySelectorAll('#services-list > tr:not(#add-row)').length === 0)
|
||||
document.getElementById('add-entry').classList.add('error', 'error-icon');
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.error === 'ApiKeyExpired' || e.error === 'ApiKeyInvalid')
|
||||
window.location.href = `${url_prefix}/`;
|
||||
else if (e.error === 'NotificationServiceInUse') {
|
||||
const delete_button = row.querySelector('button[title="Delete"]');
|
||||
delete_button.classList.add('error-icon');
|
||||
delete_button.title = `The notification service is still in use by a ${e.result.type}`;
|
||||
} else
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -167,11 +161,6 @@ function toggleAddService() {
|
||||
};
|
||||
|
||||
function addService() {
|
||||
const inputs_buttons = {
|
||||
'save_button': document.querySelector('#add-row button[data-type="save"]'),
|
||||
'title': document.querySelector('#add-row td.title-column input'),
|
||||
'url': document.querySelector('#add-row td.url-column input')
|
||||
};
|
||||
const data = {
|
||||
'title': inputs_buttons.title.value,
|
||||
'url': inputs_buttons.url.value
|
||||
@@ -182,10 +171,7 @@ function addService() {
|
||||
'body': JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
|
||||
inputs_buttons.title.value = '';
|
||||
inputs_buttons.url.value = '';
|
||||
@@ -198,15 +184,14 @@ function addService() {
|
||||
fillNotificationSelection();
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else if (e === 400) {
|
||||
if (e === 401)
|
||||
window.location.href = `${url_prefix}/`;
|
||||
else if (e === 400) {
|
||||
inputs_buttons.save_button.classList.add('error-icon');
|
||||
inputs_buttons.save_button.title = 'Invalid Apprise URL';
|
||||
inputs_buttons.save_button.setAttribute('aria-label', 'Invalid Apprise URL');
|
||||
} else {
|
||||
} else
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
function fillTable(result) {
|
||||
const table = document.getElementById('reminder-list');
|
||||
table.querySelectorAll('button.entry:not(#add-entry)').forEach(e => e.remove());
|
||||
|
||||
result.forEach(reminder => {
|
||||
const entry = document.createElement('button');
|
||||
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;
|
||||
entry.appendChild(title);
|
||||
|
||||
const time = document.createElement('p');
|
||||
var offset = new Date(reminder.time * 1000).getTimezoneOffset() * -60;
|
||||
var d = new Date((reminder.time + offset) * 1000);
|
||||
var formatted_date = d.toLocaleString('en-CA').slice(0,10) + ' ' + d.toTimeString().slice(0,5);
|
||||
if (reminder.repeat_interval !== null) {
|
||||
if (reminder.repeat_interval === 1) {
|
||||
var quantity = reminder.repeat_quantity.endsWith('s') ? reminder.repeat_quantity.slice(0, -1) : reminder.repeat_quantity;
|
||||
var interval_text = ` (each ${quantity})`;
|
||||
} else {
|
||||
var quantity = reminder.repeat_quantity.endsWith('s') ? reminder.repeat_quantity : reminder.repeat_quantity + 's';
|
||||
var interval_text = ` (every ${reminder.repeat_interval} ${quantity})`;
|
||||
};
|
||||
formatted_date += interval_text;
|
||||
};
|
||||
time.innerText = formatted_date;
|
||||
entry.appendChild(time);
|
||||
|
||||
table.appendChild(entry);
|
||||
|
||||
if (title.clientHeight < title.scrollHeight) {
|
||||
entry.classList.add('expand');
|
||||
};
|
||||
});
|
||||
table.querySelectorAll('button.entry:not(#add-entry)').forEach(reminder => reminder.classList.add('fit'));
|
||||
};
|
||||
|
||||
function fillList() {
|
||||
fetch(`${url_prefix}/api/reminders?api_key=${api_key}`)
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
fillTable(json.result);
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function search() {
|
||||
const query = document.getElementById('search-input').value;
|
||||
fetch(`${url_prefix}/api/reminders/search?api_key=${api_key}&query=${query}`)
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
fillTable(json.result);
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function clearSearch() {
|
||||
document.getElementById('search-input').value = '';
|
||||
fillList();
|
||||
}
|
||||
|
||||
// code run on load
|
||||
|
||||
fillList();
|
||||
setInterval(fillList, 60000);
|
||||
|
||||
document.getElementById('search-form').setAttribute('action', 'javascript:search();');
|
||||
document.getElementById('clear-button').addEventListener('click', e => clearSearch());
|
||||
document.getElementById('add-entry').addEventListener('click', e => showAdd());
|
||||
document.getElementById('add-template').addEventListener('click', e => showWindow('add-template'));
|
||||
@@ -8,18 +8,14 @@ function changePassword() {
|
||||
'body': JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else {
|
||||
if (e === 401)
|
||||
window.location.href = `${url_prefix}/`;
|
||||
else
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -28,7 +24,7 @@ function deleteAccount() {
|
||||
'method': 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
window.location.href = url_prefix;
|
||||
window.location.href = `${url_prefix}/`;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
130
frontend/static/js/show.js
Normal file
130
frontend/static/js/show.js
Normal file
@@ -0,0 +1,130 @@
|
||||
function showAdd(type) {
|
||||
inputs.template.value = '0';
|
||||
inputs.title.value = '';
|
||||
inputs.text.value = '';
|
||||
inputs.time.value = '';
|
||||
inputs.notification_service.querySelectorAll('input[type="checkbox"]').forEach(c => c.checked = false);
|
||||
inputs.notification_service.querySelector('input[type="checkbox"]:first-child').checked = true;
|
||||
toggleNormal();
|
||||
toggleColor(true);
|
||||
document.getElementById('test-reminder').classList.remove('show-sent');
|
||||
|
||||
const cl = document.getElementById('info').classList;
|
||||
cl.forEach(c => {
|
||||
if (info_classes.includes(c)) cl.remove(c)
|
||||
});
|
||||
document.querySelector('.options > button[type="submit"]').innerText = 'Add';
|
||||
if (type === types.reminder) {
|
||||
cl.add('show-add-reminder');
|
||||
document.querySelector('#info h2').innerText = 'Add a reminder';
|
||||
document.querySelector('#test-reminder > div:first-child').innerText = 'Test';
|
||||
inputs.time.setAttribute('required', '');
|
||||
} else if (type === types.template) {
|
||||
cl.add('show-add-template');
|
||||
document.querySelector('#info h2').innerText = 'Add a template';
|
||||
document.querySelector('#test-reminder > div:first-child').innerText = 'Test';
|
||||
inputs.time.removeAttribute('required');
|
||||
} else if (type === types.static_reminder) {
|
||||
cl.add('show-add-static-reminder');
|
||||
document.querySelector('#info h2').innerText = 'Add a static reminder';
|
||||
document.querySelector('#test-reminder > div:first-child').innerText = 'Test';
|
||||
inputs.time.removeAttribute('required');
|
||||
} else
|
||||
return;
|
||||
showWindow('info');
|
||||
};
|
||||
|
||||
function showEdit(id, type) {
|
||||
let url;
|
||||
if (type === types.reminder) {
|
||||
url = `${url_prefix}/api/reminders/${id}?api_key=${api_key}`;
|
||||
inputs.time.setAttribute('required', '');
|
||||
} else if (type === types.template) {
|
||||
url = `${url_prefix}/api/templates/${id}?api_key=${api_key}`;
|
||||
inputs.time.removeAttribute('required');
|
||||
type_buttons.repeat_interval.removeAttribute('required');
|
||||
} else if (type === types.static_reminder) {
|
||||
url = `${url_prefix}/api/staticreminders/${id}?api_key=${api_key}`;
|
||||
document.getElementById('test-reminder').classList.remove('show-sent');
|
||||
inputs.time.removeAttribute('required');
|
||||
type_buttons.repeat_interval.removeAttribute('required');
|
||||
} else return;
|
||||
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
document.getElementById('info').dataset.id = id;
|
||||
if (json.result.color !== null) {
|
||||
if (inputs.color.classList.contains('hidden')) {
|
||||
toggleColor();
|
||||
};
|
||||
selectColor(json.result['color']);
|
||||
};
|
||||
|
||||
inputs.title.value = json.result.title;
|
||||
|
||||
if (type === types.reminder) {
|
||||
var trigger_date = new Date(
|
||||
(json.result.time + new Date(json.result.time * 1000).getTimezoneOffset() * -60) * 1000
|
||||
);
|
||||
inputs.time.value = trigger_date.toLocaleString('en-CA').slice(0,10) + 'T' + trigger_date.toTimeString().slice(0,5);
|
||||
};
|
||||
inputs.notification_service.querySelectorAll('input[type="checkbox"]').forEach(
|
||||
c => c.checked = json.result.notification_services.includes(parseInt(c.dataset.id))
|
||||
);
|
||||
|
||||
if (type == types.reminder) {
|
||||
if (json.result.repeat_interval === null) {
|
||||
toggleNormal();
|
||||
} else {
|
||||
toggleRepeated();
|
||||
type_buttons.repeat_interval.value = json.result.repeat_interval;
|
||||
type_buttons.repeat_quantity.value = json.result.repeat_quantity;
|
||||
};
|
||||
};
|
||||
|
||||
inputs.text.value = json.result.text;
|
||||
|
||||
showWindow('info');
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401)
|
||||
window.location.href = `${url_prefix}/`;
|
||||
else
|
||||
console.log(e);
|
||||
});
|
||||
|
||||
const cl = document.getElementById('info').classList;
|
||||
cl.forEach(c => {
|
||||
if (info_classes.includes(c)) cl.remove(c)
|
||||
});
|
||||
document.querySelector('.options > button[type="submit"]').innerText = 'Save';
|
||||
if (type === types.reminder) {
|
||||
cl.add('show-edit-reminder');
|
||||
document.querySelector('#info h2').innerText = 'Edit a reminder';
|
||||
document.querySelector('#test-reminder > div:first-child').innerText = 'Test';
|
||||
} else if (type === types.template) {
|
||||
cl.add('show-edit-template');
|
||||
document.querySelector('#info h2').innerText = 'Edit a template';
|
||||
document.querySelector('#test-reminder > div:first-child').innerText = 'Test';
|
||||
} else if (type === types.static_reminder) {
|
||||
cl.add('show-edit-static-reminder');
|
||||
document.querySelector('#info h2').innerText = 'Edit a static reminder';
|
||||
document.querySelector('#test-reminder > div:first-child').innerText = 'Trigger';
|
||||
} else
|
||||
return;
|
||||
};
|
||||
|
||||
// code run on load
|
||||
|
||||
document.getElementById('add-reminder').addEventListener('click', e => {
|
||||
if (document.getElementById('add-reminder').classList.contains('error'))
|
||||
showWindow('notification');
|
||||
else
|
||||
showAdd(types.reminder);
|
||||
});
|
||||
document.getElementById('add-static-reminder').addEventListener('click', e => showAdd(types.static_reminder));
|
||||
document.getElementById('add-template').addEventListener('click', e => showAdd(types.template));
|
||||
@@ -1,29 +1,7 @@
|
||||
const template_inputs = {
|
||||
'title': document.getElementById('title-template-input'),
|
||||
'notification-service': document.getElementById('notification-service-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'),
|
||||
'color': document.querySelector('#edit-template .color-list')
|
||||
};
|
||||
|
||||
function loadTemplates(force=true) {
|
||||
const table = document.getElementById('template-list');
|
||||
if (!force && !!table.querySelector('button:not(#add-template)')) {
|
||||
return
|
||||
};
|
||||
|
||||
function loadTemplateSelection() {
|
||||
fetch(`${url_prefix}/api/templates?api_key=${api_key}`)
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
@@ -36,216 +14,49 @@ function loadTemplates(force=true) {
|
||||
|
||||
select_list.appendChild(entry);
|
||||
});
|
||||
|
||||
table.querySelectorAll('button:not(#add-template)').forEach(e => e.remove());
|
||||
json.result.forEach(template => {
|
||||
const entry = document.createElement('button');
|
||||
entry.classList.add('entry');
|
||||
entry.addEventListener('click', e => showEditTemplate(template.id));
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.innerText = template.title;
|
||||
entry.appendChild(title);
|
||||
|
||||
table.appendChild(entry);
|
||||
|
||||
if (title.clientHeight < title.scrollHeight) {
|
||||
entry.classList.add('expand');
|
||||
};
|
||||
});
|
||||
table.querySelectorAll('button:not(#add-template)').forEach(template => template.classList.add('fit'));
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else {
|
||||
if (e === 401)
|
||||
window.location.href = `${url_prefix}/`;
|
||||
else
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function loadTemplate() {
|
||||
const id = document.getElementById('template-selection').value;
|
||||
if (id === "0") {
|
||||
function applyTemplate() {
|
||||
if (inputs.template.value === '0') {
|
||||
inputs.title.value = '';
|
||||
inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value;
|
||||
inputs.notification_service.querySelectorAll('input[type="checkbox"]:checked').forEach(c => c.checked = false)
|
||||
inputs.text.value = '';
|
||||
if (!inputs.color.classList.contains('hidden')) {
|
||||
toggleColor(inputs.color);
|
||||
};
|
||||
toggleColor(true);
|
||||
} else {
|
||||
fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`)
|
||||
fetch(`${url_prefix}/api/templates/${inputs.template.value}?api_key=${api_key}`)
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
return response.json();
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
else return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
inputs.title.value = json.result.title;
|
||||
inputs.notification_service.value = json.result.notification_service;
|
||||
inputs.notification_service.querySelectorAll('input[type="checkbox"]').forEach(
|
||||
c => c.checked = json.result.notification_services.includes(parseInt(c.dataset.id))
|
||||
);
|
||||
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);
|
||||
};
|
||||
};
|
||||
if (inputs.color.classList.contains('hidden'))
|
||||
toggleColor();
|
||||
selectColor(json.result.color);
|
||||
} else
|
||||
toggleColor(true);
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else {
|
||||
if (e === 401)
|
||||
window.location.href = `${url_prefix}/`;
|
||||
else
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
function addTemplate() {
|
||||
const data = {
|
||||
'title': template_inputs.title.value,
|
||||
'notification_service': template_inputs["notification-service"].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(`${url_prefix}/api/templates?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);
|
||||
};
|
||||
|
||||
loadTemplates();
|
||||
closeAddTemplate();
|
||||
return
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function closeAddTemplate() {
|
||||
hideWindow();
|
||||
setTimeout(() => {
|
||||
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);
|
||||
};
|
||||
|
||||
function showEditTemplate(id) {
|
||||
fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`)
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
document.getElementById('template-edit-form').dataset.id = 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 => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function saveTemplate() {
|
||||
const id = document.getElementById('template-edit-form').dataset.id;
|
||||
const data = {
|
||||
'title': edit_template_inputs.title.value,
|
||||
'notification_service': edit_template_inputs['notification-service'].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(`${url_prefix}/api/templates/${id}?api_key=${api_key}`, {
|
||||
'method': 'PUT',
|
||||
'headers': {'Content-Type': 'application/json'},
|
||||
'body': JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
loadTemplates();
|
||||
hideWindow();
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function deleteTemplate() {
|
||||
const id = document.getElementById('template-edit-form').dataset.id;
|
||||
fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`, {
|
||||
'method': 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
// catch errors
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.status);
|
||||
};
|
||||
|
||||
loadTemplates();
|
||||
hideWindow();
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = url_prefix;
|
||||
} else {
|
||||
console.log(e);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 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());
|
||||
loadTemplateSelection();
|
||||
|
||||
278
frontend/static/js/window.js
Normal file
278
frontend/static/js/window.js
Normal file
@@ -0,0 +1,278 @@
|
||||
const colors = ["#3c3c3c", "#49191e", "#171a42", "#083b06", "#3b3506", "#300e40"];
|
||||
|
||||
const inputs = {
|
||||
'template': document.getElementById('template-selection'),
|
||||
'title': document.getElementById('title-input'),
|
||||
'time': document.getElementById('time-input'),
|
||||
'notification_service': document.querySelector('.notification-service-list'),
|
||||
'text': document.getElementById('text-input'),
|
||||
'color': document.querySelector('.color-list')
|
||||
};
|
||||
|
||||
const type_buttons = {
|
||||
'normal_button': document.getElementById('normal-button'),
|
||||
'repeat_button': document.getElementById('repeat-button'),
|
||||
|
||||
'repeat_bar': document.querySelector('.repeat-bar'),
|
||||
'repeat_interval': document.getElementById('repeat-interval'),
|
||||
'repeat_quantity': document.getElementById('repeat-quantity')
|
||||
};
|
||||
|
||||
function loadColor() {
|
||||
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(color))
|
||||
inputs.color.appendChild(entry);
|
||||
});
|
||||
};
|
||||
|
||||
function selectColor(color_code) {
|
||||
inputs.color.querySelector(`button[data-color="${color_code}"]`).dataset.selected = 'true';
|
||||
inputs.color.querySelectorAll(`button:not([data-color="${color_code}"])`).forEach(b => b.dataset.selected = 'false');
|
||||
return;
|
||||
};
|
||||
|
||||
function toggleColor(hide=false) {
|
||||
selectColor(colors[0])
|
||||
if (!hide)
|
||||
inputs.color.classList.toggle('hidden');
|
||||
else
|
||||
inputs.color.classList.add('hidden');
|
||||
};
|
||||
|
||||
function toggleNotificationService(hide=false) {
|
||||
if (!hide)
|
||||
inputs.notification_service.classList.toggle('hidden');
|
||||
else
|
||||
inputs.notification_service.classList.add('hidden');
|
||||
};
|
||||
|
||||
function toggleNormal() {
|
||||
type_buttons.normal_button.dataset.selected = 'true';
|
||||
type_buttons.repeat_button.dataset.selected = 'false';
|
||||
|
||||
type_buttons.repeat_bar.classList.add('hidden');
|
||||
type_buttons.repeat_interval.removeAttribute('required');
|
||||
type_buttons.repeat_interval.value = '';
|
||||
};
|
||||
|
||||
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');
|
||||
const cl = document.getElementById('info').classList;
|
||||
const r_id = document.getElementById('info').dataset.id;
|
||||
const headers = {
|
||||
'method': 'POST',
|
||||
'headers': {'Content-Type': 'application/json'}
|
||||
};
|
||||
let url;
|
||||
if (cl.contains('show-edit-static-reminder')) {
|
||||
// Trigger static reminder
|
||||
url = `${url_prefix}/api/staticreminders/${r_id}?api_key=${api_key}`;
|
||||
} else {
|
||||
// Test reminder draft
|
||||
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 ns = [...
|
||||
document.querySelectorAll('.notification-service-list input[type="checkbox"]:checked')
|
||||
].map(c => parseInt(c.dataset.id))
|
||||
if (!ns) {
|
||||
input.classList.add('error-input');
|
||||
input.title = 'No notification service set';
|
||||
return
|
||||
} else {
|
||||
input.classList.remove('error-input');
|
||||
input.removeAttribute('title');
|
||||
};
|
||||
|
||||
const data = {
|
||||
'title': inputs.title.value,
|
||||
'notification_services': ns,
|
||||
'text': inputs.text.value
|
||||
};
|
||||
headers.body = JSON.stringify(data);
|
||||
url = `${url_prefix}/api/reminders/test?api_key=${api_key}`;
|
||||
};
|
||||
fetch(url, headers)
|
||||
.then(response => {
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
input.classList.add('show-sent');
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401)
|
||||
window.location.href = `${url_prefix}/`;
|
||||
else
|
||||
console.log(e);
|
||||
});
|
||||
};
|
||||
|
||||
function deleteInfo() {
|
||||
let url;
|
||||
const e_id = document.getElementById('info').dataset.id;
|
||||
const cl = document.getElementById('info').classList;
|
||||
if (cl.contains('show-edit-reminder')) {
|
||||
// Delete reminder
|
||||
url = `${url_prefix}/api/reminders/${e_id}?api_key=${api_key}`;
|
||||
} else if (cl.contains('show-edit-template')) {
|
||||
// Delete template
|
||||
url = `${url_prefix}/api/templates/${e_id}?api_key=${api_key}`;
|
||||
} else if (cl.contains('show-edit-static-reminder')) {
|
||||
// Delete static reminder
|
||||
url = `${url_prefix}/api/staticreminders/${e_id}?api_key=${api_key}`;
|
||||
} else return;
|
||||
|
||||
fetch(url, {'method': 'DELETE'})
|
||||
.then(response => {
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
|
||||
fillNotificationSelection();
|
||||
fillReminders();
|
||||
fillStaticReminders();
|
||||
fillTemplates();
|
||||
hideWindow();
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401)
|
||||
window.location.href = `${url_prefix}/`;
|
||||
else
|
||||
console.log(e);
|
||||
});
|
||||
};
|
||||
|
||||
function submitInfo() {
|
||||
inputs.time.classList.remove('error-input');
|
||||
inputs.time.removeAttribute('title');
|
||||
inputs.notification_service.classList.remove('error-input');
|
||||
inputs.notification_service.removeAttribute('title');
|
||||
let fetch_data = {
|
||||
url: null,
|
||||
method: null,
|
||||
call_back: null
|
||||
};
|
||||
const data = {
|
||||
'title': inputs.title.value,
|
||||
'notification_services': [...
|
||||
document.querySelectorAll('.notification-service-list input[type="checkbox"]:checked')
|
||||
].map(c => parseInt(c.dataset.id)),
|
||||
'text': inputs.text.value,
|
||||
'color': null
|
||||
};
|
||||
if (!inputs.color.classList.contains('hidden')) {
|
||||
data['color'] = inputs.color.querySelector('button[data-selected="true"]').dataset.color;
|
||||
};
|
||||
|
||||
if (data.notification_services.length === 0) {
|
||||
inputs.notification_service.classList.add('error-input');
|
||||
inputs.notification_service.title = 'No notification service set';
|
||||
return
|
||||
};
|
||||
|
||||
const e_id = document.getElementById('info').dataset.id;
|
||||
const cl = document.getElementById('info').classList;
|
||||
if (cl.contains('show-add-reminder')) {
|
||||
// Add reminder
|
||||
data['time'] = (new Date(inputs.time.value) / 1000) + (new Date(inputs.time.value).getTimezoneOffset() * 60)
|
||||
if (type_buttons.repeat_button.dataset.selected === 'true') {
|
||||
data['repeat_quantity'] = type_buttons.repeat_quantity.value;
|
||||
data['repeat_interval'] = type_buttons.repeat_interval.value
|
||||
};
|
||||
fetch_data.url = `${url_prefix}/api/reminders?api_key=${api_key}`;
|
||||
fetch_data.method = 'POST';
|
||||
fetch_data.call_back = fillReminders;
|
||||
|
||||
} else if (cl.contains('show-add-template')) {
|
||||
// Add template
|
||||
fetch_data.url = `${url_prefix}/api/templates?api_key=${api_key}`;
|
||||
fetch_data.method = 'POST';
|
||||
fetch_data.call_back = () => {
|
||||
loadTemplateSelection();
|
||||
fillTemplates();
|
||||
};
|
||||
|
||||
} else if (cl.contains('show-add-static-reminder')) {
|
||||
// Add static reminder
|
||||
fetch_data.url = `${url_prefix}/api/staticreminders?api_key=${api_key}`;
|
||||
fetch_data.method = 'POST';
|
||||
fetch_data.call_back = fillStaticReminders;
|
||||
|
||||
} else if (cl.contains('show-edit-reminder')) {
|
||||
// Edit reminder
|
||||
data['time'] = (new Date(inputs.time.value) / 1000) + (new Date(inputs.time.value).getTimezoneOffset() * 60)
|
||||
if (type_buttons.repeat_button.dataset.selected === 'true') {
|
||||
data['repeat_quantity'] = type_buttons.repeat_quantity.value;
|
||||
data['repeat_interval'] = type_buttons.repeat_interval.value
|
||||
};
|
||||
fetch_data.url = `${url_prefix}/api/reminders/${e_id}?api_key=${api_key}`;
|
||||
fetch_data.method = 'PUT';
|
||||
fetch_data.call_back = fillReminders;
|
||||
|
||||
} else if (cl.contains('show-edit-template')) {
|
||||
// Edit template
|
||||
fetch_data.url = `${url_prefix}/api/templates/${e_id}?api_key=${api_key}`;
|
||||
fetch_data.method = 'PUT';
|
||||
fetch_data.call_back = () => {
|
||||
loadTemplateSelection();
|
||||
fillTemplates();
|
||||
};
|
||||
|
||||
} else if (cl.contains('show-edit-static-reminder')) {
|
||||
// Edit a static reminder
|
||||
fetch_data.url = `${url_prefix}/api/staticreminders/${e_id}?api_key=${api_key}`;
|
||||
fetch_data.method = 'PUT';
|
||||
fetch_data.call_back = fillStaticReminders;
|
||||
|
||||
} else return;
|
||||
|
||||
fetch(fetch_data.url, {
|
||||
'method': fetch_data.method,
|
||||
'headers': {'Content-Type': 'application/json'},
|
||||
'body': JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
|
||||
fetch_data.call_back()
|
||||
hideWindow();
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 401) {
|
||||
window.location.href = `${url_prefix}/`;
|
||||
} else if (e === 400) {
|
||||
inputs.time.classList.add('error-input');
|
||||
inputs.time.title = 'Time is in the past';
|
||||
} else
|
||||
console.log(e);
|
||||
});
|
||||
};
|
||||
|
||||
// code run on load
|
||||
|
||||
loadColor();
|
||||
|
||||
document.getElementById('template-selection').addEventListener('change', e => applyTemplate());
|
||||
document.getElementById('color-toggle').addEventListener('click', e => toggleColor());
|
||||
document.getElementById('toggle-notification-service-list').addEventListener('click', e => toggleNotificationService());
|
||||
document.getElementById('normal-button').addEventListener('click', e => toggleNormal());
|
||||
document.getElementById('repeat-button').addEventListener('click', e => toggleRepeated());
|
||||
document.getElementById('close-info').addEventListener('click', e => hideWindow());
|
||||
document.getElementById('delete-info').addEventListener('click', e => deleteInfo());
|
||||
document.getElementById('test-reminder').addEventListener('click', e => testReminder());
|
||||
document.getElementById('info-form').setAttribute('action', 'javascript:submitInfo();');
|
||||
@@ -21,22 +21,28 @@
|
||||
<form id="login-form">
|
||||
<h2>Login</h2>
|
||||
<noscript>Javascript is disabled. The web-ui of MIND does not work with JavaScript disabled.</noscript>
|
||||
<input type="text" id="username-input" autocomplete="username" placeholder="Username" required autofocus>
|
||||
|
||||
<input type="text" autocomplete="username" placeholder="Username" required autofocus>
|
||||
<p class="error hidden" id="username-error">*Username not found</p>
|
||||
<input type="password" id="password-input" autocomplete="current-password" placeholder="Password" required>
|
||||
|
||||
<input type="password" autocomplete="current-password" placeholder="Password" required>
|
||||
<p class="error hidden" id="password-error">*Password incorrect</p>
|
||||
<button type="button" class="switch-button" id="create-account">Or create an account</button>
|
||||
|
||||
<button type="button" class="switch-button">Or create an account</button>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-container">
|
||||
<form id="create-form">
|
||||
<h2>Create</h2>
|
||||
<input type="text" id="new-username-input" autocomplete="username" placeholder="Username" required>
|
||||
|
||||
<input type="text" autocomplete="username" placeholder="Username" required>
|
||||
<p class="error hidden" id="new-username-error">*Username invalid</p>
|
||||
<p class="error hidden" id="taken-username-error">*Username already taken</p>
|
||||
<input type="password" id="new-password-input" autocomplete="new-password" placeholder="Password" required>
|
||||
<button type="button" class="switch-button" id="login-account">Or log into an account</button>
|
||||
|
||||
<input type="password" autocomplete="new-password" placeholder="Password" required>
|
||||
|
||||
<button type="button" class="switch-button">Or log into an account</button>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<main>
|
||||
<h2>MIND</h1>
|
||||
<p>404 - Page not found :(</p>
|
||||
<a href="{{url_prefix}}">Go to home page</a>
|
||||
<a href="{{url_prefix}}/">Go to home page</a>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,17 +7,17 @@
|
||||
<meta id="url_prefix" data-value="{{url_prefix}}">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/general.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/reminders_templates.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/add_edit.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/info.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/library.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/notification.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
|
||||
<script src="{{ url_for('static', filename='js/general.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/reminders.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/add.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/edit.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/library.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/window.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/templates.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/show.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/notification.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/settings.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/templates.js') }}" defer></script>
|
||||
|
||||
<title>Reminders - MIND</title>
|
||||
</head>
|
||||
@@ -64,7 +64,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button id="logout-button" aria-label="Log out of MIND" tilte="Logout">
|
||||
<button id="logout-button" aria-label="Log out of MIND" title="Logout">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M11.476,15a1,1,0,0,0-1,1v3a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2H7.476a3,3,0,0,1,3,3V8a1,1,0,0,0,2,0V5a5.006,5.006,0,0,0-5-5H5A5.006,5.006,0,0,0,0,5V19a5.006,5.006,0,0,0,5,5H7.476a5.006,5.006,0,0,0,5-5V16A1,1,0,0,0,11.476,15Z"></path>
|
||||
@@ -77,8 +77,9 @@
|
||||
<main class="window-container">
|
||||
<div id="home">
|
||||
<div class="tab-selector">
|
||||
<button id="reminders-selector" data-selected="true">Reminders</button>
|
||||
<button id="templates-selector" data-selected="false">Templates</button>
|
||||
<button data-target="reminder-tab" data-selected="true">Reminders</button>
|
||||
<button data-target="static-reminder-tab">Static Reminders</button>
|
||||
<button data-target="template-tab">Templates</button>
|
||||
</div>
|
||||
<div id="reminder-tab">
|
||||
<div class="search-container">
|
||||
@@ -108,7 +109,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="reminder-list">
|
||||
<button class="entry" id="add-entry" aria-label="Add reminder" title="Add reminder">
|
||||
<button class="entry add-entry" id="add-reminder" aria-label="Add reminder" title="Add reminder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
@@ -120,9 +121,22 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="static-reminder-tab" class="hidden">
|
||||
<div id="static-reminder-list">
|
||||
<button class="entry add-entry" id="add-static-reminder" aria-label="Add static reminder" title="Add static reminder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="template-tab" class="hidden">
|
||||
<div id="template-list">
|
||||
<button class="entry" id="add-template" aria-label="Add template" title="Add template">
|
||||
<button class="entry add-entry" id="add-template" aria-label="Add template" title="Add template">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
@@ -134,10 +148,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="add">
|
||||
<h2>Add a reminder</h2>
|
||||
|
||||
<div id="info">
|
||||
<h2></h2>
|
||||
<div class="form-container">
|
||||
<form id="add-form">
|
||||
<form id="info-form">
|
||||
<div class="sub-inputs">
|
||||
<select id="template-selection">
|
||||
<option value="0" selected>No template</option>
|
||||
@@ -148,8 +163,9 @@
|
||||
<input type="text" id="title-input" placeholder="Title" required>
|
||||
<div class="sub-inputs">
|
||||
<input type="datetime-local" id="time-input" required>
|
||||
<select id="notification-service-input" required></select>
|
||||
<button type="button" id="toggle-notification-service-list">Notification Services</button>
|
||||
</div>
|
||||
<div class="notification-service-list hidden"></div>
|
||||
<div class="sub-inputs">
|
||||
<button type="button" id="normal-button" data-selected="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 507.506 507.506" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
@@ -182,66 +198,19 @@
|
||||
</div>
|
||||
<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="close-info">Cancel</button>
|
||||
<button type="button" id="test-reminder">
|
||||
<div>Test</div>
|
||||
<div>Sent</div>
|
||||
</button>
|
||||
<button type="button" id="delete-info">Delete</button>
|
||||
<button type="submit">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="edit">
|
||||
<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>
|
||||
<select id="notification-service-edit-input" required></select>
|
||||
</div>
|
||||
<div class="sub-inputs">
|
||||
<button type="button" id="normal-edit-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 507.506 507.506" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M163.865 436.934a54.228 54.228 0 0 1-38.4-15.915L9.369 304.966c-12.492-12.496-12.492-32.752 0-45.248 12.496-12.492 32.752-12.492 45.248 0l109.248 109.248L452.889 79.942c12.496-12.492 32.752-12.492 45.248 0 12.492 12.496 12.492 32.752 0 45.248L202.265 421.019a54.228 54.228 0 0 1-38.4 15.915z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
Normal
|
||||
</button>
|
||||
<button type="button" id="repeat-edit-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 507.506 507.506" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M163.865 436.934a54.228 54.228 0 0 1-38.4-15.915L9.369 304.966c-12.492-12.496-12.492-32.752 0-45.248 12.496-12.492 32.752-12.492 45.248 0l109.248 109.248L452.889 79.942c12.496-12.492 32.752-12.492 45.248 0 12.492 12.496 12.492 32.752 0 45.248L202.265 421.019a54.228 54.228 0 0 1-38.4 15.915z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
Repeated
|
||||
</button>
|
||||
</div>
|
||||
<div class="repeat-edit-bar">
|
||||
<p>Repeat every </p>
|
||||
<input type="number" id="repeat-edit-interval" placeholder="interval" min="1" step="1" oninput="validity.valid || (value='');">
|
||||
<select id="repeat-edit-quantity">
|
||||
<option value="minutes">Minute(s)</option>
|
||||
<option value="hours">Hour(s)</option>
|
||||
<option value="days" selected>Day(s)</option>
|
||||
<option value="weeks">Week(s)</option>
|
||||
<option value="months">Month(s)</option>
|
||||
<option value="years">Year(s)</option>
|
||||
</select>
|
||||
</div>
|
||||
<textarea id="text-edit-input" cols="30" rows="10" placeholder="Text (optional)"></textarea>
|
||||
<div class="options">
|
||||
<button type="button" id="close-edit">Cancel</button>
|
||||
<button type="button" id="delete-reminder">Delete</button>
|
||||
<button type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="notification">
|
||||
<h2>Notification Services</h2>
|
||||
<p>Setup your notification providers here. See the <a target="_blank" href="https://github.com/caronc/apprise#supported-notifications">Apprise URL documentation</a> to learn how to make a valid Apprise URL.</p>
|
||||
@@ -293,6 +262,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="settings">
|
||||
<h2>Settings</h2>
|
||||
<div class="settings-container">
|
||||
@@ -305,39 +276,6 @@
|
||||
<button id="delete-account-button">Delete Account</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="add-template">
|
||||
<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>
|
||||
<div class="options">
|
||||
<button type="button" id="close-template">Cancel</button>
|
||||
<button type="submit">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="edit-template">
|
||||
<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>
|
||||
<div class="options">
|
||||
<button type="button" id="close-edit-template">Cancel</button>
|
||||
<button type="button" id="delete-template">Delete</button>
|
||||
<button type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -7,10 +7,6 @@ ui = Blueprint('ui', __name__)
|
||||
|
||||
methods = ['GET']
|
||||
|
||||
@ui.errorhandler(404)
|
||||
def not_found(e):
|
||||
return render_template('page_not_found.html', url_prefix=logging.URL_PREFIX)
|
||||
|
||||
@ui.route('/', methods=methods)
|
||||
def ui_login():
|
||||
return render_template('login.html', url_prefix=logging.URL_PREFIX)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import unittest
|
||||
from threading import Thread
|
||||
|
||||
from backend.reminders import filter_function, ReminderHandler
|
||||
|
||||
@@ -9,13 +8,6 @@ class Test_Reminder_Handler(unittest.TestCase):
|
||||
instance = ReminderHandler(context)
|
||||
self.assertIs(context, instance.context)
|
||||
|
||||
self.assertIsInstance(instance.thread, Thread)
|
||||
|
||||
self.assertFalse(instance.stop)
|
||||
with self.assertRaises(RuntimeError):
|
||||
instance.stop_handling()
|
||||
self.assertTrue(instance.stop)
|
||||
|
||||
def test_filter_function(self):
|
||||
p = {
|
||||
'title': 'TITLE',
|
||||
|
||||
Reference in New Issue
Block a user