bouquin 0.4.3__tar.gz → 0.4.4__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.4.3 → bouquin-0.4.4}/PKG-INFO +6 -6
- {bouquin-0.4.3 → bouquin-0.4.4}/README.md +5 -5
- bouquin-0.4.4/bouquin/keys/mig5.asc +109 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/locales/en.json +15 -1
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/locales/fr.json +1 -1
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/locales/it.json +1 -1
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/main_window.py +112 -41
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/markdown_editor.py +1 -13
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/settings_dialog.py +1 -1
- bouquin-0.4.4/bouquin/version_check.py +386 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/pyproject.toml +2 -2
- {bouquin-0.4.3 → bouquin-0.4.4}/LICENSE +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/__init__.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/__main__.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/bug_report_dialog.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/db.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/find_bar.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/flow_layout.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/fonts/NotoSansSymbols2-Regular.ttf +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/fonts/OFL.txt +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/history_dialog.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/icons/bouquin-light.svg +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/icons/bouquin.svg +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/key_prompt.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/lock_overlay.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/main.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/markdown_highlighter.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/save_dialog.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/search.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/settings.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/statistics_dialog.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/strings.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/tag_browser.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/tags_widget.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/theme.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/time_log.py +0 -0
- {bouquin-0.4.3 → bouquin-0.4.4}/bouquin/toolbar.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bouquin
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher.
|
|
5
5
|
Home-page: https://git.mig5.net/mig5/bouquin
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -39,7 +39,7 @@ To increase security, the SQLCipher key is requested when the app is opened, and
|
|
|
39
39
|
to disk unless the user configures it to be in the settings.
|
|
40
40
|
|
|
41
41
|
There is deliberately no network connectivity or syncing intended, other than the option to send a bug
|
|
42
|
-
report from within the app.
|
|
42
|
+
report from within the app, or optionally to check for new versions to upgrade to.
|
|
43
43
|
|
|
44
44
|
## Screenshots
|
|
45
45
|
|
|
@@ -84,15 +84,15 @@ report from within the app.
|
|
|
84
84
|
* Backup the database to encrypted SQLCipher format (which can then be loaded back in to a Bouquin)
|
|
85
85
|
* Dark and light theme support
|
|
86
86
|
* Automatically generate checkboxes when typing 'TODO'
|
|
87
|
-
* It is possible to automatically move unchecked checkboxes from
|
|
87
|
+
* It is possible to automatically move unchecked checkboxes from the last 7 days to the next weekday.
|
|
88
88
|
* English, French and Italian locales provided
|
|
89
|
-
* Ability to set reminder alarms in the app against the current line of text
|
|
90
|
-
* Ability to log time per day and run timesheet reports
|
|
89
|
+
* Ability to set reminder alarms in the app against the current line of text (which will be flashed as the reminder)
|
|
90
|
+
* Ability to log time per day for different projects/activities and run timesheet reports
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
## How to install
|
|
94
94
|
|
|
95
|
-
Make sure you have `libxcb-cursor0` installed (
|
|
95
|
+
Make sure you have `libxcb-cursor0` installed (on Debian-based distributions) or `xcb-util-cursor` (RedHat/Fedora-based distributions).
|
|
96
96
|
|
|
97
97
|
It's also recommended that you have Noto Sans fonts installed, but it's up to you. It just can impact the display of unicode symbols such as checkboxes.
|
|
98
98
|
|
|
@@ -19,7 +19,7 @@ To increase security, the SQLCipher key is requested when the app is opened, and
|
|
|
19
19
|
to disk unless the user configures it to be in the settings.
|
|
20
20
|
|
|
21
21
|
There is deliberately no network connectivity or syncing intended, other than the option to send a bug
|
|
22
|
-
report from within the app.
|
|
22
|
+
report from within the app, or optionally to check for new versions to upgrade to.
|
|
23
23
|
|
|
24
24
|
## Screenshots
|
|
25
25
|
|
|
@@ -64,15 +64,15 @@ report from within the app.
|
|
|
64
64
|
* Backup the database to encrypted SQLCipher format (which can then be loaded back in to a Bouquin)
|
|
65
65
|
* Dark and light theme support
|
|
66
66
|
* Automatically generate checkboxes when typing 'TODO'
|
|
67
|
-
* It is possible to automatically move unchecked checkboxes from
|
|
67
|
+
* It is possible to automatically move unchecked checkboxes from the last 7 days to the next weekday.
|
|
68
68
|
* English, French and Italian locales provided
|
|
69
|
-
* Ability to set reminder alarms in the app against the current line of text
|
|
70
|
-
* Ability to log time per day and run timesheet reports
|
|
69
|
+
* Ability to set reminder alarms in the app against the current line of text (which will be flashed as the reminder)
|
|
70
|
+
* Ability to log time per day for different projects/activities and run timesheet reports
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
## How to install
|
|
74
74
|
|
|
75
|
-
Make sure you have `libxcb-cursor0` installed (
|
|
75
|
+
Make sure you have `libxcb-cursor0` installed (on Debian-based distributions) or `xcb-util-cursor` (RedHat/Fedora-based distributions).
|
|
76
76
|
|
|
77
77
|
It's also recommended that you have Noto Sans fonts installed, but it's up to you. It just can impact the display of unicode symbols such as checkboxes.
|
|
78
78
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
2
|
+
|
|
3
|
+
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
|
|
108
|
+
=IfFs
|
|
109
|
+
-----END PGP PUBLIC KEY BLOCK-----
|
|
@@ -57,6 +57,20 @@
|
|
|
57
57
|
"couldnt_open": "Couldn't open",
|
|
58
58
|
"report_a_bug": "Report a bug",
|
|
59
59
|
"version": "Version",
|
|
60
|
+
"update": "Update",
|
|
61
|
+
"check_for_updates": "Check for updates",
|
|
62
|
+
"could_not_check_for_updates": "Could not check for updates:\n",
|
|
63
|
+
"update_server_returned_an_empty_version_string": "Update server returned an empty version string",
|
|
64
|
+
"you_are_running_the_latest_version": "You are running the latest version:\n",
|
|
65
|
+
"there_is_a_new_version_available": "There is a new version available:\n",
|
|
66
|
+
"download_the_appimage": "Download the AppImage?",
|
|
67
|
+
"downloading": "Downloading",
|
|
68
|
+
"download_cancelled": "Download cancelled",
|
|
69
|
+
"failed_to_download_update": "Failed to download update:\n",
|
|
70
|
+
"could_not_read_bundled_gpg_public_key": "Could not read bundled GPG public key:\n",
|
|
71
|
+
"could_not_find_gpg_executable": "Could not find the 'gpg' executable to verify the download.",
|
|
72
|
+
"gpg_key_verification_failed": "GPG signature verification failed. The downloaded files have been deleted.\n\n",
|
|
73
|
+
"downloaded_and_verified_new_appimage": "Downloaded and verified new AppImage:\n\n",
|
|
60
74
|
"navigate": "Navigate",
|
|
61
75
|
"current": "current",
|
|
62
76
|
"selected": "selected",
|
|
@@ -82,7 +96,7 @@
|
|
|
82
96
|
"open_in_new_tab": "Open in new tab",
|
|
83
97
|
"autosave": "autosave",
|
|
84
98
|
"unchecked_checkbox_items_moved_to_next_day": "Unchecked checkbox items moved to next day",
|
|
85
|
-
"
|
|
99
|
+
"move_unchecked_todos_to_today_on_startup": "Automatically move unchecked TODOs from the last 7 days to next weekday",
|
|
86
100
|
"insert_images": "Insert images",
|
|
87
101
|
"images": "Images",
|
|
88
102
|
"reopen_failed": "Re-open failed",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"open_in_new_tab": "Ouvrir dans un nouvel onglet",
|
|
82
82
|
"autosave": "enregistrement automatique",
|
|
83
83
|
"unchecked_checkbox_items_moved_to_next_day": "Les cases non cochées ont été reportées au jour suivant",
|
|
84
|
-
"
|
|
84
|
+
"move_unchecked_todos_to_today_on_startup": "Déplacer automatiquement les TODO non cochés au jour suivant",
|
|
85
85
|
"insert_images": "Insérer des images",
|
|
86
86
|
"images": "Images",
|
|
87
87
|
"reopen_failed": "Échec de la réouverture",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"open_in_new_tab": "Apri in una nuova scheda",
|
|
82
82
|
"autosave": "salvataggio automatico",
|
|
83
83
|
"unchecked_checkbox_items_moved_to_next_day": "Le caselle non spuntate sono state spostate al giorno successivo",
|
|
84
|
-
"
|
|
84
|
+
"move_unchecked_todos_to_today_on_startup": "Sposta i TODO non completati a oggi all'avvio",
|
|
85
85
|
"insert_images": "Inserisci immagini",
|
|
86
86
|
"images": "Immagini",
|
|
87
87
|
"reopen_failed": "Riapertura fallita",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import datetime
|
|
4
|
-
import importlib.metadata
|
|
5
4
|
import os
|
|
6
5
|
import sys
|
|
7
6
|
import re
|
|
@@ -68,6 +67,7 @@ from .tags_widget import PageTagsWidget
|
|
|
68
67
|
from .theme import ThemeManager
|
|
69
68
|
from .time_log import TimeLogWidget
|
|
70
69
|
from .toolbar import ToolBar
|
|
70
|
+
from .version_check import VersionChecker
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
class MainWindow(QMainWindow):
|
|
@@ -77,6 +77,7 @@ class MainWindow(QMainWindow):
|
|
|
77
77
|
self.setMinimumSize(1000, 650)
|
|
78
78
|
|
|
79
79
|
self.themes = themes # Store the themes manager
|
|
80
|
+
self.version_checker = VersionChecker(self)
|
|
80
81
|
|
|
81
82
|
self.cfg = load_db_config()
|
|
82
83
|
if not os.path.exists(self.cfg.path):
|
|
@@ -310,7 +311,7 @@ class MainWindow(QMainWindow):
|
|
|
310
311
|
self._reminder_timers: list[QTimer] = []
|
|
311
312
|
|
|
312
313
|
# First load + mark dates in calendar with content
|
|
313
|
-
if not self.
|
|
314
|
+
if not self._load_unchecked_todos():
|
|
314
315
|
self._load_selected_date()
|
|
315
316
|
self._refresh_calendar_marks()
|
|
316
317
|
|
|
@@ -333,6 +334,12 @@ class MainWindow(QMainWindow):
|
|
|
333
334
|
# Build any alarms for *today* from stored markdown
|
|
334
335
|
self._rebuild_reminders_for_today()
|
|
335
336
|
|
|
337
|
+
# Rollover unchecked todos automatically when the calendar day changes
|
|
338
|
+
self._day_change_timer = QTimer(self)
|
|
339
|
+
self._day_change_timer.setSingleShot(True)
|
|
340
|
+
self._day_change_timer.timeout.connect(self._on_day_changed)
|
|
341
|
+
self._schedule_next_day_change()
|
|
342
|
+
|
|
336
343
|
@property
|
|
337
344
|
def editor(self) -> MarkdownEditor | None:
|
|
338
345
|
"""Get the currently active editor."""
|
|
@@ -783,46 +790,112 @@ class MainWindow(QMainWindow):
|
|
|
783
790
|
today = QDate.currentDate()
|
|
784
791
|
self._create_new_tab(today)
|
|
785
792
|
|
|
786
|
-
def
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
text = self.db.get_entry(yesterday_str)
|
|
791
|
-
unchecked_items = []
|
|
792
|
-
|
|
793
|
-
# Split into lines and find unchecked checkbox items
|
|
794
|
-
lines = text.split("\n")
|
|
795
|
-
remaining_lines = []
|
|
796
|
-
|
|
797
|
-
for line in lines:
|
|
798
|
-
# Check for unchecked markdown checkboxes: - [ ] or - [☐]
|
|
799
|
-
if re.match(r"^\s*-\s*\[\s*\]\s+", line) or re.match(
|
|
800
|
-
r"^\s*-\s*\[☐\]\s+", line
|
|
801
|
-
):
|
|
802
|
-
# Extract the text after the checkbox
|
|
803
|
-
item_text = re.sub(r"^\s*-\s*\[[\s☐]\]\s+", "", line)
|
|
804
|
-
unchecked_items.append(f"- [ ] {item_text}")
|
|
805
|
-
else:
|
|
806
|
-
# Keep all other lines
|
|
807
|
-
remaining_lines.append(line)
|
|
808
|
-
|
|
809
|
-
# Save modified content back if we moved items
|
|
810
|
-
if unchecked_items:
|
|
811
|
-
modified_text = "\n".join(remaining_lines)
|
|
812
|
-
self.db.save_new_version(
|
|
813
|
-
yesterday_str,
|
|
814
|
-
modified_text,
|
|
815
|
-
strings._("unchecked_checkbox_items_moved_to_next_day"),
|
|
816
|
-
)
|
|
793
|
+
def _rollover_target_date(self, day: QDate) -> QDate:
|
|
794
|
+
"""
|
|
795
|
+
Given a 'new day' (system date), return the date we should move
|
|
796
|
+
unfinished todos *to*.
|
|
817
797
|
|
|
818
|
-
|
|
819
|
-
|
|
798
|
+
If the new day is Saturday or Sunday, we skip ahead to the next Monday.
|
|
799
|
+
Otherwise we just return the same day.
|
|
800
|
+
"""
|
|
801
|
+
# Qt: Monday=1 ... Sunday=7
|
|
802
|
+
dow = day.dayOfWeek()
|
|
803
|
+
if dow >= 6: # Saturday (6) or Sunday (7)
|
|
804
|
+
return day.addDays(8 - dow) # 6 -> +2, 7 -> +1 (next Monday)
|
|
805
|
+
return day
|
|
820
806
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
807
|
+
def _schedule_next_day_change(self) -> None:
|
|
808
|
+
"""
|
|
809
|
+
Schedule a one-shot timer to fire shortly after the next midnight.
|
|
810
|
+
"""
|
|
811
|
+
now = QDateTime.currentDateTime()
|
|
812
|
+
tomorrow = now.date().addDays(1)
|
|
813
|
+
# A couple of minutes after midnight to be safe
|
|
814
|
+
next_run = QDateTime(tomorrow, QTime(0, 2))
|
|
815
|
+
msecs = max(60_000, now.msecsTo(next_run)) # at least 1 minute
|
|
816
|
+
self._day_change_timer.start(msecs)
|
|
817
|
+
|
|
818
|
+
@Slot()
|
|
819
|
+
def _on_day_changed(self) -> None:
|
|
820
|
+
"""
|
|
821
|
+
Called when we've crossed into a new calendar day (according to the timer).
|
|
822
|
+
Re-runs the rollover logic and refreshes the UI.
|
|
823
|
+
"""
|
|
824
|
+
# Make the calendar show the *real* new day first
|
|
825
|
+
today = QDate.currentDate()
|
|
826
|
+
with QSignalBlocker(self.calendar):
|
|
827
|
+
self.calendar.setSelectedDate(today)
|
|
828
|
+
|
|
829
|
+
# Same logic as on startup
|
|
830
|
+
if not self._load_unchecked_todos():
|
|
831
|
+
self._load_selected_date()
|
|
832
|
+
|
|
833
|
+
self._refresh_calendar_marks()
|
|
834
|
+
self._rebuild_reminders_for_today()
|
|
835
|
+
self._schedule_next_day_change()
|
|
836
|
+
|
|
837
|
+
def _load_unchecked_todos(self, days_back: int = 7) -> bool:
|
|
838
|
+
"""
|
|
839
|
+
Move unchecked checkbox items from the last `days_back` days
|
|
840
|
+
into the rollover target date (today, or next Monday if today
|
|
841
|
+
is a weekend).
|
|
842
|
+
|
|
843
|
+
Returns True if any items were moved, False otherwise.
|
|
844
|
+
"""
|
|
845
|
+
if not getattr(self.cfg, "move_todos", False):
|
|
846
|
+
return False
|
|
847
|
+
|
|
848
|
+
if not getattr(self, "db", None):
|
|
824
849
|
return False
|
|
825
850
|
|
|
851
|
+
today = QDate.currentDate()
|
|
852
|
+
target_date = self._rollover_target_date(today)
|
|
853
|
+
target_iso = target_date.toString("yyyy-MM-dd")
|
|
854
|
+
|
|
855
|
+
all_unchecked: list[str] = []
|
|
856
|
+
any_moved = False
|
|
857
|
+
|
|
858
|
+
# Look back N days (yesterday = 1, up to `days_back`)
|
|
859
|
+
for delta in range(1, days_back + 1):
|
|
860
|
+
src_date = today.addDays(-delta)
|
|
861
|
+
src_iso = src_date.toString("yyyy-MM-dd")
|
|
862
|
+
text = self.db.get_entry(src_iso)
|
|
863
|
+
if not text:
|
|
864
|
+
continue
|
|
865
|
+
|
|
866
|
+
lines = text.split("\n")
|
|
867
|
+
remaining_lines: list[str] = []
|
|
868
|
+
moved_from_this_day = False
|
|
869
|
+
|
|
870
|
+
for line in lines:
|
|
871
|
+
# Unchecked markdown checkboxes: "- [ ] " or "- [☐] "
|
|
872
|
+
if re.match(r"^\s*-\s*\[\s*\]\s+", line) or re.match(
|
|
873
|
+
r"^\s*-\s*\[☐\]\s+", line
|
|
874
|
+
):
|
|
875
|
+
item_text = re.sub(r"^\s*-\s*\[[\s☐]\]\s+", "", line)
|
|
876
|
+
all_unchecked.append(f"- [ ] {item_text}")
|
|
877
|
+
moved_from_this_day = True
|
|
878
|
+
any_moved = True
|
|
879
|
+
else:
|
|
880
|
+
remaining_lines.append(line)
|
|
881
|
+
|
|
882
|
+
if moved_from_this_day:
|
|
883
|
+
modified_text = "\n".join(remaining_lines)
|
|
884
|
+
# Save the cleaned-up source day
|
|
885
|
+
self.db.save_new_version(
|
|
886
|
+
src_iso,
|
|
887
|
+
modified_text,
|
|
888
|
+
strings._("unchecked_checkbox_items_moved_to_next_day"),
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
if not any_moved:
|
|
892
|
+
return False
|
|
893
|
+
|
|
894
|
+
# Append everything we collected to the *target* date
|
|
895
|
+
unchecked_str = "\n".join(all_unchecked) + "\n"
|
|
896
|
+
self._load_selected_date(target_iso, unchecked_str)
|
|
897
|
+
return True
|
|
898
|
+
|
|
826
899
|
def _on_date_changed(self):
|
|
827
900
|
"""
|
|
828
901
|
When the calendar selection changes, save the previous day's note if dirty,
|
|
@@ -1562,9 +1635,7 @@ class MainWindow(QMainWindow):
|
|
|
1562
1635
|
dlg.exec()
|
|
1563
1636
|
|
|
1564
1637
|
def _open_version(self):
|
|
1565
|
-
|
|
1566
|
-
version_formatted = f"{APP_NAME} {version}"
|
|
1567
|
-
QMessageBox.information(self, strings._("version"), version_formatted)
|
|
1638
|
+
self.version_checker.show_version_dialog()
|
|
1568
1639
|
|
|
1569
1640
|
# ----------------- Idle handlers ----------------- #
|
|
1570
1641
|
def _apply_idle_minutes(self, minutes: int):
|
|
@@ -551,6 +551,7 @@ class MarkdownEditor(QTextEdit):
|
|
|
551
551
|
c.setPosition(new_pos)
|
|
552
552
|
self.setTextCursor(c)
|
|
553
553
|
return
|
|
554
|
+
|
|
554
555
|
# Step out of a code block with Down at EOF
|
|
555
556
|
if event.key() == Qt.Key.Key_Down:
|
|
556
557
|
c = self.textCursor()
|
|
@@ -758,19 +759,6 @@ class MarkdownEditor(QTextEdit):
|
|
|
758
759
|
super().keyPressEvent(event)
|
|
759
760
|
return
|
|
760
761
|
|
|
761
|
-
# Auto-insert an extra blank line after headings (#, ##, ###)
|
|
762
|
-
# when pressing Enter at the end of the line.
|
|
763
|
-
if re.match(r"^#{1,3}\s+", stripped) and pos_in_block >= len(line_text):
|
|
764
|
-
cursor.beginEditBlock()
|
|
765
|
-
# First blank line: visual separator between heading and body
|
|
766
|
-
cursor.insertBlock()
|
|
767
|
-
# Second blank line: where body text will start (caret ends here)
|
|
768
|
-
cursor.insertBlock()
|
|
769
|
-
cursor.endEditBlock()
|
|
770
|
-
|
|
771
|
-
self.setTextCursor(cursor)
|
|
772
|
-
return
|
|
773
|
-
|
|
774
762
|
# Check for list continuation
|
|
775
763
|
list_type, prefix = self._detect_list_type(current_line)
|
|
776
764
|
|
|
@@ -160,7 +160,7 @@ class SettingsDialog(QDialog):
|
|
|
160
160
|
features_layout = QVBoxLayout(features_group)
|
|
161
161
|
|
|
162
162
|
self.move_todos = QCheckBox(
|
|
163
|
-
strings._("
|
|
163
|
+
strings._("move_unchecked_todos_to_today_on_startup")
|
|
164
164
|
)
|
|
165
165
|
self.move_todos.setChecked(self.current_settings.move_todos)
|
|
166
166
|
self.move_todos.setCursor(Qt.PointingHandCursor)
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import subprocess # nosec
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
from importlib.resources import files
|
|
12
|
+
from PySide6.QtCore import QStandardPaths, Qt
|
|
13
|
+
from PySide6.QtWidgets import (
|
|
14
|
+
QApplication,
|
|
15
|
+
QMessageBox,
|
|
16
|
+
QWidget,
|
|
17
|
+
QProgressDialog,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .settings import APP_NAME
|
|
21
|
+
from . import strings
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Where to fetch the latest version string from
|
|
25
|
+
VERSION_URL = "https://mig5.net/bouquin/version.txt"
|
|
26
|
+
|
|
27
|
+
# Name of the installed distribution according to pyproject.toml
|
|
28
|
+
# (used with importlib.metadata.version)
|
|
29
|
+
DIST_NAME = "bouquin"
|
|
30
|
+
|
|
31
|
+
# Base URL where AppImages are hosted
|
|
32
|
+
APPIMAGE_BASE_URL = "https://git.mig5.net/mig5/bouquin/releases/download"
|
|
33
|
+
|
|
34
|
+
# Where we expect to find the bundled public key, relative to the *installed* package.
|
|
35
|
+
GPG_PUBKEY_RESOURCE = ("bouquin", "keys", "mig5.asc")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class VersionChecker:
|
|
39
|
+
"""
|
|
40
|
+
Handles:
|
|
41
|
+
* showing the version dialog
|
|
42
|
+
* checking for updates
|
|
43
|
+
* downloading & verifying a new AppImage
|
|
44
|
+
|
|
45
|
+
All dialogs use `parent` as their parent widget.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, parent: QWidget | None = None):
|
|
49
|
+
self._parent = parent
|
|
50
|
+
|
|
51
|
+
# ---------- Version helpers ---------- #
|
|
52
|
+
|
|
53
|
+
def current_version(self) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Return the current app version as reported by importlib.metadata
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
return importlib.metadata.version(DIST_NAME)
|
|
59
|
+
except importlib.metadata.PackageNotFoundError:
|
|
60
|
+
# Fallback for editable installs / dev trees
|
|
61
|
+
return "0.0.0"
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _parse_version(v: str) -> tuple[int, ...]:
|
|
65
|
+
"""
|
|
66
|
+
Very small helper to compare simple semantic versions like 1.2.3.
|
|
67
|
+
Extracts numeric components and returns them as a tuple.
|
|
68
|
+
"""
|
|
69
|
+
parts = re.findall(r"\d+", v)
|
|
70
|
+
if not parts:
|
|
71
|
+
return (0,)
|
|
72
|
+
return tuple(int(p) for p in parts)
|
|
73
|
+
|
|
74
|
+
def _is_newer_version(self, available: str, current: str) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
True if `available` > `current` according to _parse_version.
|
|
77
|
+
"""
|
|
78
|
+
return self._parse_version(available) > self._parse_version(current)
|
|
79
|
+
|
|
80
|
+
# ---------- Public entrypoint for Help → Version ---------- #
|
|
81
|
+
|
|
82
|
+
def show_version_dialog(self) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Show the Version dialog with a 'Check for updates' button.
|
|
85
|
+
"""
|
|
86
|
+
version = self.current_version()
|
|
87
|
+
version_formatted = f"{APP_NAME} {version}"
|
|
88
|
+
|
|
89
|
+
box = QMessageBox(self._parent)
|
|
90
|
+
box.setIcon(QMessageBox.Information)
|
|
91
|
+
box.setWindowTitle(strings._("version"))
|
|
92
|
+
box.setText(version_formatted)
|
|
93
|
+
|
|
94
|
+
check_button = box.addButton(
|
|
95
|
+
strings._("check_for_updates"), QMessageBox.ActionRole
|
|
96
|
+
)
|
|
97
|
+
box.addButton(QMessageBox.Close)
|
|
98
|
+
|
|
99
|
+
box.exec()
|
|
100
|
+
|
|
101
|
+
if box.clickedButton() is check_button:
|
|
102
|
+
self.check_for_updates()
|
|
103
|
+
|
|
104
|
+
# ---------- Core update logic ---------- #
|
|
105
|
+
|
|
106
|
+
def check_for_updates(self) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Fetch VERSION_URL, compare against the current version, and optionally
|
|
109
|
+
download + verify a new AppImage.
|
|
110
|
+
"""
|
|
111
|
+
current = self.current_version()
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
resp = requests.get(VERSION_URL, timeout=10)
|
|
115
|
+
resp.raise_for_status()
|
|
116
|
+
available_raw = resp.text.strip()
|
|
117
|
+
except Exception as e:
|
|
118
|
+
QMessageBox.warning(
|
|
119
|
+
self._parent,
|
|
120
|
+
strings._("update"),
|
|
121
|
+
strings._("could_not_check_for_updates") + e,
|
|
122
|
+
)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
if not available_raw:
|
|
126
|
+
QMessageBox.warning(
|
|
127
|
+
self._parent,
|
|
128
|
+
strings._("update"),
|
|
129
|
+
strings._("update_server_returned_an_empty_version_string"),
|
|
130
|
+
)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
if not self._is_newer_version(available_raw, current):
|
|
134
|
+
QMessageBox.information(
|
|
135
|
+
self._parent,
|
|
136
|
+
strings._("update"),
|
|
137
|
+
strings._("you_are_running_the_latest_version") + f"({current}).",
|
|
138
|
+
)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
# Newer version is available
|
|
142
|
+
reply = QMessageBox.question(
|
|
143
|
+
self._parent,
|
|
144
|
+
strings._("update"),
|
|
145
|
+
(
|
|
146
|
+
strings._("there_is_a_new_version_available")
|
|
147
|
+
+ available_raw
|
|
148
|
+
+ "\n\n"
|
|
149
|
+
+ strings._("download_the_appimage")
|
|
150
|
+
),
|
|
151
|
+
QMessageBox.Yes | QMessageBox.No,
|
|
152
|
+
)
|
|
153
|
+
if reply != QMessageBox.Yes:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
self._download_and_verify_appimage(available_raw)
|
|
157
|
+
|
|
158
|
+
# ---------- Download + verification helpers ---------- #
|
|
159
|
+
def _download_file(
|
|
160
|
+
self,
|
|
161
|
+
url: str,
|
|
162
|
+
dest_path: Path,
|
|
163
|
+
timeout: int = 30,
|
|
164
|
+
progress: QProgressDialog | None = None,
|
|
165
|
+
label: str | None = None,
|
|
166
|
+
) -> None:
|
|
167
|
+
"""
|
|
168
|
+
Stream a URL to a local file, optionally updating a QProgressDialog.
|
|
169
|
+
If the user cancels via the dialog, raises RuntimeError.
|
|
170
|
+
"""
|
|
171
|
+
resp = requests.get(url, timeout=timeout, stream=True)
|
|
172
|
+
resp.raise_for_status()
|
|
173
|
+
|
|
174
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
|
|
176
|
+
total_bytes: int | None = None
|
|
177
|
+
content_length = resp.headers.get("Content-Length")
|
|
178
|
+
if content_length is not None:
|
|
179
|
+
try:
|
|
180
|
+
total_bytes = int(content_length)
|
|
181
|
+
except ValueError:
|
|
182
|
+
total_bytes = None
|
|
183
|
+
|
|
184
|
+
if progress is not None:
|
|
185
|
+
progress.setLabelText(
|
|
186
|
+
label or strings._("downloading") + f" {dest_path.name}..."
|
|
187
|
+
)
|
|
188
|
+
# Unknown size → busy indicator; known size → real range
|
|
189
|
+
if total_bytes is not None and total_bytes > 0:
|
|
190
|
+
progress.setRange(0, total_bytes)
|
|
191
|
+
else:
|
|
192
|
+
progress.setRange(0, 0) # indeterminate
|
|
193
|
+
progress.setValue(0)
|
|
194
|
+
progress.show()
|
|
195
|
+
QApplication.processEvents()
|
|
196
|
+
|
|
197
|
+
downloaded = 0
|
|
198
|
+
with dest_path.open("wb") as f:
|
|
199
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
200
|
+
if not chunk:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
f.write(chunk)
|
|
204
|
+
downloaded += len(chunk)
|
|
205
|
+
|
|
206
|
+
if progress is not None:
|
|
207
|
+
if total_bytes is not None and total_bytes > 0:
|
|
208
|
+
progress.setValue(downloaded)
|
|
209
|
+
else:
|
|
210
|
+
# Just bump a little so the dialog looks alive
|
|
211
|
+
progress.setValue(progress.value() + 1)
|
|
212
|
+
QApplication.processEvents()
|
|
213
|
+
|
|
214
|
+
if progress.wasCanceled():
|
|
215
|
+
raise RuntimeError(strings._("download_cancelled"))
|
|
216
|
+
|
|
217
|
+
if progress is not None and total_bytes is not None and total_bytes > 0:
|
|
218
|
+
progress.setValue(total_bytes)
|
|
219
|
+
QApplication.processEvents()
|
|
220
|
+
|
|
221
|
+
def _download_and_verify_appimage(self, version: str) -> None:
|
|
222
|
+
"""
|
|
223
|
+
Download the AppImage + its GPG signature to the user's Downloads dir,
|
|
224
|
+
then verify it with a bundled public key.
|
|
225
|
+
"""
|
|
226
|
+
# Where to put the file
|
|
227
|
+
download_dir = QStandardPaths.writableLocation(QStandardPaths.DownloadLocation)
|
|
228
|
+
if not download_dir:
|
|
229
|
+
download_dir = os.path.expanduser("~/Downloads")
|
|
230
|
+
download_dir = Path(download_dir)
|
|
231
|
+
download_dir.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
|
|
233
|
+
# Construct AppImage filename and URLs
|
|
234
|
+
appimage_path = download_dir / "Bouquin.AppImage"
|
|
235
|
+
sig_path = Path(str(appimage_path) + ".asc")
|
|
236
|
+
|
|
237
|
+
appimage_url = f"{APPIMAGE_BASE_URL}/{version}/Bouquin.AppImage"
|
|
238
|
+
sig_url = f"{appimage_url}.asc"
|
|
239
|
+
|
|
240
|
+
# Progress dialog covering both downloads
|
|
241
|
+
progress = QProgressDialog(
|
|
242
|
+
"Downloading update...",
|
|
243
|
+
"Cancel",
|
|
244
|
+
0,
|
|
245
|
+
100,
|
|
246
|
+
self._parent,
|
|
247
|
+
)
|
|
248
|
+
progress.setWindowTitle(strings._("update"))
|
|
249
|
+
progress.setWindowModality(Qt.WindowModal)
|
|
250
|
+
progress.setAutoClose(False)
|
|
251
|
+
progress.setAutoReset(False)
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
# AppImage download
|
|
255
|
+
self._download_file(
|
|
256
|
+
appimage_url,
|
|
257
|
+
appimage_path,
|
|
258
|
+
progress=progress,
|
|
259
|
+
label=strings._("downloading") + " Bouquin.AppImage...",
|
|
260
|
+
)
|
|
261
|
+
# Signature download (usually tiny, but we still show it)
|
|
262
|
+
self._download_file(
|
|
263
|
+
sig_url,
|
|
264
|
+
sig_path,
|
|
265
|
+
progress=progress,
|
|
266
|
+
label=strings._("downloading") + " signature...",
|
|
267
|
+
)
|
|
268
|
+
except RuntimeError:
|
|
269
|
+
# User cancelled
|
|
270
|
+
for p in (appimage_path, sig_path):
|
|
271
|
+
try:
|
|
272
|
+
if p.exists():
|
|
273
|
+
p.unlink()
|
|
274
|
+
except OSError:
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
progress.close()
|
|
278
|
+
QMessageBox.information(
|
|
279
|
+
self._parent,
|
|
280
|
+
strings._("update"),
|
|
281
|
+
strings._("download_cancelled"),
|
|
282
|
+
)
|
|
283
|
+
return
|
|
284
|
+
except Exception as e:
|
|
285
|
+
# Other error
|
|
286
|
+
for p in (appimage_path, sig_path):
|
|
287
|
+
try:
|
|
288
|
+
if p.exists():
|
|
289
|
+
p.unlink()
|
|
290
|
+
except OSError:
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
progress.close()
|
|
294
|
+
QMessageBox.critical(
|
|
295
|
+
self._parent,
|
|
296
|
+
strings._("update"),
|
|
297
|
+
strings._("failed_to_download_update") + e,
|
|
298
|
+
)
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
progress.close()
|
|
302
|
+
|
|
303
|
+
# Load the bundled public key
|
|
304
|
+
try:
|
|
305
|
+
pkg, *rel = GPG_PUBKEY_RESOURCE
|
|
306
|
+
pubkey_bytes = (files(pkg) / "/".join(rel)).read_bytes()
|
|
307
|
+
except Exception as e:
|
|
308
|
+
QMessageBox.critical(
|
|
309
|
+
self._parent,
|
|
310
|
+
strings._("update"),
|
|
311
|
+
strings._("could_not_read_bundled_gpg_public_key") + e,
|
|
312
|
+
)
|
|
313
|
+
# On failure, delete the downloaded files for safety
|
|
314
|
+
for p in (appimage_path, sig_path):
|
|
315
|
+
try:
|
|
316
|
+
if p.exists():
|
|
317
|
+
p.unlink()
|
|
318
|
+
except OSError:
|
|
319
|
+
pass
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
# Use a temporary GNUPGHOME so we don't touch the user's main keyring
|
|
323
|
+
try:
|
|
324
|
+
with tempfile.TemporaryDirectory() as gnupg_home:
|
|
325
|
+
pubkey_path = Path(gnupg_home) / "pubkey.asc"
|
|
326
|
+
pubkey_path.write_bytes(pubkey_bytes)
|
|
327
|
+
|
|
328
|
+
# Import the key
|
|
329
|
+
subprocess.run(
|
|
330
|
+
["gpg", "--homedir", gnupg_home, "--import", str(pubkey_path)],
|
|
331
|
+
check=True,
|
|
332
|
+
stdout=subprocess.DEVNULL,
|
|
333
|
+
stderr=subprocess.PIPE,
|
|
334
|
+
) # nosec
|
|
335
|
+
|
|
336
|
+
# Verify the signature
|
|
337
|
+
subprocess.run(
|
|
338
|
+
[
|
|
339
|
+
"gpg",
|
|
340
|
+
"--homedir",
|
|
341
|
+
gnupg_home,
|
|
342
|
+
"--verify",
|
|
343
|
+
str(sig_path),
|
|
344
|
+
str(appimage_path),
|
|
345
|
+
],
|
|
346
|
+
check=True,
|
|
347
|
+
stdout=subprocess.DEVNULL,
|
|
348
|
+
stderr=subprocess.PIPE,
|
|
349
|
+
) # nosec
|
|
350
|
+
except FileNotFoundError:
|
|
351
|
+
# gpg not installed / not on PATH
|
|
352
|
+
for p in (appimage_path, sig_path):
|
|
353
|
+
try:
|
|
354
|
+
if p.exists():
|
|
355
|
+
p.unlink()
|
|
356
|
+
except OSError:
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
QMessageBox.critical(
|
|
360
|
+
self._parent,
|
|
361
|
+
strings._("update"),
|
|
362
|
+
strings._("could_not_find_gpg_executable"),
|
|
363
|
+
)
|
|
364
|
+
return
|
|
365
|
+
except subprocess.CalledProcessError as e:
|
|
366
|
+
for p in (appimage_path, sig_path):
|
|
367
|
+
try:
|
|
368
|
+
if p.exists():
|
|
369
|
+
p.unlink()
|
|
370
|
+
except OSError:
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
QMessageBox.critical(
|
|
374
|
+
self._parent,
|
|
375
|
+
strings._("update"),
|
|
376
|
+
strings._("gpg_signature_verification_failed")
|
|
377
|
+
+ e.stderr.decode(errors="ignore"),
|
|
378
|
+
)
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
# Success
|
|
382
|
+
QMessageBox.information(
|
|
383
|
+
self._parent,
|
|
384
|
+
strings._("update"),
|
|
385
|
+
strings._("downloaded_and_verified_new_appimage") + appimage_path,
|
|
386
|
+
)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "bouquin"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.4"
|
|
4
4
|
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
|
5
5
|
authors = ["Miguel Jacq <mig@mig5.net>"]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "GPL-3.0-or-later"
|
|
8
8
|
repository = "https://git.mig5.net/mig5/bouquin"
|
|
9
9
|
packages = [{ include = "bouquin" }]
|
|
10
|
-
include = ["bouquin/locales/*.json"]
|
|
10
|
+
include = ["bouquin/locales/*.json", "bouquin/keys/mig5.asc", "bouquin/fonts/NotoSansSymbols2-Regular.ttf", "bouquin/fonts/OFL.txt"]
|
|
11
11
|
|
|
12
12
|
[tool.poetry.dependencies]
|
|
13
13
|
python = ">=3.10,<3.14"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|