bouquin 0.1.10__py3-none-any.whl → 0.2.1.2__py3-none-any.whl
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/db.py +34 -91
- bouquin/find_bar.py +208 -0
- bouquin/history_dialog.py +29 -28
- bouquin/key_prompt.py +6 -0
- bouquin/lock_overlay.py +8 -2
- bouquin/main_window.py +598 -119
- bouquin/markdown_editor.py +813 -0
- bouquin/save_dialog.py +3 -0
- bouquin/search.py +46 -31
- bouquin/settings_dialog.py +1 -1
- bouquin/theme.py +1 -2
- bouquin/toolbar.py +4 -41
- {bouquin-0.1.10.dist-info → bouquin-0.2.1.2.dist-info}/METADATA +10 -7
- bouquin-0.2.1.2.dist-info/RECORD +21 -0
- bouquin/editor.py +0 -897
- bouquin-0.1.10.dist-info/RECORD +0 -20
- {bouquin-0.1.10.dist-info → bouquin-0.2.1.2.dist-info}/LICENSE +0 -0
- {bouquin-0.1.10.dist-info → bouquin-0.2.1.2.dist-info}/WHEEL +0 -0
- {bouquin-0.1.10.dist-info → bouquin-0.2.1.2.dist-info}/entry_points.txt +0 -0
bouquin/db.py
CHANGED
|
@@ -3,10 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
import csv
|
|
4
4
|
import html
|
|
5
5
|
import json
|
|
6
|
-
import os
|
|
7
6
|
|
|
8
7
|
from dataclasses import dataclass
|
|
9
|
-
from markdownify import markdownify as md
|
|
10
8
|
from pathlib import Path
|
|
11
9
|
from sqlcipher3 import dbapi2 as sqlite
|
|
12
10
|
from typing import List, Sequence, Tuple
|
|
@@ -257,68 +255,31 @@ class DBManager:
|
|
|
257
255
|
).fetchall()
|
|
258
256
|
return [dict(r) for r in rows]
|
|
259
257
|
|
|
260
|
-
def get_version(
|
|
261
|
-
self,
|
|
262
|
-
*,
|
|
263
|
-
date_iso: str | None = None,
|
|
264
|
-
version_no: int | None = None,
|
|
265
|
-
version_id: int | None = None,
|
|
266
|
-
) -> dict | None:
|
|
258
|
+
def get_version(self, *, version_id: int) -> dict | None:
|
|
267
259
|
"""
|
|
268
|
-
Fetch a specific version by
|
|
260
|
+
Fetch a specific version by version_id.
|
|
269
261
|
Returns a dict with keys: id, date, version_no, created_at, note, content.
|
|
270
262
|
"""
|
|
271
263
|
cur = self.conn.cursor()
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
).fetchone()
|
|
278
|
-
else:
|
|
279
|
-
if date_iso is None or version_no is None:
|
|
280
|
-
raise ValueError(
|
|
281
|
-
"Provide either version_id OR (date_iso and version_no)"
|
|
282
|
-
)
|
|
283
|
-
row = cur.execute(
|
|
284
|
-
"SELECT id, date, version_no, created_at, note, content "
|
|
285
|
-
"FROM versions WHERE date=? AND version_no=?;",
|
|
286
|
-
(date_iso, version_no),
|
|
287
|
-
).fetchone()
|
|
264
|
+
row = cur.execute(
|
|
265
|
+
"SELECT id, date, version_no, created_at, note, content "
|
|
266
|
+
"FROM versions WHERE id=?;",
|
|
267
|
+
(version_id,),
|
|
268
|
+
).fetchone()
|
|
288
269
|
return dict(row) if row else None
|
|
289
270
|
|
|
290
|
-
def revert_to_version(
|
|
291
|
-
self,
|
|
292
|
-
date_iso: str,
|
|
293
|
-
*,
|
|
294
|
-
version_no: int | None = None,
|
|
295
|
-
version_id: int | None = None,
|
|
296
|
-
) -> None:
|
|
271
|
+
def revert_to_version(self, date_iso: str, version_id: int) -> None:
|
|
297
272
|
"""
|
|
298
273
|
Point the page head (pages.current_version_id) to an existing version.
|
|
299
|
-
Fast revert: no content is rewritten.
|
|
300
274
|
"""
|
|
301
|
-
if self.conn is None:
|
|
302
|
-
raise RuntimeError("Database is not connected")
|
|
303
275
|
cur = self.conn.cursor()
|
|
304
276
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
).fetchone()
|
|
312
|
-
if row is None:
|
|
313
|
-
raise ValueError("Version not found for this date")
|
|
314
|
-
version_id = int(row["id"])
|
|
315
|
-
else:
|
|
316
|
-
# Ensure that version_id belongs to the given date
|
|
317
|
-
row = cur.execute(
|
|
318
|
-
"SELECT date FROM versions WHERE id=?;", (version_id,)
|
|
319
|
-
).fetchone()
|
|
320
|
-
if row is None or row["date"] != date_iso:
|
|
321
|
-
raise ValueError("version_id does not belong to the given date")
|
|
277
|
+
# Ensure that version_id belongs to the given date
|
|
278
|
+
row = cur.execute(
|
|
279
|
+
"SELECT date FROM versions WHERE id=?;", (version_id,)
|
|
280
|
+
).fetchone()
|
|
281
|
+
if row is None or row["date"] != date_iso:
|
|
282
|
+
raise ValueError("version_id does not belong to the given date")
|
|
322
283
|
|
|
323
284
|
with self.conn:
|
|
324
285
|
cur.execute(
|
|
@@ -342,20 +303,18 @@ class DBManager:
|
|
|
342
303
|
).fetchall()
|
|
343
304
|
return [(r[0], r[1]) for r in rows]
|
|
344
305
|
|
|
345
|
-
def export_json(
|
|
346
|
-
self, entries: Sequence[Entry], file_path: str, pretty: bool = True
|
|
347
|
-
) -> None:
|
|
306
|
+
def export_json(self, entries: Sequence[Entry], file_path: str) -> None:
|
|
348
307
|
"""
|
|
349
308
|
Export to json.
|
|
350
309
|
"""
|
|
351
310
|
data = [{"date": d, "content": c} for d, c in entries]
|
|
352
311
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
353
|
-
|
|
354
|
-
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
355
|
-
else:
|
|
356
|
-
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
|
|
312
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
357
313
|
|
|
358
314
|
def export_csv(self, entries: Sequence[Entry], file_path: str) -> None:
|
|
315
|
+
"""
|
|
316
|
+
Export pages to CSV.
|
|
317
|
+
"""
|
|
359
318
|
# utf-8-sig adds a BOM so Excel opens as UTF-8 by default.
|
|
360
319
|
with open(file_path, "w", encoding="utf-8-sig", newline="") as f:
|
|
361
320
|
writer = csv.writer(f)
|
|
@@ -369,6 +328,10 @@ class DBManager:
|
|
|
369
328
|
separator: str = "\n\n— — — — —\n\n",
|
|
370
329
|
strip_html: bool = True,
|
|
371
330
|
) -> None:
|
|
331
|
+
"""
|
|
332
|
+
Strip the HTML from the latest version of the pages
|
|
333
|
+
and save to a text file.
|
|
334
|
+
"""
|
|
372
335
|
import re, html as _html
|
|
373
336
|
|
|
374
337
|
# Precompiled patterns
|
|
@@ -407,6 +370,9 @@ class DBManager:
|
|
|
407
370
|
def export_html(
|
|
408
371
|
self, entries: Sequence[Entry], file_path: str, title: str = "Bouquin export"
|
|
409
372
|
) -> None:
|
|
373
|
+
"""
|
|
374
|
+
Export to HTML with a heading.
|
|
375
|
+
"""
|
|
410
376
|
parts = [
|
|
411
377
|
"<!doctype html>",
|
|
412
378
|
'<html lang="en">',
|
|
@@ -429,25 +395,17 @@ class DBManager:
|
|
|
429
395
|
def export_markdown(
|
|
430
396
|
self, entries: Sequence[Entry], file_path: str, title: str = "Bouquin export"
|
|
431
397
|
) -> None:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
]
|
|
398
|
+
"""
|
|
399
|
+
Export to HTML, similar to export_html, but then convert to Markdown
|
|
400
|
+
using markdownify, and finally save to file.
|
|
401
|
+
"""
|
|
402
|
+
parts = []
|
|
438
403
|
for d, c in entries:
|
|
439
|
-
parts.append(
|
|
440
|
-
|
|
441
|
-
)
|
|
442
|
-
parts.append("</body></html>")
|
|
443
|
-
|
|
444
|
-
# Convert html to markdown
|
|
445
|
-
md_items = []
|
|
446
|
-
for item in parts:
|
|
447
|
-
md_items.append(md(item, heading_style="ATX"))
|
|
404
|
+
parts.append(f"# {d}")
|
|
405
|
+
parts.append(c)
|
|
448
406
|
|
|
449
407
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
450
|
-
f.write("\n".join(
|
|
408
|
+
f.write("\n".join(parts))
|
|
451
409
|
|
|
452
410
|
def export_sql(self, file_path: str) -> None:
|
|
453
411
|
"""
|
|
@@ -468,21 +426,6 @@ class DBManager:
|
|
|
468
426
|
cur.execute("SELECT sqlcipher_export('backup')")
|
|
469
427
|
cur.execute("DETACH DATABASE backup")
|
|
470
428
|
|
|
471
|
-
def export_by_extension(self, file_path: str) -> None:
|
|
472
|
-
entries = self.get_all_entries()
|
|
473
|
-
ext = os.path.splitext(file_path)[1].lower()
|
|
474
|
-
|
|
475
|
-
if ext == ".json":
|
|
476
|
-
self.export_json(entries, file_path)
|
|
477
|
-
elif ext == ".csv":
|
|
478
|
-
self.export_csv(entries, file_path)
|
|
479
|
-
elif ext == ".txt":
|
|
480
|
-
self.export_txt(entries, file_path)
|
|
481
|
-
elif ext in {".html", ".htm"}:
|
|
482
|
-
self.export_html(entries, file_path)
|
|
483
|
-
else:
|
|
484
|
-
raise ValueError(f"Unsupported extension: {ext}")
|
|
485
|
-
|
|
486
429
|
def compact(self) -> None:
|
|
487
430
|
"""
|
|
488
431
|
Runs VACUUM on the db.
|
bouquin/find_bar.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from PySide6.QtCore import Qt, Signal
|
|
4
|
+
from PySide6.QtGui import (
|
|
5
|
+
QShortcut,
|
|
6
|
+
QTextCursor,
|
|
7
|
+
QTextCharFormat,
|
|
8
|
+
QTextDocument,
|
|
9
|
+
)
|
|
10
|
+
from PySide6.QtWidgets import (
|
|
11
|
+
QWidget,
|
|
12
|
+
QHBoxLayout,
|
|
13
|
+
QLineEdit,
|
|
14
|
+
QLabel,
|
|
15
|
+
QPushButton,
|
|
16
|
+
QCheckBox,
|
|
17
|
+
QTextEdit,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FindBar(QWidget):
|
|
22
|
+
"""Widget for finding text in the Editor"""
|
|
23
|
+
|
|
24
|
+
# emitted when the bar is hidden (Esc/✕), so caller can refocus editor
|
|
25
|
+
closed = Signal()
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
editor: QTextEdit,
|
|
30
|
+
shortcut_parent: QWidget | None = None,
|
|
31
|
+
parent: QWidget | None = None,
|
|
32
|
+
):
|
|
33
|
+
|
|
34
|
+
super().__init__(parent)
|
|
35
|
+
|
|
36
|
+
# store how to get the current editor
|
|
37
|
+
self._editor_getter = editor if callable(editor) else (lambda: editor)
|
|
38
|
+
self.shortcut_parent = shortcut_parent
|
|
39
|
+
|
|
40
|
+
# UI (build ONCE)
|
|
41
|
+
layout = QHBoxLayout(self)
|
|
42
|
+
layout.setContentsMargins(6, 0, 6, 0)
|
|
43
|
+
|
|
44
|
+
layout.addWidget(QLabel("Find:"))
|
|
45
|
+
|
|
46
|
+
self.edit = QLineEdit(self)
|
|
47
|
+
self.edit.setPlaceholderText("Type to search")
|
|
48
|
+
layout.addWidget(self.edit)
|
|
49
|
+
|
|
50
|
+
self.case = QCheckBox("Match case", self)
|
|
51
|
+
layout.addWidget(self.case)
|
|
52
|
+
|
|
53
|
+
self.prevBtn = QPushButton("Prev", self)
|
|
54
|
+
self.nextBtn = QPushButton("Next", self)
|
|
55
|
+
self.closeBtn = QPushButton("✕", self)
|
|
56
|
+
self.closeBtn.setFlat(True)
|
|
57
|
+
layout.addWidget(self.prevBtn)
|
|
58
|
+
layout.addWidget(self.nextBtn)
|
|
59
|
+
layout.addWidget(self.closeBtn)
|
|
60
|
+
|
|
61
|
+
self.setVisible(False)
|
|
62
|
+
|
|
63
|
+
# Shortcut (press Esc to hide bar)
|
|
64
|
+
sp = (
|
|
65
|
+
self.shortcut_parent
|
|
66
|
+
if self.shortcut_parent is not None
|
|
67
|
+
else (self.parent() or self)
|
|
68
|
+
)
|
|
69
|
+
QShortcut(Qt.Key_Escape, sp, activated=self._maybe_hide)
|
|
70
|
+
|
|
71
|
+
# Signals (connect ONCE)
|
|
72
|
+
self.edit.returnPressed.connect(self.find_next)
|
|
73
|
+
self.edit.textChanged.connect(self._update_highlight)
|
|
74
|
+
self.case.toggled.connect(self._update_highlight)
|
|
75
|
+
self.nextBtn.clicked.connect(self.find_next)
|
|
76
|
+
self.prevBtn.clicked.connect(self.find_prev)
|
|
77
|
+
self.closeBtn.clicked.connect(self.hide_bar)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def editor(self) -> QTextEdit | None:
|
|
81
|
+
"""Get the current editor"""
|
|
82
|
+
return self._editor_getter()
|
|
83
|
+
|
|
84
|
+
# ----- Public API -----
|
|
85
|
+
|
|
86
|
+
def show_bar(self):
|
|
87
|
+
"""Show the bar, seed with current selection if sensible, focus the line edit."""
|
|
88
|
+
if not self.editor:
|
|
89
|
+
return
|
|
90
|
+
tc = self.editor.textCursor()
|
|
91
|
+
sel = tc.selectedText().strip()
|
|
92
|
+
if sel and "\u2029" not in sel: # ignore multi-paragraph selections
|
|
93
|
+
self.edit.setText(sel)
|
|
94
|
+
self.setVisible(True)
|
|
95
|
+
self.edit.setFocus(Qt.ShortcutFocusReason)
|
|
96
|
+
self.edit.selectAll()
|
|
97
|
+
self._update_highlight()
|
|
98
|
+
|
|
99
|
+
def hide_bar(self):
|
|
100
|
+
self.setVisible(False)
|
|
101
|
+
self._clear_highlight()
|
|
102
|
+
self.closed.emit()
|
|
103
|
+
|
|
104
|
+
def refresh(self):
|
|
105
|
+
"""Recompute highlights"""
|
|
106
|
+
self._update_highlight()
|
|
107
|
+
|
|
108
|
+
# ----- Internals -----
|
|
109
|
+
|
|
110
|
+
def _maybe_hide(self):
|
|
111
|
+
if self.isVisible():
|
|
112
|
+
self.hide_bar()
|
|
113
|
+
|
|
114
|
+
def _flags(self, backward: bool = False) -> QTextDocument.FindFlags:
|
|
115
|
+
flags = QTextDocument.FindFlags()
|
|
116
|
+
if backward:
|
|
117
|
+
flags |= QTextDocument.FindBackward
|
|
118
|
+
if self.case.isChecked():
|
|
119
|
+
flags |= QTextDocument.FindCaseSensitively
|
|
120
|
+
return flags
|
|
121
|
+
|
|
122
|
+
def find_next(self):
|
|
123
|
+
if not self.editor:
|
|
124
|
+
return
|
|
125
|
+
txt = self.edit.text()
|
|
126
|
+
if not txt:
|
|
127
|
+
return
|
|
128
|
+
# If current selection == query, bump caret to the end so we don't re-match it.
|
|
129
|
+
c = self.editor.textCursor()
|
|
130
|
+
if c.hasSelection():
|
|
131
|
+
sel = c.selectedText()
|
|
132
|
+
same = (
|
|
133
|
+
(sel == txt)
|
|
134
|
+
if self.case.isChecked()
|
|
135
|
+
else (sel.casefold() == txt.casefold())
|
|
136
|
+
)
|
|
137
|
+
if same:
|
|
138
|
+
end = max(c.position(), c.anchor())
|
|
139
|
+
c.setPosition(end, QTextCursor.MoveAnchor)
|
|
140
|
+
self.editor.setTextCursor(c)
|
|
141
|
+
if not self.editor.find(txt, self._flags(False)):
|
|
142
|
+
cur = self.editor.textCursor()
|
|
143
|
+
cur.movePosition(QTextCursor.Start)
|
|
144
|
+
self.editor.setTextCursor(cur)
|
|
145
|
+
self.editor.find(txt, self._flags(False))
|
|
146
|
+
self.editor.ensureCursorVisible()
|
|
147
|
+
self._update_highlight()
|
|
148
|
+
|
|
149
|
+
def find_prev(self):
|
|
150
|
+
if not self.editor:
|
|
151
|
+
return
|
|
152
|
+
txt = self.edit.text()
|
|
153
|
+
if not txt:
|
|
154
|
+
return
|
|
155
|
+
# If current selection == query, bump caret to the start so we don't re-match it.
|
|
156
|
+
c = self.editor.textCursor()
|
|
157
|
+
if c.hasSelection():
|
|
158
|
+
sel = c.selectedText()
|
|
159
|
+
same = (
|
|
160
|
+
(sel == txt)
|
|
161
|
+
if self.case.isChecked()
|
|
162
|
+
else (sel.casefold() == txt.casefold())
|
|
163
|
+
)
|
|
164
|
+
if same:
|
|
165
|
+
start = min(c.position(), c.anchor())
|
|
166
|
+
c.setPosition(start, QTextCursor.MoveAnchor)
|
|
167
|
+
self.editor.setTextCursor(c)
|
|
168
|
+
if not self.editor.find(txt, self._flags(True)):
|
|
169
|
+
cur = self.editor.textCursor()
|
|
170
|
+
cur.movePosition(QTextCursor.End)
|
|
171
|
+
self.editor.setTextCursor(cur)
|
|
172
|
+
self.editor.find(txt, self._flags(True))
|
|
173
|
+
self.editor.ensureCursorVisible()
|
|
174
|
+
self._update_highlight()
|
|
175
|
+
|
|
176
|
+
def _update_highlight(self):
|
|
177
|
+
if not self.editor:
|
|
178
|
+
return
|
|
179
|
+
txt = self.edit.text()
|
|
180
|
+
if not txt:
|
|
181
|
+
self._clear_highlight()
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
doc = self.editor.document()
|
|
185
|
+
flags = self._flags(False)
|
|
186
|
+
cur = QTextCursor(doc)
|
|
187
|
+
cur.movePosition(QTextCursor.Start)
|
|
188
|
+
|
|
189
|
+
fmt = QTextCharFormat()
|
|
190
|
+
hl = self.palette().highlight().color()
|
|
191
|
+
hl.setAlpha(90)
|
|
192
|
+
fmt.setBackground(hl)
|
|
193
|
+
|
|
194
|
+
selections = []
|
|
195
|
+
while True:
|
|
196
|
+
cur = doc.find(txt, cur, flags)
|
|
197
|
+
if cur.isNull():
|
|
198
|
+
break
|
|
199
|
+
sel = QTextEdit.ExtraSelection()
|
|
200
|
+
sel.cursor = cur
|
|
201
|
+
sel.format = fmt
|
|
202
|
+
selections.append(sel)
|
|
203
|
+
|
|
204
|
+
self.editor.setExtraSelections(selections)
|
|
205
|
+
|
|
206
|
+
def _clear_highlight(self):
|
|
207
|
+
if self.editor:
|
|
208
|
+
self.editor.setExtraSelections([])
|
bouquin/history_dialog.py
CHANGED
|
@@ -16,31 +16,33 @@ from PySide6.QtWidgets import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
s =
|
|
30
|
-
|
|
31
|
-
s =
|
|
32
|
-
|
|
33
|
-
s =
|
|
34
|
-
|
|
35
|
-
s =
|
|
36
|
-
s =
|
|
19
|
+
def _markdown_to_text(s: str) -> str:
|
|
20
|
+
"""Convert markdown to plain text for diff comparison."""
|
|
21
|
+
# Remove images
|
|
22
|
+
s = re.sub(r"!\[.*?\]\(.*?\)", "[ Image ]", s)
|
|
23
|
+
# Remove inline code formatting
|
|
24
|
+
s = re.sub(r"`([^`]+)`", r"\1", s)
|
|
25
|
+
# Remove bold/italic markers
|
|
26
|
+
s = re.sub(r"\*\*([^*]+)\*\*", r"\1", s)
|
|
27
|
+
s = re.sub(r"__([^_]+)__", r"\1", s)
|
|
28
|
+
s = re.sub(r"\*([^*]+)\*", r"\1", s)
|
|
29
|
+
s = re.sub(r"_([^_]+)_", r"\1", s)
|
|
30
|
+
# Remove strikethrough
|
|
31
|
+
s = re.sub(r"~~([^~]+)~~", r"\1", s)
|
|
32
|
+
# Remove heading markers
|
|
33
|
+
s = re.sub(r"^#{1,6}\s+", "", s, flags=re.MULTILINE)
|
|
34
|
+
# Remove list markers
|
|
35
|
+
s = re.sub(r"^\s*[-*+]\s+", "", s, flags=re.MULTILINE)
|
|
36
|
+
s = re.sub(r"^\s*\d+\.\s+", "", s, flags=re.MULTILINE)
|
|
37
|
+
# Remove checkbox markers
|
|
38
|
+
s = re.sub(r"^\s*-\s*\[[x ☐☑]\]\s+", "", s, flags=re.MULTILINE)
|
|
37
39
|
return s.strip()
|
|
38
40
|
|
|
39
41
|
|
|
40
|
-
def _colored_unified_diff_html(
|
|
42
|
+
def _colored_unified_diff_html(old_md: str, new_md: str) -> str:
|
|
41
43
|
"""Return HTML with colored unified diff (+ green, - red, context gray)."""
|
|
42
|
-
a =
|
|
43
|
-
b =
|
|
44
|
+
a = _markdown_to_text(old_md).splitlines()
|
|
45
|
+
b = _markdown_to_text(new_md).splitlines()
|
|
44
46
|
ud = difflib.unified_diff(a, b, fromfile="current", tofile="selected", lineterm="")
|
|
45
47
|
lines = []
|
|
46
48
|
for line in ud:
|
|
@@ -116,9 +118,9 @@ class HistoryDialog(QDialog):
|
|
|
116
118
|
return local.strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
117
119
|
|
|
118
120
|
def _load_versions(self):
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
# [{id,version_no,created_at,note,is_current}]
|
|
122
|
+
self._versions = self._db.list_versions(self._date)
|
|
123
|
+
|
|
122
124
|
self._current_id = next(
|
|
123
125
|
(v["id"] for v in self._versions if v["is_current"]), None
|
|
124
126
|
)
|
|
@@ -150,13 +152,12 @@ class HistoryDialog(QDialog):
|
|
|
150
152
|
self.btn_revert.setEnabled(False)
|
|
151
153
|
return
|
|
152
154
|
sel_id = item.data(Qt.UserRole)
|
|
153
|
-
# Preview selected as HTML
|
|
154
155
|
sel = self._db.get_version(version_id=sel_id)
|
|
155
|
-
self.preview.
|
|
156
|
+
self.preview.setMarkdown(sel["content"])
|
|
156
157
|
# Diff vs current (textual diff)
|
|
157
158
|
cur = self._db.get_version(version_id=self._current_id)
|
|
158
159
|
self.diff.setHtml(_colored_unified_diff_html(cur["content"], sel["content"]))
|
|
159
|
-
# Enable revert only if selecting a non-current
|
|
160
|
+
# Enable revert only if selecting a non-current version
|
|
160
161
|
self.btn_revert.setEnabled(sel_id != self._current_id)
|
|
161
162
|
|
|
162
163
|
@Slot()
|
|
@@ -167,7 +168,7 @@ class HistoryDialog(QDialog):
|
|
|
167
168
|
sel_id = item.data(Qt.UserRole)
|
|
168
169
|
if sel_id == self._current_id:
|
|
169
170
|
return
|
|
170
|
-
# Flip head pointer
|
|
171
|
+
# Flip head pointer to the older version
|
|
171
172
|
try:
|
|
172
173
|
self._db.revert_to_version(self._date, version_id=sel_id)
|
|
173
174
|
except Exception as e:
|
bouquin/key_prompt.py
CHANGED
|
@@ -17,6 +17,12 @@ class KeyPrompt(QDialog):
|
|
|
17
17
|
title: str = "Enter key",
|
|
18
18
|
message: str = "Enter key",
|
|
19
19
|
):
|
|
20
|
+
"""
|
|
21
|
+
Prompt the user for the key required to decrypt the database.
|
|
22
|
+
|
|
23
|
+
Used when opening the app, unlocking the idle locked screen,
|
|
24
|
+
or when rekeying.
|
|
25
|
+
"""
|
|
20
26
|
super().__init__(parent)
|
|
21
27
|
self.setWindowTitle(title)
|
|
22
28
|
v = QVBoxLayout(self)
|
bouquin/lock_overlay.py
CHANGED
|
@@ -7,6 +7,9 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton
|
|
|
7
7
|
|
|
8
8
|
class LockOverlay(QWidget):
|
|
9
9
|
def __init__(self, parent: QWidget, on_unlock: callable):
|
|
10
|
+
"""
|
|
11
|
+
Widget that 'locks' the screen after a configured idle time.
|
|
12
|
+
"""
|
|
10
13
|
super().__init__(parent)
|
|
11
14
|
self.setObjectName("LockOverlay")
|
|
12
15
|
self.setAttribute(Qt.WA_StyledBackground, True)
|
|
@@ -39,6 +42,9 @@ class LockOverlay(QWidget):
|
|
|
39
42
|
self.hide()
|
|
40
43
|
|
|
41
44
|
def _is_dark(self, pal: QPalette) -> bool:
|
|
45
|
+
"""
|
|
46
|
+
Detect if dark mode is in use.
|
|
47
|
+
"""
|
|
42
48
|
c = pal.color(QPalette.Window)
|
|
43
49
|
luma = 0.2126 * c.redF() + 0.7152 * c.greenF() + 0.0722 * c.blueF()
|
|
44
50
|
return luma < 0.5
|
|
@@ -58,7 +64,7 @@ class LockOverlay(QWidget):
|
|
|
58
64
|
|
|
59
65
|
self.setStyleSheet(
|
|
60
66
|
f"""
|
|
61
|
-
#LockOverlay {{ background-color: rgb(0,0,0); }}
|
|
67
|
+
#LockOverlay {{ background-color: rgb(0,0,0); }}
|
|
62
68
|
#LockOverlay QLabel#lockLabel {{ color: {accent_hex}; font-weight: 600; }}
|
|
63
69
|
|
|
64
70
|
#LockOverlay QPushButton#unlockButton {{
|
|
@@ -113,7 +119,7 @@ class LockOverlay(QWidget):
|
|
|
113
119
|
|
|
114
120
|
def changeEvent(self, ev):
|
|
115
121
|
super().changeEvent(ev)
|
|
116
|
-
# Only re-style on palette flips
|
|
122
|
+
# Only re-style on palette flips (user changed theme)
|
|
117
123
|
if ev.type() in (QEvent.PaletteChange, QEvent.ApplicationPaletteChange):
|
|
118
124
|
self._apply_overlay_style()
|
|
119
125
|
|