mirror of
https://github.com/Casvt/MIND.git
synced 2026-02-19 11:54:46 -05:00
Fixes issue #4
This commit is contained in:
@@ -57,4 +57,4 @@ class InvalidKeyValue(Exception):
|
||||
|
||||
@property
|
||||
def api_response(self) -> Dict[str, Any]:
|
||||
return {'error': 'KeyNotFound', 'result': {'key': self.key, 'value': self.value}, 'code': 400}
|
||||
return {'error': 'InvalidKeyValue', 'result': {'key': self.key, 'value': self.value}, 'code': 400}
|
||||
|
||||
@@ -85,6 +85,10 @@ def setup_db() -> None:
|
||||
text TEXT,
|
||||
time INTEGER NOT NULL,
|
||||
notification_service INTEGER NOT NULL,
|
||||
|
||||
repeat_quantity VARCHAR(15),
|
||||
repeat_interval INTEGER,
|
||||
original_time INTEGER,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (notification_service) REFERENCES notification_services(id)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from sqlite3 import IntegrityError
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
@@ -7,9 +8,10 @@ from time import time as epoch_time
|
||||
from typing import List, Literal
|
||||
|
||||
from apprise import Apprise
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from flask import Flask
|
||||
|
||||
from backend.custom_exceptions import (InvalidTime,
|
||||
from backend.custom_exceptions import (InvalidKeyValue, InvalidTime,
|
||||
NotificationServiceNotFound,
|
||||
ReminderNotFound)
|
||||
from backend.db import close_db, get_db
|
||||
@@ -20,6 +22,18 @@ filter_function = lambda query, p: (
|
||||
or query in p["notification_service_title"].lower()
|
||||
)
|
||||
|
||||
def _find_next_time(
|
||||
original_time: int,
|
||||
repeat_quantity: Literal["year", "month", "week", "day", "hours", "minutes"],
|
||||
repeat_interval: int
|
||||
) -> int:
|
||||
td = relativedelta(**{repeat_quantity: repeat_interval})
|
||||
new_time = datetime.fromtimestamp(original_time)
|
||||
current_time = datetime.fromtimestamp(epoch_time())
|
||||
while new_time <= current_time:
|
||||
new_time += td
|
||||
return int(new_time.timestamp())
|
||||
|
||||
class ReminderHandler():
|
||||
"""Run in a thread to handle the set reminders
|
||||
"""
|
||||
@@ -33,7 +47,6 @@ class ReminderHandler():
|
||||
def _find_next_reminder(self) -> None:
|
||||
"""Note when next reminder is (could be in the past) or otherwise None
|
||||
"""
|
||||
|
||||
with self.context():
|
||||
next_timestamp = get_db().execute(
|
||||
"SELECT time FROM reminders ORDER BY time LIMIT 1;"
|
||||
@@ -65,26 +78,45 @@ class ReminderHandler():
|
||||
while not self.stop:
|
||||
if self.next_reminder and self.next_reminder <= epoch_time():
|
||||
with self.context():
|
||||
cursor = get_db()
|
||||
cursor = get_db(dict)
|
||||
# Get all reminders for the timestamp
|
||||
reminders = cursor.execute(
|
||||
"SELECT notification_service, title, text FROM reminders WHERE time = ?",
|
||||
reminders = cursor.execute("""
|
||||
SELECT
|
||||
id,
|
||||
notification_service, title, text,
|
||||
repeat_quantity, repeat_interval, original_time
|
||||
FROM reminders
|
||||
WHERE time = ?;
|
||||
""",
|
||||
(self.next_reminder,)
|
||||
).fetchall()
|
||||
|
||||
# Send of each reminder
|
||||
for reminder in reminders:
|
||||
# Send of reminder
|
||||
a = Apprise()
|
||||
url = cursor.execute(
|
||||
"SELECT url FROM notification_services WHERE id = ?",
|
||||
(reminder[0],)
|
||||
).fetchone()[0]
|
||||
(reminder["notification_service"],)
|
||||
).fetchone()["url"]
|
||||
a.add(url)
|
||||
a.notify(title=reminder[1], body=reminder[2])
|
||||
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'])
|
||||
)
|
||||
|
||||
# Delete the reminders from the database
|
||||
cursor.execute("DELETE FROM reminders WHERE time = ?", (self.next_reminder,))
|
||||
|
||||
# Note when next reminder is (could be in the past) or otherwise None
|
||||
self._find_next_reminder()
|
||||
|
||||
@@ -122,7 +154,9 @@ class Reminder:
|
||||
r.title, r.text,
|
||||
r.time,
|
||||
r.notification_service,
|
||||
ns.title AS notification_service_title
|
||||
ns.title AS notification_service_title,
|
||||
r.repeat_quantity,
|
||||
r.repeat_interval
|
||||
FROM
|
||||
reminders r
|
||||
INNER JOIN notification_services ns
|
||||
@@ -140,7 +174,9 @@ class Reminder:
|
||||
title: str = None,
|
||||
time: int = None,
|
||||
notification_service: int = None,
|
||||
text: str = None
|
||||
text: str = None,
|
||||
repeat_quantity: Literal["year", "month", "week", "day", "hours", "minutes"] = None,
|
||||
repeat_interval: int = None
|
||||
) -> dict:
|
||||
"""Edit the reminder
|
||||
|
||||
@@ -151,11 +187,20 @@ class Reminder:
|
||||
text (str, optional): The new body of the reminder. Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict: The new password info
|
||||
dict: The new reminder info
|
||||
"""
|
||||
cursor = get_db()
|
||||
|
||||
# Validate data
|
||||
if time < epoch_time():
|
||||
raise InvalidTime
|
||||
if repeat_quantity is None and repeat_interval is not None:
|
||||
raise InvalidKeyValue('repeat_quantity', repeat_quantity)
|
||||
elif repeat_quantity is not None and repeat_interval is None:
|
||||
raise InvalidKeyValue('repeat_interval', repeat_interval)
|
||||
repeated_reminder = repeat_quantity is not None and repeat_interval is not None
|
||||
|
||||
if not repeated_reminder:
|
||||
if time < epoch_time():
|
||||
raise InvalidTime
|
||||
time = round(time)
|
||||
|
||||
# Get current data and update it with new values
|
||||
@@ -164,28 +209,50 @@ class Reminder:
|
||||
'title': title,
|
||||
'time': time,
|
||||
'notification_service': notification_service,
|
||||
'text': text
|
||||
'text': text,
|
||||
'repeat_quantity': repeat_quantity,
|
||||
'repeat_interval': repeat_interval
|
||||
}
|
||||
for k, v in new_values.items():
|
||||
if v is not None:
|
||||
if k in ('repeat_quantity', 'repeat_interval') or v is not None:
|
||||
data[k] = v
|
||||
|
||||
# Update database
|
||||
try:
|
||||
get_db().execute("""
|
||||
UPDATE reminders
|
||||
SET title=?, text=?, time=?, notification_service=?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
data["title"],
|
||||
data["text"],
|
||||
data["time"],
|
||||
data["notification_service"],
|
||||
self.id
|
||||
))
|
||||
if not repeated_reminder:
|
||||
next_time = data["time"]
|
||||
cursor.execute("""
|
||||
UPDATE reminders
|
||||
SET title=?, text=?, time=?, notification_service=?, repeat_quantity=?, repeat_interval=?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
data["title"],
|
||||
data["text"],
|
||||
data["time"],
|
||||
data["notification_service"],
|
||||
data["repeat_quantity"],
|
||||
data["repeat_interval"],
|
||||
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=?
|
||||
WHERE id = ?;
|
||||
""", (
|
||||
data["title"],
|
||||
data["text"],
|
||||
next_time,
|
||||
data["notification_service"],
|
||||
data["repeat_quantity"],
|
||||
data["repeat_interval"],
|
||||
data["time"],
|
||||
self.id
|
||||
))
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
reminder_handler.submit_next_reminder(time)
|
||||
reminder_handler.submit_next_reminder(next_time)
|
||||
|
||||
return self.get()
|
||||
|
||||
@@ -230,7 +297,9 @@ class Reminders:
|
||||
r.title, r.text,
|
||||
r.time,
|
||||
r.notification_service,
|
||||
ns.title AS notification_service_title
|
||||
ns.title AS notification_service_title,
|
||||
r.repeat_quantity,
|
||||
r.repeat_interval
|
||||
FROM
|
||||
reminders r
|
||||
INNER JOIN notification_services ns
|
||||
@@ -278,7 +347,9 @@ class Reminders:
|
||||
title: str,
|
||||
time: int,
|
||||
notification_service: int,
|
||||
text: str = ''
|
||||
text: str = '',
|
||||
repeat_quantity: Literal["year", "month", "week", "day", "hours", "minutes"] = None,
|
||||
repeat_interval: int = None
|
||||
) -> Reminder:
|
||||
"""Add a reminder
|
||||
|
||||
@@ -287,22 +358,34 @@ class Reminders:
|
||||
time (int): The epoch timestamp the the reminder should be send.
|
||||
notification_service (int): The id of the notification service to use to send the reminder.
|
||||
text (str, optional): The body of the reminder. Defaults to ''.
|
||||
repeat_quantity (Literal["year", "month", "week", "day", "hours", "minutes"], optional): The quantity of the repeat specified for the reminder. Defaults to None.
|
||||
repeat_interval (int, optional): The amount of repeat_quantity, like "5" (hours). Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict: The info about the reminder
|
||||
"""
|
||||
# Validate data
|
||||
if time < epoch_time():
|
||||
raise InvalidTime
|
||||
time = round(time)
|
||||
|
||||
# Insert into db
|
||||
|
||||
if repeat_quantity is None and repeat_interval is not None:
|
||||
raise InvalidKeyValue('repeat_quantity', repeat_quantity)
|
||||
elif repeat_quantity is not None and repeat_interval is None:
|
||||
raise InvalidKeyValue('repeat_interval', repeat_interval)
|
||||
|
||||
try:
|
||||
id = get_db().execute("""
|
||||
INSERT INTO reminders(user_id, title, text, time, notification_service)
|
||||
VALUES (?,?,?,?,?);
|
||||
""", (self.user_id, title, text, time, notification_service,)
|
||||
).lastrowid
|
||||
if repeat_quantity is None and repeat_interval is None:
|
||||
id = get_db().execute("""
|
||||
INSERT INTO reminders(user_id, title, text, time, notification_service)
|
||||
VALUES (?,?,?,?,?);
|
||||
""", (self.user_id, title, text, time, notification_service)
|
||||
).lastrowid
|
||||
else:
|
||||
id = get_db().execute("""
|
||||
INSERT INTO reminders(user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, original_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
""", (self.user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, time)
|
||||
).lastrowid
|
||||
except IntegrityError:
|
||||
raise NotificationServiceNotFound
|
||||
reminder_handler.submit_next_reminder(time)
|
||||
|
||||
@@ -82,10 +82,22 @@ def extract_key(values: dict, key: str, check_existence: bool=True) -> Any:
|
||||
value = int(value)
|
||||
except (ValueError, TypeError):
|
||||
raise InvalidKeyValue(key, value)
|
||||
|
||||
elif key == 'repeat_interval':
|
||||
try:
|
||||
value = int(value)
|
||||
if value <= 0:
|
||||
raise ValueError
|
||||
except (ValueError, TypeError):
|
||||
raise InvalidKeyValue(key, value)
|
||||
|
||||
elif key == 'sort_by':
|
||||
if not value in Reminders.sort_functions:
|
||||
raise InvalidKeyValue(key, value)
|
||||
|
||||
elif key == 'repeat_quantity':
|
||||
if not value in ("year", "month", "week", "day", "hours", "minutes"):
|
||||
raise InvalidKeyValue(key, value)
|
||||
|
||||
else:
|
||||
if key == 'sort_by':
|
||||
@@ -405,11 +417,15 @@ def api_reminders_list():
|
||||
time = extract_key(data, 'time')
|
||||
notification_service = extract_key(data, 'notification_service')
|
||||
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)
|
||||
|
||||
result = reminders.add(title=title,
|
||||
time=time,
|
||||
notification_service=notification_service,
|
||||
text=text)
|
||||
text=text,
|
||||
repeat_quantity=repeat_quantity,
|
||||
repeat_interval=repeat_interval)
|
||||
return return_api(result.get(), code=201)
|
||||
|
||||
@api.route('/reminders/search', methods=['GET'])
|
||||
@@ -484,11 +500,16 @@ def api_get_reminder(r_id: int):
|
||||
time = extract_key(data, 'time', check_existence=False)
|
||||
notification_service = extract_key(data, 'notification_service', 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)
|
||||
|
||||
|
||||
result = reminders.fetchone(r_id).update(title=title,
|
||||
time=time,
|
||||
notification_service=notification_service,
|
||||
text=text)
|
||||
text=text,
|
||||
repeat_quantity=repeat_quantity,
|
||||
repeat_interval=repeat_interval)
|
||||
return return_api(result)
|
||||
|
||||
elif request.method == 'DELETE':
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
|
||||
.form-container > form input,
|
||||
.form-container > form select,
|
||||
.form-container > form textarea {
|
||||
.form-container > form textarea,
|
||||
.form-container > form button {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@@ -27,11 +28,69 @@
|
||||
}
|
||||
|
||||
.sub-inputs > input,
|
||||
.sub-inputs > select {
|
||||
.sub-inputs > select,
|
||||
.sub-inputs > button {
|
||||
width: calc(50% - (var(--gap) / 2));
|
||||
}
|
||||
|
||||
.options > button {
|
||||
.sub-inputs > button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: .75rem;
|
||||
|
||||
border: 2px solid var(--color-gray);
|
||||
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.sub-inputs > button > svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.sub-inputs > button[data-selected="false"] > svg {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.repeat-bar,
|
||||
.repeat-edit-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
|
||||
border: 2px solid var(--color-gray);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.repeat-bar > *,
|
||||
.repeat-edit-bar > * {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.repeat-bar > p,
|
||||
.repeat-edit-bar > p {
|
||||
min-width: 7rem;
|
||||
|
||||
padding: 1rem;
|
||||
padding-right: .25rem;
|
||||
}
|
||||
|
||||
.repeat-bar > input[type="number"],
|
||||
.repeat-edit-bar > input[type="number"] {
|
||||
min-width: 5rem;
|
||||
|
||||
padding-left: .25rem;
|
||||
}
|
||||
|
||||
.repeat-bar > select,
|
||||
.repeat-edit-bar > select {
|
||||
min-width: 8rem;
|
||||
}
|
||||
|
||||
div.options > button {
|
||||
width: 6rem;
|
||||
|
||||
border: 2px solid var(--color-gray);
|
||||
|
||||
@@ -5,6 +5,15 @@ const inputs = {
|
||||
'text': document.getElementById('text-input')
|
||||
};
|
||||
|
||||
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() {
|
||||
const data = {
|
||||
'title': inputs.title.value,
|
||||
@@ -12,6 +21,11 @@ function addReminder() {
|
||||
'notification_service': inputs.notification_service.value,
|
||||
'text': inputs.text.value
|
||||
};
|
||||
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(`/api/reminders?api_key=${api_key}`, {
|
||||
'method': 'POST',
|
||||
'headers': {'Content-Type': 'application/json'},
|
||||
@@ -50,11 +64,31 @@ function closeAdd() {
|
||||
inputs.title.value = '';
|
||||
inputs.time.value = '';
|
||||
inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value;
|
||||
toggleNormal();
|
||||
inputs.text.value = '';
|
||||
}, 500);
|
||||
};
|
||||
|
||||
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', '');
|
||||
};
|
||||
|
||||
// code run on load
|
||||
|
||||
document.getElementById('add-form').setAttribute('action', 'javascript:addReminder();');
|
||||
document.getElementById('normal-button').addEventListener('click', e => toggleNormal());
|
||||
document.getElementById('repeat-button').addEventListener('click', e => toggleRepeated());
|
||||
document.getElementById('close-add').addEventListener('click', e => closeAdd());
|
||||
|
||||
@@ -5,14 +5,30 @@ const edit_inputs = {
|
||||
'text': document.getElementById('text-edit-input')
|
||||
};
|
||||
|
||||
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).getTime() / 1000,
|
||||
'notification_service': edit_inputs.notification_service.value,
|
||||
'text': edit_inputs.text.value
|
||||
'text': edit_inputs.text.value,
|
||||
'repeat_quantity': null,
|
||||
'repeat_interval': null
|
||||
};
|
||||
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(`/api/reminders/${id}?api_key=${api_key}`, {
|
||||
'method': 'PUT',
|
||||
'headers': {'Content-Type': 'application/json'},
|
||||
@@ -50,11 +66,22 @@ function showEdit(id) {
|
||||
})
|
||||
.then(json => {
|
||||
edit_inputs.title.value = json.result.title;
|
||||
|
||||
edit_inputs.time.value = new Date(
|
||||
(json.result.time + new Date(json.result.time * 1000).getTimezoneOffset() * -60) * 1000
|
||||
).toISOString().slice(0, 16);
|
||||
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 => {
|
||||
@@ -68,7 +95,26 @@ function showEdit(id) {
|
||||
});
|
||||
};
|
||||
|
||||
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', '');
|
||||
};
|
||||
|
||||
// code run on load
|
||||
|
||||
document.getElementById('edit-form').setAttribute('action', 'javascript:editReminder();');
|
||||
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());
|
||||
|
||||
@@ -14,6 +14,16 @@ function fillTable(result) {
|
||||
const time = document.createElement('p');
|
||||
var d = new Date(reminder.time * 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);
|
||||
|
||||
@@ -119,7 +129,7 @@ function deleteReminder(id) {
|
||||
// code run on load
|
||||
|
||||
fillList();
|
||||
// setInterval(fillList, 60000);
|
||||
setInterval(fillList, 60000);
|
||||
|
||||
document.getElementById('search-form').setAttribute('action', 'javascript:search();');
|
||||
document.getElementById('clear-button').addEventListener('click', e => clearSearch());
|
||||
|
||||
@@ -122,6 +122,36 @@
|
||||
<input type="datetime-local" id="time-input" required>
|
||||
<select id="notification-service-input" required></select>
|
||||
</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">
|
||||
<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-button" data-selected="false">
|
||||
<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-bar hidden">
|
||||
<p>Repeat every </p>
|
||||
<input type="number" id="repeat-interval" placeholder="interval" min="1" step="1" oninput="validity.valid || (value='');">
|
||||
<select id="repeat-quantity">
|
||||
<option value="minutes">Minute(s)</option>
|
||||
<option value="hours">Hour(s)</option>
|
||||
<option value="day" selected>Day(s)</option>
|
||||
<option value="week">Week(s)</option>
|
||||
<option value="month">Month(s)</option>
|
||||
<option value="year">Year(s)</option>
|
||||
</select>
|
||||
</div>
|
||||
<textarea id="text-input" cols="30" rows="10" placeholder="Text (optional)"></textarea>
|
||||
<div class="options">
|
||||
<button type="button" id="close-add">Cancel</button>
|
||||
@@ -139,6 +169,36 @@
|
||||
<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="day" selected>Day(s)</option>
|
||||
<option value="week">Week(s)</option>
|
||||
<option value="month">Month(s)</option>
|
||||
<option value="year">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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
apprise>=0.9.9
|
||||
python-dateutil>=2.8.2
|
||||
Flask>=2.1.2
|
||||
waitress>=2.1.2
|
||||
Reference in New Issue
Block a user