bouquin 0.7.0__tar.gz → 0.7.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bouquin-0.7.0 → bouquin-0.7.2}/PKG-INFO +1 -1
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/db.py +128 -2
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/locales/en.json +16 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/main_window.py +135 -10
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/markdown_editor.py +39 -9
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/reminders.py +161 -80
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/settings.py +7 -1
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/settings_dialog.py +84 -1
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/statistics_dialog.py +177 -39
- {bouquin-0.7.0 → bouquin-0.7.2}/pyproject.toml +1 -1
- {bouquin-0.7.0 → bouquin-0.7.2}/LICENSE +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/README.md +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/__init__.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/__main__.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/bug_report_dialog.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/code_block_editor_dialog.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/code_highlighter.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/document_utils.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/documents.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/find_bar.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/flow_layout.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/fonts/DejaVu.license +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/fonts/DejaVuSans.ttf +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/fonts/Noto.license +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/fonts/NotoSansSymbols2-Regular.ttf +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/history_dialog.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/icons/bouquin.svg +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/invoices.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/key_prompt.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/keys/mig5.asc +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/locales/fr.json +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/locales/it.json +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/lock_overlay.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/main.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/markdown_highlighter.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/pomodoro_timer.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/save_dialog.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/search.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/strings.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/tag_browser.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/tags_widget.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/theme.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/time_log.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/toolbar.py +0 -0
- {bouquin-0.7.0 → bouquin-0.7.2}/bouquin/version_check.py +0 -0
|
@@ -95,6 +95,8 @@ class DBConfig:
|
|
|
95
95
|
tags: bool = True
|
|
96
96
|
time_log: bool = True
|
|
97
97
|
reminders: bool = True
|
|
98
|
+
reminders_webhook_url: str = (None,)
|
|
99
|
+
reminders_webhook_secret: str = (None,)
|
|
98
100
|
documents: bool = True
|
|
99
101
|
invoicing: bool = False
|
|
100
102
|
locale: str = "en"
|
|
@@ -971,7 +973,7 @@ class DBManager:
|
|
|
971
973
|
|
|
972
974
|
# 2 & 3) total revisions + page with most revisions + per-date counts
|
|
973
975
|
total_revisions = 0
|
|
974
|
-
page_most_revisions = None
|
|
976
|
+
page_most_revisions: str | None = None
|
|
975
977
|
page_most_revisions_count = 0
|
|
976
978
|
revisions_by_date: Dict[_dt.date, int] = {}
|
|
977
979
|
|
|
@@ -1008,7 +1010,6 @@ class DBManager:
|
|
|
1008
1010
|
words_by_date[d] = wc
|
|
1009
1011
|
|
|
1010
1012
|
# tags + page with most tags
|
|
1011
|
-
|
|
1012
1013
|
rows = cur.execute("SELECT COUNT(*) AS total_unique FROM tags;").fetchall()
|
|
1013
1014
|
unique_tags = int(rows[0]["total_unique"]) if rows else 0
|
|
1014
1015
|
|
|
@@ -1029,6 +1030,119 @@ class DBManager:
|
|
|
1029
1030
|
page_most_tags = None
|
|
1030
1031
|
page_most_tags_count = 0
|
|
1031
1032
|
|
|
1033
|
+
# 5) Time logging stats (minutes / hours)
|
|
1034
|
+
time_minutes_by_date: Dict[_dt.date, int] = {}
|
|
1035
|
+
total_time_minutes = 0
|
|
1036
|
+
day_most_time: str | None = None
|
|
1037
|
+
day_most_time_minutes = 0
|
|
1038
|
+
|
|
1039
|
+
try:
|
|
1040
|
+
rows = cur.execute(
|
|
1041
|
+
"""
|
|
1042
|
+
SELECT page_date, SUM(minutes) AS total_minutes
|
|
1043
|
+
FROM time_log
|
|
1044
|
+
GROUP BY page_date
|
|
1045
|
+
ORDER BY page_date;
|
|
1046
|
+
"""
|
|
1047
|
+
).fetchall()
|
|
1048
|
+
except Exception:
|
|
1049
|
+
rows = []
|
|
1050
|
+
|
|
1051
|
+
for r in rows:
|
|
1052
|
+
date_iso = r["page_date"]
|
|
1053
|
+
if not date_iso:
|
|
1054
|
+
continue
|
|
1055
|
+
m = int(r["total_minutes"] or 0)
|
|
1056
|
+
total_time_minutes += m
|
|
1057
|
+
if m > day_most_time_minutes:
|
|
1058
|
+
day_most_time_minutes = m
|
|
1059
|
+
day_most_time = date_iso
|
|
1060
|
+
try:
|
|
1061
|
+
d = _dt.date.fromisoformat(date_iso)
|
|
1062
|
+
except Exception: # nosec B112
|
|
1063
|
+
continue
|
|
1064
|
+
time_minutes_by_date[d] = m
|
|
1065
|
+
|
|
1066
|
+
# Project with most logged time
|
|
1067
|
+
project_most_minutes_name: str | None = None
|
|
1068
|
+
project_most_minutes = 0
|
|
1069
|
+
|
|
1070
|
+
try:
|
|
1071
|
+
rows = cur.execute(
|
|
1072
|
+
"""
|
|
1073
|
+
SELECT p.name AS project_name,
|
|
1074
|
+
SUM(t.minutes) AS total_minutes
|
|
1075
|
+
FROM time_log t
|
|
1076
|
+
JOIN projects p ON p.id = t.project_id
|
|
1077
|
+
GROUP BY t.project_id, p.name
|
|
1078
|
+
ORDER BY total_minutes DESC, LOWER(project_name) ASC
|
|
1079
|
+
LIMIT 1;
|
|
1080
|
+
"""
|
|
1081
|
+
).fetchall()
|
|
1082
|
+
except Exception:
|
|
1083
|
+
rows = []
|
|
1084
|
+
|
|
1085
|
+
if rows:
|
|
1086
|
+
project_most_minutes_name = rows[0]["project_name"]
|
|
1087
|
+
project_most_minutes = int(rows[0]["total_minutes"] or 0)
|
|
1088
|
+
|
|
1089
|
+
# Activity with most logged time
|
|
1090
|
+
activity_most_minutes_name: str | None = None
|
|
1091
|
+
activity_most_minutes = 0
|
|
1092
|
+
|
|
1093
|
+
try:
|
|
1094
|
+
rows = cur.execute(
|
|
1095
|
+
"""
|
|
1096
|
+
SELECT a.name AS activity_name,
|
|
1097
|
+
SUM(t.minutes) AS total_minutes
|
|
1098
|
+
FROM time_log t
|
|
1099
|
+
JOIN activities a ON a.id = t.activity_id
|
|
1100
|
+
GROUP BY t.activity_id, a.name
|
|
1101
|
+
ORDER BY total_minutes DESC, LOWER(activity_name) ASC
|
|
1102
|
+
LIMIT 1;
|
|
1103
|
+
"""
|
|
1104
|
+
).fetchall()
|
|
1105
|
+
except Exception:
|
|
1106
|
+
rows = []
|
|
1107
|
+
|
|
1108
|
+
if rows:
|
|
1109
|
+
activity_most_minutes_name = rows[0]["activity_name"]
|
|
1110
|
+
activity_most_minutes = int(rows[0]["total_minutes"] or 0)
|
|
1111
|
+
|
|
1112
|
+
# 6) Reminder stats
|
|
1113
|
+
reminders_by_date: Dict[_dt.date, int] = {}
|
|
1114
|
+
total_reminders = 0
|
|
1115
|
+
day_most_reminders: str | None = None
|
|
1116
|
+
day_most_reminders_count = 0
|
|
1117
|
+
|
|
1118
|
+
try:
|
|
1119
|
+
rows = cur.execute(
|
|
1120
|
+
"""
|
|
1121
|
+
SELECT substr(created_at, 1, 10) AS date_iso,
|
|
1122
|
+
COUNT(*) AS c
|
|
1123
|
+
FROM reminders
|
|
1124
|
+
GROUP BY date_iso
|
|
1125
|
+
ORDER BY date_iso;
|
|
1126
|
+
"""
|
|
1127
|
+
).fetchall()
|
|
1128
|
+
except Exception:
|
|
1129
|
+
rows = []
|
|
1130
|
+
|
|
1131
|
+
for r in rows:
|
|
1132
|
+
date_iso = r["date_iso"]
|
|
1133
|
+
if not date_iso:
|
|
1134
|
+
continue
|
|
1135
|
+
c = int(r["c"] or 0)
|
|
1136
|
+
total_reminders += c
|
|
1137
|
+
if c > day_most_reminders_count:
|
|
1138
|
+
day_most_reminders_count = c
|
|
1139
|
+
day_most_reminders = date_iso
|
|
1140
|
+
try:
|
|
1141
|
+
d = _dt.date.fromisoformat(date_iso)
|
|
1142
|
+
except Exception: # nosec B112
|
|
1143
|
+
continue
|
|
1144
|
+
reminders_by_date[d] = c
|
|
1145
|
+
|
|
1032
1146
|
return (
|
|
1033
1147
|
pages_with_content,
|
|
1034
1148
|
total_revisions,
|
|
@@ -1040,6 +1154,18 @@ class DBManager:
|
|
|
1040
1154
|
page_most_tags,
|
|
1041
1155
|
page_most_tags_count,
|
|
1042
1156
|
revisions_by_date,
|
|
1157
|
+
time_minutes_by_date,
|
|
1158
|
+
total_time_minutes,
|
|
1159
|
+
day_most_time,
|
|
1160
|
+
day_most_time_minutes,
|
|
1161
|
+
project_most_minutes_name,
|
|
1162
|
+
project_most_minutes,
|
|
1163
|
+
activity_most_minutes_name,
|
|
1164
|
+
activity_most_minutes,
|
|
1165
|
+
reminders_by_date,
|
|
1166
|
+
total_reminders,
|
|
1167
|
+
day_most_reminders,
|
|
1168
|
+
day_most_reminders_count,
|
|
1043
1169
|
)
|
|
1044
1170
|
|
|
1045
1171
|
# -------- Time logging: projects & activities ---------------------
|
|
@@ -154,6 +154,11 @@
|
|
|
154
154
|
"tag_already_exists_with_that_name": "A tag already exists with that name",
|
|
155
155
|
"statistics": "Statistics",
|
|
156
156
|
"main_window_statistics_accessible_flag": "Stat&istics",
|
|
157
|
+
"stats_group_pages": "Pages",
|
|
158
|
+
"stats_group_tags": "Tags",
|
|
159
|
+
"stats_group_documents": "Documents",
|
|
160
|
+
"stats_group_time_logging": "Time logging",
|
|
161
|
+
"stats_group_reminders": "Reminders",
|
|
157
162
|
"stats_pages_with_content": "Pages with content (current version)",
|
|
158
163
|
"stats_total_revisions": "Total revisions",
|
|
159
164
|
"stats_page_most_revisions": "Page with most revisions",
|
|
@@ -168,6 +173,14 @@
|
|
|
168
173
|
"stats_total_documents": "Total documents",
|
|
169
174
|
"stats_date_most_documents": "Date with most documents",
|
|
170
175
|
"stats_no_data": "No statistics available yet.",
|
|
176
|
+
"stats_time_total_hours": "Total hours logged",
|
|
177
|
+
"stats_time_day_most_hours": "Day with most hours logged",
|
|
178
|
+
"stats_time_project_most_hours": "Project with most hours logged",
|
|
179
|
+
"stats_time_activity_most_hours": "Activity with most hours logged",
|
|
180
|
+
"stats_total_reminders": "Total reminders",
|
|
181
|
+
"stats_date_most_reminders": "Day with most reminders",
|
|
182
|
+
"stats_metric_hours": "Hours",
|
|
183
|
+
"stats_metric_reminders": "Reminders",
|
|
171
184
|
"select_notebook": "Select notebook",
|
|
172
185
|
"bug_report_explanation": "Describe what went wrong, what you expected to happen, and any steps to reproduce.\n\nWe do not collect anything else except the Bouquin version number.\n\nIf you wish to be contacted, please leave contact information.\n\nYour request will be sent over HTTPS.",
|
|
173
186
|
"bug_report_placeholder": "Type your bug report here",
|
|
@@ -277,6 +290,9 @@
|
|
|
277
290
|
"enable_tags_feature": "Enable Tags",
|
|
278
291
|
"enable_time_log_feature": "Enable Time Logging",
|
|
279
292
|
"enable_reminders_feature": "Enable Reminders",
|
|
293
|
+
"reminders_webhook_section_title": "Send Reminders to a webhook",
|
|
294
|
+
"reminders_webhook_url_label":"Webhook URL",
|
|
295
|
+
"reminders_webhook_secret_label": "Webhook Secret (sent as\nX-Bouquin-Secret header)",
|
|
280
296
|
"enable_documents_feature": "Enable storing of documents",
|
|
281
297
|
"pomodoro_time_log_default_text": "Focus session",
|
|
282
298
|
"toolbar_pomodoro_timer": "Time-logging timer",
|
|
@@ -58,7 +58,7 @@ from .key_prompt import KeyPrompt
|
|
|
58
58
|
from .lock_overlay import LockOverlay
|
|
59
59
|
from .markdown_editor import MarkdownEditor
|
|
60
60
|
from .pomodoro_timer import PomodoroManager
|
|
61
|
-
from .reminders import UpcomingRemindersWidget
|
|
61
|
+
from .reminders import UpcomingRemindersWidget, ReminderWebHook
|
|
62
62
|
from .save_dialog import SaveDialog
|
|
63
63
|
from .search import Search
|
|
64
64
|
from .settings import APP_NAME, APP_ORG, load_db_config, save_db_config
|
|
@@ -115,6 +115,7 @@ class MainWindow(QMainWindow):
|
|
|
115
115
|
self.tags.tagAdded.connect(self._on_tag_added)
|
|
116
116
|
|
|
117
117
|
self.upcoming_reminders = UpcomingRemindersWidget(self.db)
|
|
118
|
+
self.upcoming_reminders.reminderTriggered.connect(self._send_reminder_webhook)
|
|
118
119
|
self.upcoming_reminders.reminderTriggered.connect(self._show_flashing_reminder)
|
|
119
120
|
|
|
120
121
|
# When invoices change reminders (e.g. invoice paid), refresh the Reminders widget
|
|
@@ -878,7 +879,74 @@ class MainWindow(QMainWindow):
|
|
|
878
879
|
target_date = self._rollover_target_date(today)
|
|
879
880
|
target_iso = target_date.toString("yyyy-MM-dd")
|
|
880
881
|
|
|
881
|
-
|
|
882
|
+
# Regexes for markdown headings and checkboxes
|
|
883
|
+
heading_re = re.compile(r"^\s{0,3}(#+)\s+(.*)$")
|
|
884
|
+
unchecked_re = re.compile(r"^\s*-\s*\[[\s☐]\]\s+")
|
|
885
|
+
|
|
886
|
+
def _normalize_heading(text: str) -> str:
|
|
887
|
+
"""
|
|
888
|
+
Strip trailing closing hashes and whitespace, e.g.
|
|
889
|
+
"## Foo ###" -> "Foo"
|
|
890
|
+
"""
|
|
891
|
+
text = text.strip()
|
|
892
|
+
text = re.sub(r"\s+#+\s*$", "", text)
|
|
893
|
+
return text.strip()
|
|
894
|
+
|
|
895
|
+
def _insert_todos_under_heading(
|
|
896
|
+
target_lines: list[str],
|
|
897
|
+
heading_level: int,
|
|
898
|
+
heading_text: str,
|
|
899
|
+
todos: list[str],
|
|
900
|
+
) -> list[str]:
|
|
901
|
+
"""Ensure a heading exists and append todos to the end of its section."""
|
|
902
|
+
normalized = _normalize_heading(heading_text)
|
|
903
|
+
|
|
904
|
+
# 1) Find existing heading with same text (any level)
|
|
905
|
+
start_idx = None
|
|
906
|
+
effective_level = None
|
|
907
|
+
for idx, line in enumerate(target_lines):
|
|
908
|
+
m = heading_re.match(line)
|
|
909
|
+
if not m:
|
|
910
|
+
continue
|
|
911
|
+
level = len(m.group(1))
|
|
912
|
+
text = _normalize_heading(m.group(2))
|
|
913
|
+
if text == normalized:
|
|
914
|
+
start_idx = idx
|
|
915
|
+
effective_level = level
|
|
916
|
+
break
|
|
917
|
+
|
|
918
|
+
# 2) If not found, create a new heading at the end
|
|
919
|
+
if start_idx is None:
|
|
920
|
+
if target_lines and target_lines[-1].strip():
|
|
921
|
+
target_lines.append("") # blank line before new heading
|
|
922
|
+
target_lines.append(f"{'#' * heading_level} {heading_text}")
|
|
923
|
+
start_idx = len(target_lines) - 1
|
|
924
|
+
effective_level = heading_level
|
|
925
|
+
|
|
926
|
+
# 3) Find the end of this heading's section
|
|
927
|
+
end_idx = len(target_lines)
|
|
928
|
+
for i in range(start_idx + 1, len(target_lines)):
|
|
929
|
+
m = heading_re.match(target_lines[i])
|
|
930
|
+
if m and len(m.group(1)) <= effective_level:
|
|
931
|
+
end_idx = i
|
|
932
|
+
break
|
|
933
|
+
|
|
934
|
+
# 4) Insert before any trailing blank lines in the section
|
|
935
|
+
insert_at = end_idx
|
|
936
|
+
while (
|
|
937
|
+
insert_at > start_idx + 1 and target_lines[insert_at - 1].strip() == ""
|
|
938
|
+
):
|
|
939
|
+
insert_at -= 1
|
|
940
|
+
|
|
941
|
+
for todo in todos:
|
|
942
|
+
target_lines.insert(insert_at, todo)
|
|
943
|
+
insert_at += 1
|
|
944
|
+
|
|
945
|
+
return target_lines
|
|
946
|
+
|
|
947
|
+
# Collect moved todos as (heading_info, item_text)
|
|
948
|
+
# heading_info is either None or (level, heading_text)
|
|
949
|
+
moved_items: list[tuple[tuple[int, str] | None, str]] = []
|
|
882
950
|
any_moved = False
|
|
883
951
|
|
|
884
952
|
# Look back N days (yesterday = 1, up to `days_back`)
|
|
@@ -892,14 +960,24 @@ class MainWindow(QMainWindow):
|
|
|
892
960
|
lines = text.split("\n")
|
|
893
961
|
remaining_lines: list[str] = []
|
|
894
962
|
moved_from_this_day = False
|
|
963
|
+
current_heading: tuple[int, str] | None = None
|
|
895
964
|
|
|
896
965
|
for line in lines:
|
|
966
|
+
# Track the last seen heading (# / ## / ###)
|
|
967
|
+
m_head = heading_re.match(line)
|
|
968
|
+
if m_head:
|
|
969
|
+
level = len(m_head.group(1))
|
|
970
|
+
heading_text = _normalize_heading(m_head.group(2))
|
|
971
|
+
if level <= 3:
|
|
972
|
+
current_heading = (level, heading_text)
|
|
973
|
+
# Keep headings in the original day
|
|
974
|
+
remaining_lines.append(line)
|
|
975
|
+
continue
|
|
976
|
+
|
|
897
977
|
# Unchecked markdown checkboxes: "- [ ] " or "- [☐] "
|
|
898
|
-
if
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
item_text = re.sub(r"^\s*-\s*\[[\s☐]\]\s+", "", line)
|
|
902
|
-
all_unchecked.append(f"- [ ] {item_text}")
|
|
978
|
+
if unchecked_re.match(line):
|
|
979
|
+
item_text = unchecked_re.sub("", line)
|
|
980
|
+
moved_items.append((current_heading, item_text))
|
|
903
981
|
moved_from_this_day = True
|
|
904
982
|
any_moved = True
|
|
905
983
|
else:
|
|
@@ -917,9 +995,45 @@ class MainWindow(QMainWindow):
|
|
|
917
995
|
if not any_moved:
|
|
918
996
|
return False
|
|
919
997
|
|
|
920
|
-
#
|
|
921
|
-
|
|
922
|
-
self.
|
|
998
|
+
# --- Merge all moved items into the *target* date ---
|
|
999
|
+
|
|
1000
|
+
target_text = self.db.get_entry(target_iso) or ""
|
|
1001
|
+
target_lines = target_text.split("\n") if target_text else []
|
|
1002
|
+
|
|
1003
|
+
by_heading: dict[tuple[int, str], list[str]] = {}
|
|
1004
|
+
plain_items: list[str] = []
|
|
1005
|
+
|
|
1006
|
+
for heading_info, item_text in moved_items:
|
|
1007
|
+
todo_line = f"- [ ] {item_text}"
|
|
1008
|
+
if heading_info is None:
|
|
1009
|
+
# No heading above this checkbox in the source: behave as before
|
|
1010
|
+
plain_items.append(todo_line)
|
|
1011
|
+
else:
|
|
1012
|
+
by_heading.setdefault(heading_info, []).append(todo_line)
|
|
1013
|
+
|
|
1014
|
+
# First insert all items that have headings
|
|
1015
|
+
for (level, heading_text), todos in by_heading.items():
|
|
1016
|
+
target_lines = _insert_todos_under_heading(
|
|
1017
|
+
target_lines, level, heading_text, todos
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
# Then append all items without headings at the end, like before
|
|
1021
|
+
if plain_items:
|
|
1022
|
+
if target_lines and target_lines[-1].strip():
|
|
1023
|
+
target_lines.append("") # one blank line before the "unsectioned" todos
|
|
1024
|
+
target_lines.extend(plain_items)
|
|
1025
|
+
|
|
1026
|
+
new_target_text = "\n".join(target_lines)
|
|
1027
|
+
if not new_target_text.endswith("\n"):
|
|
1028
|
+
new_target_text += "\n"
|
|
1029
|
+
|
|
1030
|
+
# Save the updated target date and load it into the editor
|
|
1031
|
+
self.db.save_new_version(
|
|
1032
|
+
target_iso,
|
|
1033
|
+
new_target_text,
|
|
1034
|
+
strings._("unchecked_checkbox_items_moved_to_next_day"),
|
|
1035
|
+
)
|
|
1036
|
+
self._load_selected_date(target_iso)
|
|
923
1037
|
return True
|
|
924
1038
|
|
|
925
1039
|
def _on_date_changed(self):
|
|
@@ -1222,6 +1336,11 @@ class MainWindow(QMainWindow):
|
|
|
1222
1336
|
# Turned off -> cancel any running timer and remove the widget
|
|
1223
1337
|
self.pomodoro_manager.cancel_timer()
|
|
1224
1338
|
|
|
1339
|
+
def _send_reminder_webhook(self, text: str):
|
|
1340
|
+
if self.cfg.reminders and self.cfg.reminders_webhook_url:
|
|
1341
|
+
reminder_webhook = ReminderWebHook(text)
|
|
1342
|
+
reminder_webhook._send()
|
|
1343
|
+
|
|
1225
1344
|
def _show_flashing_reminder(self, text: str):
|
|
1226
1345
|
"""
|
|
1227
1346
|
Show a small flashing dialog and request attention from the OS.
|
|
@@ -1450,6 +1569,12 @@ class MainWindow(QMainWindow):
|
|
|
1450
1569
|
self.cfg.tags = getattr(new_cfg, "tags", self.cfg.tags)
|
|
1451
1570
|
self.cfg.time_log = getattr(new_cfg, "time_log", self.cfg.time_log)
|
|
1452
1571
|
self.cfg.reminders = getattr(new_cfg, "reminders", self.cfg.reminders)
|
|
1572
|
+
self.cfg.reminders_webhook_url = getattr(
|
|
1573
|
+
new_cfg, "reminders_webhook_url", self.cfg.reminders_webhook_url
|
|
1574
|
+
)
|
|
1575
|
+
self.cfg.reminders_webhook_secret = getattr(
|
|
1576
|
+
new_cfg, "reminders_webhook_secret", self.cfg.reminders_webhook_secret
|
|
1577
|
+
)
|
|
1453
1578
|
self.cfg.documents = getattr(new_cfg, "documents", self.cfg.documents)
|
|
1454
1579
|
self.cfg.invoicing = getattr(new_cfg, "invoicing", self.cfg.invoicing)
|
|
1455
1580
|
self.cfg.locale = getattr(new_cfg, "locale", self.cfg.locale)
|
|
@@ -1317,15 +1317,43 @@ class MarkdownEditor(QTextEdit):
|
|
|
1317
1317
|
if icon:
|
|
1318
1318
|
# absolute document position of the icon
|
|
1319
1319
|
doc_pos = block.position() + i
|
|
1320
|
-
|
|
1320
|
+
r_icon = char_rect_at(doc_pos, icon)
|
|
1321
1321
|
|
|
1322
|
-
#
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1322
|
+
# --- Find where the first non-space "real text" starts ---
|
|
1323
|
+
first_idx = i + len(icon) + 1 # skip icon + trailing space
|
|
1324
|
+
while first_idx < len(text) and text[first_idx].isspace():
|
|
1325
|
+
first_idx += 1
|
|
1326
|
+
|
|
1327
|
+
# Start with some padding around the icon itself
|
|
1328
|
+
left_pad = r_icon.width() // 2
|
|
1329
|
+
right_pad = r_icon.width() // 2
|
|
1330
|
+
|
|
1331
|
+
hit_left = r_icon.left() - left_pad
|
|
1332
|
+
|
|
1333
|
+
# If there's actual text after the checkbox, clamp the
|
|
1334
|
+
# clickable area so it stops *before* the first letter.
|
|
1335
|
+
if first_idx < len(text):
|
|
1336
|
+
first_doc_pos = block.position() + first_idx
|
|
1337
|
+
c_first = QTextCursor(self.document())
|
|
1338
|
+
c_first.setPosition(first_doc_pos)
|
|
1339
|
+
first_x = self.cursorRect(c_first).x()
|
|
1340
|
+
|
|
1341
|
+
expanded_right = r_icon.right() + right_pad
|
|
1342
|
+
hit_right = min(expanded_right, first_x)
|
|
1343
|
+
else:
|
|
1344
|
+
# No text after the checkbox on this line
|
|
1345
|
+
hit_right = r_icon.right() + right_pad
|
|
1346
|
+
|
|
1347
|
+
# Make sure the rect is at least 1px wide
|
|
1348
|
+
if hit_right <= hit_left:
|
|
1349
|
+
hit_right = r_icon.right()
|
|
1350
|
+
|
|
1351
|
+
hit_rect = QRect(
|
|
1352
|
+
hit_left,
|
|
1353
|
+
r_icon.top(),
|
|
1354
|
+
max(1, hit_right - hit_left),
|
|
1355
|
+
r_icon.height(),
|
|
1356
|
+
)
|
|
1329
1357
|
|
|
1330
1358
|
if hit_rect.contains(pt):
|
|
1331
1359
|
# Build the replacement: swap ☐ <-> ☑ (keep trailing space)
|
|
@@ -1339,7 +1367,9 @@ class MarkdownEditor(QTextEdit):
|
|
|
1339
1367
|
edit.setPosition(doc_pos)
|
|
1340
1368
|
# icon + space
|
|
1341
1369
|
edit.movePosition(
|
|
1342
|
-
QTextCursor.Right,
|
|
1370
|
+
QTextCursor.Right,
|
|
1371
|
+
QTextCursor.KeepAnchor,
|
|
1372
|
+
len(icon) + 1,
|
|
1343
1373
|
)
|
|
1344
1374
|
edit.insertText(f"{new_icon} ")
|
|
1345
1375
|
edit.endEditBlock()
|