Reminders improvements
* Fix Reminders to fire right on the minute after adding them during runtime * It is now possible to set up Webhooks for Reminders! A URL and a secret value (sent as X-Bouquin-Header) can be set in the Settings
This commit is contained in:
parent
d809244cf8
commit
3106d408ab
6 changed files with 200 additions and 54 deletions
|
|
@ -3,6 +3,8 @@
|
||||||
* Reduce the scope for toggling a checkbox on/off when not clicking precisely on it (must be to the left of the first letter)
|
* Reduce the scope for toggling a checkbox on/off when not clicking precisely on it (must be to the left of the first letter)
|
||||||
* Moving unchecked TODO items to the next weekday now brings across the header that was above it, if present
|
* Moving unchecked TODO items to the next weekday now brings across the header that was above it, if present
|
||||||
* Invoicing should not be enabled by default
|
* Invoicing should not be enabled by default
|
||||||
|
* Fix Reminders to fire right on the minute after adding them during runtime
|
||||||
|
* It is now possible to set up Webhooks for Reminders! A URL and a secret value (sent as X-Bouquin-Header) can be set in the Settings.
|
||||||
|
|
||||||
# 0.7.0
|
# 0.7.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,9 @@
|
||||||
"enable_tags_feature": "Enable Tags",
|
"enable_tags_feature": "Enable Tags",
|
||||||
"enable_time_log_feature": "Enable Time Logging",
|
"enable_time_log_feature": "Enable Time Logging",
|
||||||
"enable_reminders_feature": "Enable Reminders",
|
"enable_reminders_feature": "Enable Reminders",
|
||||||
|
"reminders_webhook_section_title": "Send Reminders to a webhook",
|
||||||
|
"reminders_webhook_url_label":"Webhook URL",
|
||||||
|
"reminders_webhook_secret_label": "Webhook Secret (sent as\nX-Bouquin-Secret header)",
|
||||||
"enable_documents_feature": "Enable storing of documents",
|
"enable_documents_feature": "Enable storing of documents",
|
||||||
"pomodoro_time_log_default_text": "Focus session",
|
"pomodoro_time_log_default_text": "Focus session",
|
||||||
"toolbar_pomodoro_timer": "Time-logging timer",
|
"toolbar_pomodoro_timer": "Time-logging timer",
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ from .key_prompt import KeyPrompt
|
||||||
from .lock_overlay import LockOverlay
|
from .lock_overlay import LockOverlay
|
||||||
from .markdown_editor import MarkdownEditor
|
from .markdown_editor import MarkdownEditor
|
||||||
from .pomodoro_timer import PomodoroManager
|
from .pomodoro_timer import PomodoroManager
|
||||||
from .reminders import UpcomingRemindersWidget
|
from .reminders import UpcomingRemindersWidget, ReminderWebHook
|
||||||
from .save_dialog import SaveDialog
|
from .save_dialog import SaveDialog
|
||||||
from .search import Search
|
from .search import Search
|
||||||
from .settings import APP_NAME, APP_ORG, load_db_config, save_db_config
|
from .settings import APP_NAME, APP_ORG, load_db_config, save_db_config
|
||||||
|
|
@ -115,6 +115,7 @@ class MainWindow(QMainWindow):
|
||||||
self.tags.tagAdded.connect(self._on_tag_added)
|
self.tags.tagAdded.connect(self._on_tag_added)
|
||||||
|
|
||||||
self.upcoming_reminders = UpcomingRemindersWidget(self.db)
|
self.upcoming_reminders = UpcomingRemindersWidget(self.db)
|
||||||
|
self.upcoming_reminders.reminderTriggered.connect(self._send_reminder_webhook)
|
||||||
self.upcoming_reminders.reminderTriggered.connect(self._show_flashing_reminder)
|
self.upcoming_reminders.reminderTriggered.connect(self._show_flashing_reminder)
|
||||||
|
|
||||||
# When invoices change reminders (e.g. invoice paid), refresh the Reminders widget
|
# When invoices change reminders (e.g. invoice paid), refresh the Reminders widget
|
||||||
|
|
@ -1335,6 +1336,11 @@ class MainWindow(QMainWindow):
|
||||||
# Turned off -> cancel any running timer and remove the widget
|
# Turned off -> cancel any running timer and remove the widget
|
||||||
self.pomodoro_manager.cancel_timer()
|
self.pomodoro_manager.cancel_timer()
|
||||||
|
|
||||||
|
def _send_reminder_webhook(self, text: str):
|
||||||
|
if self.cfg.reminders and self.cfg.reminders_webhook_url:
|
||||||
|
reminder_webhook = ReminderWebHook(text)
|
||||||
|
reminder_webhook._send()
|
||||||
|
|
||||||
def _show_flashing_reminder(self, text: str):
|
def _show_flashing_reminder(self, text: str):
|
||||||
"""
|
"""
|
||||||
Show a small flashing dialog and request attention from the OS.
|
Show a small flashing dialog and request attention from the OS.
|
||||||
|
|
@ -1563,6 +1569,12 @@ class MainWindow(QMainWindow):
|
||||||
self.cfg.tags = getattr(new_cfg, "tags", self.cfg.tags)
|
self.cfg.tags = getattr(new_cfg, "tags", self.cfg.tags)
|
||||||
self.cfg.time_log = getattr(new_cfg, "time_log", self.cfg.time_log)
|
self.cfg.time_log = getattr(new_cfg, "time_log", self.cfg.time_log)
|
||||||
self.cfg.reminders = getattr(new_cfg, "reminders", self.cfg.reminders)
|
self.cfg.reminders = getattr(new_cfg, "reminders", self.cfg.reminders)
|
||||||
|
self.cfg.reminders_webhook_url = getattr(
|
||||||
|
new_cfg, "reminders_webhook_url", self.cfg.reminders_webhook_url
|
||||||
|
)
|
||||||
|
self.cfg.reminders_webhook_secret = getattr(
|
||||||
|
new_cfg, "reminders_webhook_secret", self.cfg.reminders_webhook_secret
|
||||||
|
)
|
||||||
self.cfg.documents = getattr(new_cfg, "documents", self.cfg.documents)
|
self.cfg.documents = getattr(new_cfg, "documents", self.cfg.documents)
|
||||||
self.cfg.invoicing = getattr(new_cfg, "invoicing", self.cfg.invoicing)
|
self.cfg.invoicing = getattr(new_cfg, "invoicing", self.cfg.invoicing)
|
||||||
self.cfg.locale = getattr(new_cfg, "locale", self.cfg.locale)
|
self.cfg.locale = getattr(new_cfg, "locale", self.cfg.locale)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PySide6.QtCore import QDate, QDateTime, Qt, QTime, QTimer, Signal, Slot
|
from PySide6.QtCore import QDate, QDateTime, Qt, QTime, QTimer, Signal, Slot, QObject
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
|
|
@ -32,6 +32,9 @@ from PySide6.QtWidgets import (
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
from .db import DBManager
|
from .db import DBManager
|
||||||
|
from .settings import load_db_config
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
class ReminderType(Enum):
|
class ReminderType(Enum):
|
||||||
|
|
@ -332,43 +335,36 @@ class UpcomingRemindersWidget(QFrame):
|
||||||
main.addWidget(self.body)
|
main.addWidget(self.body)
|
||||||
|
|
||||||
# Timer to check and fire reminders
|
# Timer to check and fire reminders
|
||||||
# Start by syncing to the next minute boundary
|
#
|
||||||
self._check_timer = QTimer(self)
|
# We tick once per second, but only hit the DB when the clock is
|
||||||
self._check_timer.timeout.connect(self._check_reminders)
|
# exactly on a :00 second. That way a reminder for HH:MM fires at
|
||||||
|
# HH:MM:00, independent of when it was created.
|
||||||
|
self._tick_timer = QTimer(self)
|
||||||
|
self._tick_timer.setInterval(1000) # 1 second
|
||||||
|
self._tick_timer.timeout.connect(self._on_tick)
|
||||||
|
self._tick_timer.start()
|
||||||
|
|
||||||
# Calculate milliseconds until next minute (HH:MM:00)
|
# Also check once on startup so we don't miss reminders that
|
||||||
|
# should have fired a moment ago when the app wasn't running.
|
||||||
|
QTimer.singleShot(0, self._check_reminders)
|
||||||
|
|
||||||
|
def _on_tick(self) -> None:
|
||||||
|
"""Called every second; run reminder check only on exact minute boundaries."""
|
||||||
now = QDateTime.currentDateTime()
|
now = QDateTime.currentDateTime()
|
||||||
current_second = now.time().second()
|
if now.time().second() == 0:
|
||||||
current_msec = now.time().msec()
|
# Only do the heavier DB work once per minute, at HH:MM:00,
|
||||||
|
# so reminders are aligned to the clock and not to when they
|
||||||
# Milliseconds until next minute
|
# were created.
|
||||||
ms_until_next_minute = (60 - current_second) * 1000 - current_msec
|
self._check_reminders(now)
|
||||||
|
|
||||||
# Start with a single-shot to sync to the minute
|
|
||||||
self._sync_timer = QTimer(self)
|
|
||||||
self._sync_timer.setSingleShot(True)
|
|
||||||
self._sync_timer.timeout.connect(self._start_regular_timer)
|
|
||||||
self._sync_timer.start(ms_until_next_minute)
|
|
||||||
|
|
||||||
# Also check immediately in case there are pending reminders
|
|
||||||
QTimer.singleShot(1000, self._check_reminders)
|
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""Cleanup timers when widget is destroyed."""
|
"""Cleanup timers when widget is destroyed."""
|
||||||
try:
|
try:
|
||||||
if hasattr(self, "_check_timer") and self._check_timer:
|
if hasattr(self, "_tick_timer") and self._tick_timer:
|
||||||
self._check_timer.stop()
|
self._tick_timer.stop()
|
||||||
if hasattr(self, "_sync_timer") and self._sync_timer:
|
except Exception:
|
||||||
self._sync_timer.stop()
|
|
||||||
except:
|
|
||||||
pass # Ignore any cleanup errors
|
pass # Ignore any cleanup errors
|
||||||
|
|
||||||
def _start_regular_timer(self):
|
|
||||||
"""Start the regular check timer after initial sync."""
|
|
||||||
# Now we're at a minute boundary, check and start regular timer
|
|
||||||
self._check_reminders()
|
|
||||||
self._check_timer.start(60000) # Check every minute
|
|
||||||
|
|
||||||
def _on_toggle(self, checked: bool):
|
def _on_toggle(self, checked: bool):
|
||||||
"""Toggle visibility of reminder list."""
|
"""Toggle visibility of reminder list."""
|
||||||
self.body.setVisible(checked)
|
self.body.setVisible(checked)
|
||||||
|
|
@ -492,21 +488,28 @@ class UpcomingRemindersWidget(QFrame):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _check_reminders(self):
|
def _check_reminders(self, now: QDateTime | None = None):
|
||||||
"""Check if any reminders should fire now."""
|
"""
|
||||||
|
Check and trigger due reminders.
|
||||||
|
|
||||||
|
This uses absolute clock time, so a reminder for HH:MM will fire
|
||||||
|
when the system clock reaches HH:MM:00, independent of when the
|
||||||
|
reminder was created.
|
||||||
|
"""
|
||||||
# Guard: Check if database connection is valid
|
# Guard: Check if database connection is valid
|
||||||
if not self._db or not hasattr(self._db, "conn") or self._db.conn is None:
|
if not self._db or not hasattr(self._db, "conn") or self._db.conn is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
now = QDateTime.currentDateTime()
|
if now is None:
|
||||||
today = QDate.currentDate()
|
now = QDateTime.currentDateTime()
|
||||||
|
|
||||||
# Round current time to the minute (set seconds to 0)
|
|
||||||
current_minute = QDateTime(
|
|
||||||
today, QTime(now.time().hour(), now.time().minute(), 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
today = now.date()
|
||||||
reminders = self._db.get_all_reminders()
|
reminders = self._db.get_all_reminders()
|
||||||
|
|
||||||
|
# Small grace window (in seconds) so we still fire reminders if
|
||||||
|
# the app was just opened or the event loop was briefly busy.
|
||||||
|
GRACE_WINDOW_SECS = 120 # 2 minutes
|
||||||
|
|
||||||
for reminder in reminders:
|
for reminder in reminders:
|
||||||
if not reminder.active:
|
if not reminder.active:
|
||||||
continue
|
continue
|
||||||
|
|
@ -514,28 +517,35 @@ class UpcomingRemindersWidget(QFrame):
|
||||||
if not self._should_fire_on_date(reminder, today):
|
if not self._should_fire_on_date(reminder, today):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Parse time
|
# Parse time: stored as "HH:MM", we treat that as HH:MM:00
|
||||||
hour, minute = map(int, reminder.time_str.split(":"))
|
hour, minute = map(int, reminder.time_str.split(":"))
|
||||||
target = QDateTime(today, QTime(hour, minute, 0))
|
target = QDateTime(today, QTime(hour, minute, 0))
|
||||||
|
|
||||||
# Fire if we've passed the target minute (within last 2 minutes to catch missed ones)
|
# Skip if this reminder is still in the future
|
||||||
seconds_diff = current_minute.secsTo(target)
|
if now < target:
|
||||||
if -120 <= seconds_diff <= 0:
|
continue
|
||||||
# Check if we haven't already fired this one
|
|
||||||
|
# How long ago should this reminder have fired?
|
||||||
|
seconds_late = target.secsTo(now) # target -> now
|
||||||
|
|
||||||
|
if 0 <= seconds_late <= GRACE_WINDOW_SECS:
|
||||||
|
# Check if we haven't already fired this occurrence
|
||||||
if not hasattr(self, "_fired_reminders"):
|
if not hasattr(self, "_fired_reminders"):
|
||||||
self._fired_reminders = {}
|
self._fired_reminders = {}
|
||||||
|
|
||||||
reminder_key = (reminder.id, target.toString())
|
reminder_key = (reminder.id, target.toString())
|
||||||
|
|
||||||
# Only fire once per reminder per target time
|
if reminder_key in self._fired_reminders:
|
||||||
if reminder_key not in self._fired_reminders:
|
continue
|
||||||
self._fired_reminders[reminder_key] = current_minute
|
|
||||||
self.reminderTriggered.emit(reminder.text)
|
|
||||||
|
|
||||||
# For ONCE reminders, deactivate after firing
|
# Mark as fired and emit
|
||||||
if reminder.reminder_type == ReminderType.ONCE:
|
self._fired_reminders[reminder_key] = now
|
||||||
self._db.update_reminder_active(reminder.id, False)
|
self.reminderTriggered.emit(reminder.text)
|
||||||
self.refresh() # Refresh the list to show deactivated reminder
|
|
||||||
|
# For ONCE reminders, deactivate after firing
|
||||||
|
if reminder.reminder_type == ReminderType.ONCE:
|
||||||
|
self._db.update_reminder_active(reminder.id, False)
|
||||||
|
self.refresh() # Refresh the list to show deactivated reminder
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def _add_reminder(self):
|
def _add_reminder(self):
|
||||||
|
|
@ -834,3 +844,33 @@ class ManageRemindersDialog(QDialog):
|
||||||
if reply == QMessageBox.Yes:
|
if reply == QMessageBox.Yes:
|
||||||
self._db.delete_reminder(reminder.id)
|
self._db.delete_reminder(reminder.id)
|
||||||
self._load_reminders()
|
self._load_reminders()
|
||||||
|
|
||||||
|
|
||||||
|
class ReminderWebHook:
|
||||||
|
def __init__(self, text):
|
||||||
|
self.text = text
|
||||||
|
self.cfg = load_db_config()
|
||||||
|
|
||||||
|
def _send(self):
|
||||||
|
payload: dict[str, str] = {
|
||||||
|
"reminder": self.text,
|
||||||
|
}
|
||||||
|
|
||||||
|
url = self.cfg.reminders_webhook_url
|
||||||
|
secret = self.cfg.reminders_webhook_secret
|
||||||
|
|
||||||
|
_headers = {}
|
||||||
|
if secret:
|
||||||
|
_headers["X-Bouquin-Secret"] = secret
|
||||||
|
|
||||||
|
if url:
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
url,
|
||||||
|
json=payload,
|
||||||
|
timeout=10,
|
||||||
|
headers=_headers,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# We did our best
|
||||||
|
pass
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ def load_db_config() -> DBConfig:
|
||||||
tags = s.value("ui/tags", True, type=bool)
|
tags = s.value("ui/tags", True, type=bool)
|
||||||
time_log = s.value("ui/time_log", True, type=bool)
|
time_log = s.value("ui/time_log", True, type=bool)
|
||||||
reminders = s.value("ui/reminders", True, type=bool)
|
reminders = s.value("ui/reminders", True, type=bool)
|
||||||
|
reminders_webhook_url = s.value("ui/reminders_webhook_url", None, type=str)
|
||||||
|
reminders_webhook_secret = s.value("ui/reminders_webhook_secret", None, type=str)
|
||||||
documents = s.value("ui/documents", True, type=bool)
|
documents = s.value("ui/documents", True, type=bool)
|
||||||
invoicing = s.value("ui/invoicing", False, type=bool)
|
invoicing = s.value("ui/invoicing", False, type=bool)
|
||||||
locale = s.value("ui/locale", "en", type=str)
|
locale = s.value("ui/locale", "en", type=str)
|
||||||
|
|
@ -58,6 +60,8 @@ def load_db_config() -> DBConfig:
|
||||||
tags=tags,
|
tags=tags,
|
||||||
time_log=time_log,
|
time_log=time_log,
|
||||||
reminders=reminders,
|
reminders=reminders,
|
||||||
|
reminders_webhook_url=reminders_webhook_url,
|
||||||
|
reminders_webhook_secret=reminders_webhook_secret,
|
||||||
documents=documents,
|
documents=documents,
|
||||||
invoicing=invoicing,
|
invoicing=invoicing,
|
||||||
locale=locale,
|
locale=locale,
|
||||||
|
|
@ -75,6 +79,8 @@ def save_db_config(cfg: DBConfig) -> None:
|
||||||
s.setValue("ui/tags", str(cfg.tags))
|
s.setValue("ui/tags", str(cfg.tags))
|
||||||
s.setValue("ui/time_log", str(cfg.time_log))
|
s.setValue("ui/time_log", str(cfg.time_log))
|
||||||
s.setValue("ui/reminders", str(cfg.reminders))
|
s.setValue("ui/reminders", str(cfg.reminders))
|
||||||
|
s.setValue("ui/reminders_webhook_url", str(cfg.reminders_webhook_url))
|
||||||
|
s.setValue("ui/reminders_webhook_secret", str(cfg.reminders_webhook_secret))
|
||||||
s.setValue("ui/documents", str(cfg.documents))
|
s.setValue("ui/documents", str(cfg.documents))
|
||||||
s.setValue("ui/invoicing", str(cfg.invoicing))
|
s.setValue("ui/invoicing", str(cfg.invoicing))
|
||||||
s.setValue("ui/locale", str(cfg.locale))
|
s.setValue("ui/locale", str(cfg.locale))
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ from PySide6.QtWidgets import (
|
||||||
QSpinBox,
|
QSpinBox,
|
||||||
QTabWidget,
|
QTabWidget,
|
||||||
QTextEdit,
|
QTextEdit,
|
||||||
|
QToolButton,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
@ -44,7 +45,7 @@ class SettingsDialog(QDialog):
|
||||||
|
|
||||||
self.current_settings = load_db_config()
|
self.current_settings = load_db_config()
|
||||||
|
|
||||||
self.setMinimumWidth(480)
|
self.setMinimumWidth(600)
|
||||||
self.setSizeGripEnabled(True)
|
self.setSizeGripEnabled(True)
|
||||||
|
|
||||||
# --- Tabs ----------------------------------------------------------
|
# --- Tabs ----------------------------------------------------------
|
||||||
|
|
@ -189,11 +190,66 @@ class SettingsDialog(QDialog):
|
||||||
self.invoicing.setEnabled(False)
|
self.invoicing.setEnabled(False)
|
||||||
self.time_log.toggled.connect(self._on_time_log_toggled)
|
self.time_log.toggled.connect(self._on_time_log_toggled)
|
||||||
|
|
||||||
|
# --- Reminders feature + webhook options -------------------------
|
||||||
self.reminders = QCheckBox(strings._("enable_reminders_feature"))
|
self.reminders = QCheckBox(strings._("enable_reminders_feature"))
|
||||||
self.reminders.setChecked(self.current_settings.reminders)
|
self.reminders.setChecked(self.current_settings.reminders)
|
||||||
|
self.reminders.toggled.connect(self._on_reminders_toggled)
|
||||||
self.reminders.setCursor(Qt.PointingHandCursor)
|
self.reminders.setCursor(Qt.PointingHandCursor)
|
||||||
features_layout.addWidget(self.reminders)
|
features_layout.addWidget(self.reminders)
|
||||||
|
|
||||||
|
# Container for reminder-specific options, indented under the checkbox
|
||||||
|
self.reminders_options_container = QWidget()
|
||||||
|
reminders_options_layout = QVBoxLayout(self.reminders_options_container)
|
||||||
|
reminders_options_layout.setContentsMargins(24, 0, 0, 0)
|
||||||
|
reminders_options_layout.setSpacing(4)
|
||||||
|
|
||||||
|
self.reminders_options_toggle = QToolButton()
|
||||||
|
self.reminders_options_toggle.setText(
|
||||||
|
strings._("reminders_webhook_section_title")
|
||||||
|
)
|
||||||
|
self.reminders_options_toggle.setCheckable(True)
|
||||||
|
self.reminders_options_toggle.setChecked(False)
|
||||||
|
self.reminders_options_toggle.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
|
||||||
|
self.reminders_options_toggle.setArrowType(Qt.RightArrow)
|
||||||
|
self.reminders_options_toggle.clicked.connect(
|
||||||
|
self._on_reminders_options_toggled
|
||||||
|
)
|
||||||
|
|
||||||
|
toggle_row = QHBoxLayout()
|
||||||
|
toggle_row.addWidget(self.reminders_options_toggle)
|
||||||
|
toggle_row.addStretch()
|
||||||
|
reminders_options_layout.addLayout(toggle_row)
|
||||||
|
|
||||||
|
# Actual options (labels + QLineEdits)
|
||||||
|
self.reminders_options_widget = QWidget()
|
||||||
|
options_form = QFormLayout(self.reminders_options_widget)
|
||||||
|
options_form.setContentsMargins(0, 0, 0, 0)
|
||||||
|
options_form.setSpacing(4)
|
||||||
|
|
||||||
|
self.reminders_webhook_url = QLineEdit(
|
||||||
|
self.current_settings.reminders_webhook_url or ""
|
||||||
|
)
|
||||||
|
self.reminders_webhook_secret = QLineEdit(
|
||||||
|
self.current_settings.reminders_webhook_secret or ""
|
||||||
|
)
|
||||||
|
self.reminders_webhook_secret.setEchoMode(QLineEdit.Password)
|
||||||
|
|
||||||
|
options_form.addRow(
|
||||||
|
strings._("reminders_webhook_url_label") + ":",
|
||||||
|
self.reminders_webhook_url,
|
||||||
|
)
|
||||||
|
options_form.addRow(
|
||||||
|
strings._("reminders_webhook_secret_label") + ":",
|
||||||
|
self.reminders_webhook_secret,
|
||||||
|
)
|
||||||
|
|
||||||
|
reminders_options_layout.addWidget(self.reminders_options_widget)
|
||||||
|
|
||||||
|
features_layout.addWidget(self.reminders_options_container)
|
||||||
|
|
||||||
|
self.reminders_options_container.setVisible(self.reminders.isChecked())
|
||||||
|
self.reminders_options_widget.setVisible(False)
|
||||||
|
|
||||||
self.documents = QCheckBox(strings._("enable_documents_feature"))
|
self.documents = QCheckBox(strings._("enable_documents_feature"))
|
||||||
self.documents.setChecked(self.current_settings.documents)
|
self.documents.setChecked(self.current_settings.documents)
|
||||||
self.documents.setCursor(Qt.PointingHandCursor)
|
self.documents.setCursor(Qt.PointingHandCursor)
|
||||||
|
|
@ -388,6 +444,9 @@ class SettingsDialog(QDialog):
|
||||||
tags=self.tags.isChecked(),
|
tags=self.tags.isChecked(),
|
||||||
time_log=self.time_log.isChecked(),
|
time_log=self.time_log.isChecked(),
|
||||||
reminders=self.reminders.isChecked(),
|
reminders=self.reminders.isChecked(),
|
||||||
|
reminders_webhook_url=self.reminders_webhook_url.text().strip() or None,
|
||||||
|
reminders_webhook_secret=self.reminders_webhook_secret.text().strip()
|
||||||
|
or None,
|
||||||
documents=self.documents.isChecked(),
|
documents=self.documents.isChecked(),
|
||||||
invoicing=(
|
invoicing=(
|
||||||
self.invoicing.isChecked() if self.time_log.isChecked() else False
|
self.invoicing.isChecked() if self.time_log.isChecked() else False
|
||||||
|
|
@ -414,6 +473,30 @@ class SettingsDialog(QDialog):
|
||||||
self.parent().themes.set(selected_theme)
|
self.parent().themes.set(selected_theme)
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
def _on_reminders_options_toggled(self, checked: bool) -> None:
|
||||||
|
"""
|
||||||
|
Expand/collapse the advanced reminders options (webhook URL/secret).
|
||||||
|
"""
|
||||||
|
if checked:
|
||||||
|
self.reminders_options_toggle.setArrowType(Qt.DownArrow)
|
||||||
|
self.reminders_options_widget.show()
|
||||||
|
else:
|
||||||
|
self.reminders_options_toggle.setArrowType(Qt.RightArrow)
|
||||||
|
self.reminders_options_widget.hide()
|
||||||
|
|
||||||
|
def _on_reminders_toggled(self, checked: bool) -> None:
|
||||||
|
"""
|
||||||
|
Conditionally show reminder webhook options depending
|
||||||
|
on if the reminders feature is toggled on or off.
|
||||||
|
"""
|
||||||
|
if hasattr(self, "reminders_options_container"):
|
||||||
|
self.reminders_options_container.setVisible(checked)
|
||||||
|
|
||||||
|
# When turning reminders off, also collapse the section
|
||||||
|
if not checked and hasattr(self, "reminders_options_toggle"):
|
||||||
|
self.reminders_options_toggle.setChecked(False)
|
||||||
|
self._on_reminders_options_toggled(False)
|
||||||
|
|
||||||
def _on_time_log_toggled(self, checked: bool) -> None:
|
def _on_time_log_toggled(self, checked: bool) -> None:
|
||||||
"""
|
"""
|
||||||
Enforce 'invoicing depends on time logging'.
|
Enforce 'invoicing depends on time logging'.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue