Add ability to set alarm reminders
This commit is contained in:
parent
63cf561bfe
commit
83f25405db
8 changed files with 241 additions and 7 deletions
|
|
@ -5,6 +5,7 @@
|
||||||
* Add Tag relationship visualisation tool
|
* Add Tag relationship visualisation tool
|
||||||
* Improve size of checkboxes
|
* Improve size of checkboxes
|
||||||
* Convert bullet - to actual unicode bullets
|
* Convert bullet - to actual unicode bullets
|
||||||
|
* Add alarm option to set reminders
|
||||||
|
|
||||||
# 0.3.2
|
# 0.3.2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,16 @@ report from within the app.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
### General view
|
||||||

|

|
||||||
|
|
||||||
|
### History panes
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
### Tag relationship visualiser
|
||||||
|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Data is encrypted at rest
|
* Data is encrypted at rest
|
||||||
|
|
@ -41,6 +47,7 @@ report from within the app.
|
||||||
* Automatically generate checkboxes when typing 'TODO'
|
* Automatically generate checkboxes when typing 'TODO'
|
||||||
* It is possible to automatically move unchecked checkboxes from yesterday to today, on startup
|
* It is possible to automatically move unchecked checkboxes from yesterday to today, on startup
|
||||||
* English, French and Italian locales provided
|
* English, French and Italian locales provided
|
||||||
|
* Ability to set reminder alarms in the app against the current line of text on today's date
|
||||||
|
|
||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
|
|
|
||||||
|
|
@ -155,5 +155,12 @@
|
||||||
"bug_report_empty": "Please enter some details about the bug before sending.",
|
"bug_report_empty": "Please enter some details about the bug before sending.",
|
||||||
"bug_report_send_failed": "Could not send bug report.",
|
"bug_report_send_failed": "Could not send bug report.",
|
||||||
"bug_report_sent_ok": "Bug report sent. Thank you!",
|
"bug_report_sent_ok": "Bug report sent. Thank you!",
|
||||||
"send": "Send"
|
"send": "Send",
|
||||||
|
"set_reminder": "Set reminder prompt",
|
||||||
|
"set_reminder_prompt": "Enter a time",
|
||||||
|
"reminder_no_text_fallback": "You scheduled a reminder to alert you now!",
|
||||||
|
"invalid_time_title": "Invalid time",
|
||||||
|
"invalid_time_message": "Please enter a time in the format HH:MM",
|
||||||
|
"dismiss": "Dismiss",
|
||||||
|
"toolbar_alarm": "Set reminder alarm"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ from PySide6.QtCore import (
|
||||||
QUrl,
|
QUrl,
|
||||||
QEvent,
|
QEvent,
|
||||||
QSignalBlocker,
|
QSignalBlocker,
|
||||||
|
QDateTime,
|
||||||
|
QTime,
|
||||||
)
|
)
|
||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import (
|
||||||
QAction,
|
QAction,
|
||||||
|
|
@ -43,6 +45,10 @@ from PySide6.QtWidgets import (
|
||||||
QTabWidget,
|
QTabWidget,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
|
QInputDialog,
|
||||||
|
QLabel,
|
||||||
|
QPushButton,
|
||||||
|
QApplication,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .db import DBManager
|
from .db import DBManager
|
||||||
|
|
@ -164,8 +170,6 @@ class MainWindow(QMainWindow):
|
||||||
self._locked = False
|
self._locked = False
|
||||||
|
|
||||||
# reset idle timer on any key press anywhere in the app
|
# reset idle timer on any key press anywhere in the app
|
||||||
from PySide6.QtWidgets import QApplication
|
|
||||||
|
|
||||||
QApplication.instance().installEventFilter(self)
|
QApplication.instance().installEventFilter(self)
|
||||||
|
|
||||||
# Focus on the editor
|
# Focus on the editor
|
||||||
|
|
@ -292,7 +296,9 @@ class MainWindow(QMainWindow):
|
||||||
self._save_timer = QTimer(self)
|
self._save_timer = QTimer(self)
|
||||||
self._save_timer.setSingleShot(True)
|
self._save_timer.setSingleShot(True)
|
||||||
self._save_timer.timeout.connect(self._save_current)
|
self._save_timer.timeout.connect(self._save_current)
|
||||||
# Note: textChanged will be connected per-editor in _create_new_tab
|
|
||||||
|
# Reminders / alarms
|
||||||
|
self._reminder_timers: list[QTimer] = []
|
||||||
|
|
||||||
# First load + mark dates in calendar with content
|
# First load + mark dates in calendar with content
|
||||||
if not self._load_yesterday_todos():
|
if not self._load_yesterday_todos():
|
||||||
|
|
@ -310,6 +316,9 @@ class MainWindow(QMainWindow):
|
||||||
# apply once on startup so links / calendar colors are set immediately
|
# apply once on startup so links / calendar colors are set immediately
|
||||||
self._retheme_overrides()
|
self._retheme_overrides()
|
||||||
|
|
||||||
|
# Build any alarms for *today* from stored markdown
|
||||||
|
self._rebuild_reminders_for_today()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def editor(self) -> MarkdownEditor | None:
|
def editor(self) -> MarkdownEditor | None:
|
||||||
"""Get the currently active editor."""
|
"""Get the currently active editor."""
|
||||||
|
|
@ -966,6 +975,7 @@ class MainWindow(QMainWindow):
|
||||||
self._tb_bullets = lambda: self._call_editor("toggle_bullets")
|
self._tb_bullets = lambda: self._call_editor("toggle_bullets")
|
||||||
self._tb_numbers = lambda: self._call_editor("toggle_numbers")
|
self._tb_numbers = lambda: self._call_editor("toggle_numbers")
|
||||||
self._tb_checkboxes = lambda: self._call_editor("toggle_checkboxes")
|
self._tb_checkboxes = lambda: self._call_editor("toggle_checkboxes")
|
||||||
|
self._tb_alarm = self._on_alarm_requested
|
||||||
|
|
||||||
tb.boldRequested.connect(self._tb_bold)
|
tb.boldRequested.connect(self._tb_bold)
|
||||||
tb.italicRequested.connect(self._tb_italic)
|
tb.italicRequested.connect(self._tb_italic)
|
||||||
|
|
@ -975,9 +985,9 @@ class MainWindow(QMainWindow):
|
||||||
tb.bulletsRequested.connect(self._tb_bullets)
|
tb.bulletsRequested.connect(self._tb_bullets)
|
||||||
tb.numbersRequested.connect(self._tb_numbers)
|
tb.numbersRequested.connect(self._tb_numbers)
|
||||||
tb.checkboxesRequested.connect(self._tb_checkboxes)
|
tb.checkboxesRequested.connect(self._tb_checkboxes)
|
||||||
|
tb.alarmRequested.connect(self._tb_alarm)
|
||||||
tb.historyRequested.connect(self._open_history)
|
|
||||||
tb.insertImageRequested.connect(self._on_insert_image)
|
tb.insertImageRequested.connect(self._on_insert_image)
|
||||||
|
tb.historyRequested.connect(self._open_history)
|
||||||
|
|
||||||
self._toolbar_bound = True
|
self._toolbar_bound = True
|
||||||
|
|
||||||
|
|
@ -1023,6 +1033,177 @@ class MainWindow(QMainWindow):
|
||||||
self.toolBar.actBullets.setChecked(bool(bullets_on))
|
self.toolBar.actBullets.setChecked(bool(bullets_on))
|
||||||
self.toolBar.actNumbers.setChecked(bool(numbers_on))
|
self.toolBar.actNumbers.setChecked(bool(numbers_on))
|
||||||
|
|
||||||
|
# ----------- Alarms handler ------------#
|
||||||
|
def _on_alarm_requested(self):
|
||||||
|
"""Create a one-shot reminder based on the current line in the editor."""
|
||||||
|
editor = getattr(self, "editor", None)
|
||||||
|
if editor is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use the current line in the markdown editor as the reminder text
|
||||||
|
try:
|
||||||
|
line_text = editor.get_current_line_text().strip()
|
||||||
|
except AttributeError:
|
||||||
|
c = editor.textCursor()
|
||||||
|
line_text = c.block().text().strip()
|
||||||
|
|
||||||
|
# Ask user for a time today in HH:MM format
|
||||||
|
time_str, ok = QInputDialog.getText(
|
||||||
|
self,
|
||||||
|
strings._("set_reminder"),
|
||||||
|
strings._("set_reminder_prompt") + " (HH:MM)",
|
||||||
|
)
|
||||||
|
if not ok or not time_str.strip():
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
hour, minute = map(int, time_str.strip().split(":", 1))
|
||||||
|
except ValueError:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
strings._("invalid_time_title"),
|
||||||
|
strings._("invalid_time_message"),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
now = QDateTime.currentDateTime()
|
||||||
|
target = QDateTime(now.date(), QTime(hour, minute))
|
||||||
|
|
||||||
|
t = QTime(hour, minute)
|
||||||
|
if not t.isValid():
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
strings._("invalid_time_title"),
|
||||||
|
strings._("invalid_time_message"),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Normalise to HH:MM
|
||||||
|
time_str = f"{t.hour():02d}:{t.minute():02d}"
|
||||||
|
|
||||||
|
# Insert / update ⏰ in the editor text
|
||||||
|
if hasattr(editor, "insert_alarm_marker"):
|
||||||
|
editor.insert_alarm_marker(time_str)
|
||||||
|
|
||||||
|
# Rebuild timers, but only if this page is for "today"
|
||||||
|
self._rebuild_reminders_for_today()
|
||||||
|
|
||||||
|
def _show_flashing_reminder(self, text: str):
|
||||||
|
"""
|
||||||
|
Show a small flashing dialog and request attention from the OS.
|
||||||
|
Called by reminder timers.
|
||||||
|
"""
|
||||||
|
# Ask OS to flash / bounce our app in the dock/taskbar
|
||||||
|
QApplication.alert(self, 0)
|
||||||
|
|
||||||
|
# Try to bring the window to the front
|
||||||
|
self.showNormal()
|
||||||
|
self.raise_()
|
||||||
|
self.activateWindow()
|
||||||
|
|
||||||
|
# Simple dialog with a flashing background to reinforce the alert
|
||||||
|
dlg = QDialog(self)
|
||||||
|
dlg.setWindowTitle(strings._("reminder"))
|
||||||
|
dlg.setModal(True)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(dlg)
|
||||||
|
label = QLabel(text)
|
||||||
|
label.setWordWrap(True)
|
||||||
|
layout.addWidget(label)
|
||||||
|
|
||||||
|
btn = QPushButton(strings._("dismiss"))
|
||||||
|
btn.clicked.connect(dlg.accept)
|
||||||
|
layout.addWidget(btn)
|
||||||
|
|
||||||
|
flash_timer = QTimer(dlg)
|
||||||
|
flash_state = {"on": False}
|
||||||
|
|
||||||
|
def toggle():
|
||||||
|
flash_state["on"] = not flash_state["on"]
|
||||||
|
if flash_state["on"]:
|
||||||
|
dlg.setStyleSheet("background-color: #3B3B3B;")
|
||||||
|
else:
|
||||||
|
dlg.setStyleSheet("")
|
||||||
|
|
||||||
|
flash_timer.timeout.connect(toggle)
|
||||||
|
flash_timer.start(500) # ms
|
||||||
|
|
||||||
|
dlg.exec()
|
||||||
|
|
||||||
|
flash_timer.stop()
|
||||||
|
|
||||||
|
def _clear_reminder_timers(self):
|
||||||
|
"""Stop and delete any existing reminder timers."""
|
||||||
|
for t in self._reminder_timers:
|
||||||
|
try:
|
||||||
|
t.stop()
|
||||||
|
t.deleteLater()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._reminder_timers = []
|
||||||
|
|
||||||
|
def _rebuild_reminders_for_today(self):
|
||||||
|
"""
|
||||||
|
Scan the markdown for today's date and create QTimers
|
||||||
|
only for alarms on the *current day* (system date).
|
||||||
|
"""
|
||||||
|
# We only ever set timers for the real current date
|
||||||
|
today = QDate.currentDate()
|
||||||
|
today_iso = today.toString("yyyy-MM-dd")
|
||||||
|
|
||||||
|
# Clear any previously scheduled "today" reminders
|
||||||
|
self._clear_reminder_timers()
|
||||||
|
|
||||||
|
# Prefer live editor content if it is showing today's page
|
||||||
|
text = ""
|
||||||
|
if (
|
||||||
|
hasattr(self, "editor")
|
||||||
|
and hasattr(self.editor, "current_date")
|
||||||
|
and self.editor.current_date == today
|
||||||
|
):
|
||||||
|
text = self.editor.to_markdown()
|
||||||
|
else:
|
||||||
|
# Fallback to DB: still only today's date
|
||||||
|
text = self.db.get_entry(today_iso) if hasattr(self, "db") else ""
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
|
||||||
|
now = QDateTime.currentDateTime()
|
||||||
|
|
||||||
|
for line in text.splitlines():
|
||||||
|
# Look for "⏰ HH:MM" anywhere in the line
|
||||||
|
m = re.search(r"⏰\s*(\d{1,2}):(\d{2})", line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
|
||||||
|
hour = int(m.group(1))
|
||||||
|
minute = int(m.group(2))
|
||||||
|
|
||||||
|
t = QTime(hour, minute)
|
||||||
|
if not t.isValid():
|
||||||
|
continue
|
||||||
|
|
||||||
|
target = QDateTime(today, t)
|
||||||
|
|
||||||
|
# Skip alarms that are already in the past
|
||||||
|
if target <= now:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# The reminder text is the part before the symbol
|
||||||
|
reminder_text = line.split("⏰", 1)[0].strip()
|
||||||
|
if not reminder_text:
|
||||||
|
reminder_text = strings._("reminder_no_text_fallback")
|
||||||
|
|
||||||
|
msecs = now.msecsTo(target)
|
||||||
|
timer = QTimer(self)
|
||||||
|
timer.setSingleShot(True)
|
||||||
|
timer.timeout.connect(
|
||||||
|
lambda txt=reminder_text: self._show_flashing_reminder(txt)
|
||||||
|
)
|
||||||
|
timer.start(msecs)
|
||||||
|
self._reminder_timers.append(timer)
|
||||||
|
|
||||||
# ----------- History handler ------------#
|
# ----------- History handler ------------#
|
||||||
def _open_history(self):
|
def _open_history(self):
|
||||||
if hasattr(self.editor, "current_date"):
|
if hasattr(self.editor, "current_date"):
|
||||||
|
|
|
||||||
|
|
@ -391,12 +391,41 @@ class MarkdownEditor(QTextEdit):
|
||||||
cursor.setPosition(match.end(), QTextCursor.MoveMode.KeepAnchor)
|
cursor.setPosition(match.end(), QTextCursor.MoveMode.KeepAnchor)
|
||||||
cursor.insertImage(img_format)
|
cursor.insertImage(img_format)
|
||||||
|
|
||||||
|
def insert_alarm_marker(self, time_str: str) -> None:
|
||||||
|
"""
|
||||||
|
Append or replace an ⏰ HH:MM marker on the current line.
|
||||||
|
time_str is expected to be 'HH:MM'.
|
||||||
|
"""
|
||||||
|
cursor = self.textCursor()
|
||||||
|
block = cursor.block()
|
||||||
|
line = block.text()
|
||||||
|
|
||||||
|
# Strip any existing ⏰ HH:MM at the end of the line
|
||||||
|
new_line = re.sub(r"\s*⏰\s*\d{1,2}:\d{2}\s*$", "", line).rstrip()
|
||||||
|
|
||||||
|
# Append the new marker
|
||||||
|
new_line = f"{new_line} ⏰ {time_str}"
|
||||||
|
|
||||||
|
bc = QTextCursor(block)
|
||||||
|
bc.beginEditBlock()
|
||||||
|
bc.select(QTextCursor.SelectionType.BlockUnderCursor)
|
||||||
|
bc.insertText(new_line)
|
||||||
|
bc.endEditBlock()
|
||||||
|
|
||||||
|
# Move cursor to end of the edited line
|
||||||
|
cursor.setPosition(block.position() + len(new_line))
|
||||||
|
self.setTextCursor(cursor)
|
||||||
|
|
||||||
def _get_current_line(self) -> str:
|
def _get_current_line(self) -> str:
|
||||||
"""Get the text of the current line."""
|
"""Get the text of the current line."""
|
||||||
cursor = self.textCursor()
|
cursor = self.textCursor()
|
||||||
cursor.select(QTextCursor.SelectionType.LineUnderCursor)
|
cursor.select(QTextCursor.SelectionType.LineUnderCursor)
|
||||||
return cursor.selectedText()
|
return cursor.selectedText()
|
||||||
|
|
||||||
|
def get_current_line_text(self) -> str:
|
||||||
|
"""Public wrapper used by MainWindow for reminders."""
|
||||||
|
return self._get_current_line()
|
||||||
|
|
||||||
def _detect_list_type(self, line: str) -> tuple[str | None, str]:
|
def _detect_list_type(self, line: str) -> tuple[str | None, str]:
|
||||||
"""
|
"""
|
||||||
Detect if line is a list item. Returns (list_type, prefix).
|
Detect if line is a list item. Returns (list_type, prefix).
|
||||||
|
|
|
||||||
|
|
@ -281,7 +281,7 @@ class TagGraphDialog(QDialog):
|
||||||
|
|
||||||
# Update labels
|
# Update labels
|
||||||
for i, label in enumerate(self._label_items):
|
for i, label in enumerate(self._label_items):
|
||||||
label.setPos(float(pos[i, 0]), float(pos[i, 1]) + 0.30)
|
label.setPos(float(pos[i, 0]), float(pos[i, 1]) + 0.15)
|
||||||
|
|
||||||
# Update halo positions to match nodes
|
# Update halo positions to match nodes
|
||||||
if self._halo_sizes and self._halo_brushes:
|
if self._halo_sizes and self._halo_brushes:
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class ToolBar(QToolBar):
|
||||||
checkboxesRequested = Signal()
|
checkboxesRequested = Signal()
|
||||||
historyRequested = Signal()
|
historyRequested = Signal()
|
||||||
insertImageRequested = Signal()
|
insertImageRequested = Signal()
|
||||||
|
alarmRequested = Signal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(strings._("toolbar_format"), parent)
|
super().__init__(strings._("toolbar_format"), parent)
|
||||||
|
|
@ -85,6 +86,11 @@ class ToolBar(QToolBar):
|
||||||
self.actCheckboxes.setToolTip(strings._("toolbar_toggle_checkboxes"))
|
self.actCheckboxes.setToolTip(strings._("toolbar_toggle_checkboxes"))
|
||||||
self.actCheckboxes.triggered.connect(self.checkboxesRequested)
|
self.actCheckboxes.triggered.connect(self.checkboxesRequested)
|
||||||
|
|
||||||
|
# Alarm / reminder
|
||||||
|
self.actAlarm = QAction("⏰", self)
|
||||||
|
self.actAlarm.setToolTip(strings._("toolbar_alarm"))
|
||||||
|
self.actAlarm.triggered.connect(self.alarmRequested)
|
||||||
|
|
||||||
# Images
|
# Images
|
||||||
self.actInsertImg = QAction(strings._("images"), self)
|
self.actInsertImg = QAction(strings._("images"), self)
|
||||||
self.actInsertImg.setToolTip(strings._("insert_images"))
|
self.actInsertImg.setToolTip(strings._("insert_images"))
|
||||||
|
|
@ -129,6 +135,7 @@ class ToolBar(QToolBar):
|
||||||
self.actBullets,
|
self.actBullets,
|
||||||
self.actNumbers,
|
self.actNumbers,
|
||||||
self.actCheckboxes,
|
self.actCheckboxes,
|
||||||
|
self.actAlarm,
|
||||||
self.actInsertImg,
|
self.actInsertImg,
|
||||||
self.actHistory,
|
self.actHistory,
|
||||||
]
|
]
|
||||||
|
|
@ -151,6 +158,8 @@ class ToolBar(QToolBar):
|
||||||
# Lists
|
# Lists
|
||||||
self._style_letter_button(self.actBullets, "•")
|
self._style_letter_button(self.actBullets, "•")
|
||||||
self._style_letter_button(self.actNumbers, "1.")
|
self._style_letter_button(self.actNumbers, "1.")
|
||||||
|
self._style_letter_button(self.actCheckboxes, "☐")
|
||||||
|
self._style_letter_button(self.actAlarm, "⏰")
|
||||||
|
|
||||||
# History
|
# History
|
||||||
self._style_letter_button(self.actHistory, strings._("view_history"))
|
self._style_letter_button(self.actHistory, strings._("view_history"))
|
||||||
|
|
|
||||||
BIN
screenshots/bouquin_tag_relationship_graph.png
Normal file
BIN
screenshots/bouquin_tag_relationship_graph.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Loading…
Add table
Add a link
Reference in a new issue