Compare commits
4 commits
3e6a08231c
...
cc9453997e
| Author | SHA1 | Date | |
|---|---|---|---|
| cc9453997e | |||
| 3db384e7e4 | |||
| f778afd268 | |||
| 0caf0efeef |
6 changed files with 74 additions and 6 deletions
9
CHANGELOG.md
Normal file
9
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# 0.1.1
|
||||||
|
|
||||||
|
* Add ability to change the key
|
||||||
|
* Add ability to jump to today's date
|
||||||
|
* Add shortcut for Settings (Ctrl+E) so as not to collide with Ctrl+S (Save)
|
||||||
|
|
||||||
|
# 0.1.0
|
||||||
|
|
||||||
|
* Initial release.
|
||||||
|
|
@ -31,7 +31,6 @@ There is deliberately no network connectivity or syncing intended.
|
||||||
|
|
||||||
* Search
|
* Search
|
||||||
* Taxonomy/tagging
|
* Taxonomy/tagging
|
||||||
* Ability to change the SQLCipher key
|
|
||||||
* Export to other formats (plaintext, json, sql etc)
|
* Export to other formats (plaintext, json, sql etc)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,25 @@ class DBManager:
|
||||||
cur.execute("PRAGMA user_version = 1;")
|
cur.execute("PRAGMA user_version = 1;")
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
|
def rekey(self, new_key: str) -> None:
|
||||||
|
"""
|
||||||
|
Change the SQLCipher passphrase in-place, then reopen the connection
|
||||||
|
with the new key to verify.
|
||||||
|
"""
|
||||||
|
if self.conn is None:
|
||||||
|
raise RuntimeError("Database is not connected")
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
# Change the encryption key of the currently open database
|
||||||
|
cur.execute(f"PRAGMA rekey = '{new_key}';")
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
# Close and reopen with the new key to verify and restore PRAGMAs
|
||||||
|
self.conn.close()
|
||||||
|
self.conn = None
|
||||||
|
self.cfg.key = new_key
|
||||||
|
if not self.connect():
|
||||||
|
raise sqlite.Error("Re-open failed after rekey")
|
||||||
|
|
||||||
def get_entry(self, date_iso: str) -> str:
|
def get_entry(self, date_iso: str) -> str:
|
||||||
cur = self.conn.cursor()
|
cur = self.conn.cursor()
|
||||||
cur.execute("SELECT content FROM entries WHERE date = ?;", (date_iso,))
|
cur.execute("SELECT content FROM entries WHERE date = ?;", (date_iso,))
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,8 @@ class MainWindow(QMainWindow):
|
||||||
act_save.setShortcut("Ctrl+S")
|
act_save.setShortcut("Ctrl+S")
|
||||||
act_save.triggered.connect(lambda: self._save_current(explicit=True))
|
act_save.triggered.connect(lambda: self._save_current(explicit=True))
|
||||||
file_menu.addAction(act_save)
|
file_menu.addAction(act_save)
|
||||||
act_settings = QAction("&Settings", self)
|
act_settings = QAction("S&ettings", self)
|
||||||
|
act_save.setShortcut("Ctrl+E")
|
||||||
act_settings.triggered.connect(self._open_settings)
|
act_settings.triggered.connect(self._open_settings)
|
||||||
file_menu.addAction(act_settings)
|
file_menu.addAction(act_settings)
|
||||||
file_menu.addSeparator()
|
file_menu.addSeparator()
|
||||||
|
|
@ -97,6 +98,13 @@ class MainWindow(QMainWindow):
|
||||||
nav_menu.addAction(act_next)
|
nav_menu.addAction(act_next)
|
||||||
self.addAction(act_next)
|
self.addAction(act_next)
|
||||||
|
|
||||||
|
act_today = QAction("Today", self)
|
||||||
|
act_today.setShortcut("Ctrl+T")
|
||||||
|
act_today.setShortcutContext(Qt.ApplicationShortcut)
|
||||||
|
act_today.triggered.connect(self._adjust_today)
|
||||||
|
nav_menu.addAction(act_today)
|
||||||
|
self.addAction(act_today)
|
||||||
|
|
||||||
# Autosave
|
# Autosave
|
||||||
self._dirty = False
|
self._dirty = False
|
||||||
self._save_timer = QTimer(self)
|
self._save_timer = QTimer(self)
|
||||||
|
|
@ -176,6 +184,11 @@ class MainWindow(QMainWindow):
|
||||||
d = self.calendar.selectedDate().addDays(delta)
|
d = self.calendar.selectedDate().addDays(delta)
|
||||||
self.calendar.setSelectedDate(d)
|
self.calendar.setSelectedDate(d)
|
||||||
|
|
||||||
|
def _adjust_today(self):
|
||||||
|
"""Jump to today."""
|
||||||
|
today = QDate.currentDate()
|
||||||
|
self.calendar.setSelectedDate(today)
|
||||||
|
|
||||||
def _on_date_changed(self):
|
def _on_date_changed(self):
|
||||||
"""
|
"""
|
||||||
When the calendar selection changes, save the previous day's note if dirty,
|
When the calendar selection changes, save the previous day's note if dirty,
|
||||||
|
|
@ -219,7 +232,7 @@ class MainWindow(QMainWindow):
|
||||||
self._save_date(self._current_date_iso(), explicit)
|
self._save_date(self._current_date_iso(), explicit)
|
||||||
|
|
||||||
def _open_settings(self):
|
def _open_settings(self):
|
||||||
dlg = SettingsDialog(self.cfg, self)
|
dlg = SettingsDialog(self.cfg, self.db, self)
|
||||||
if dlg.exec() == QDialog.Accepted:
|
if dlg.exec() == QDialog.Accepted:
|
||||||
new_cfg = dlg.config
|
new_cfg = dlg.config
|
||||||
if new_cfg.path != self.cfg.path:
|
if new_cfg.path != self.cfg.path:
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,20 @@ from PySide6.QtWidgets import (
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
QDialogButtonBox,
|
QDialogButtonBox,
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
|
QMessageBox,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .db import DBConfig
|
from .db import DBConfig, DBManager
|
||||||
from .settings import save_db_config
|
from .settings import save_db_config
|
||||||
|
from .key_prompt import KeyPrompt
|
||||||
|
|
||||||
|
|
||||||
class SettingsDialog(QDialog):
|
class SettingsDialog(QDialog):
|
||||||
def __init__(self, cfg: DBConfig, parent=None):
|
def __init__(self, cfg: DBConfig, db: DBManager, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Settings")
|
self.setWindowTitle("Settings")
|
||||||
self._cfg = DBConfig(path=cfg.path, key="")
|
self._cfg = DBConfig(path=cfg.path, key="")
|
||||||
|
self._db = db
|
||||||
|
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
|
form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
|
||||||
|
|
@ -44,12 +47,17 @@ class SettingsDialog(QDialog):
|
||||||
h.setStretch(1, 0)
|
h.setStretch(1, 0)
|
||||||
form.addRow("Database path", path_row)
|
form.addRow("Database path", path_row)
|
||||||
|
|
||||||
|
# Change key button
|
||||||
|
self.rekey_btn = QPushButton("Change key")
|
||||||
|
self.rekey_btn.clicked.connect(self._change_key)
|
||||||
|
|
||||||
bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||||
bb.accepted.connect(self._save)
|
bb.accepted.connect(self._save)
|
||||||
bb.rejected.connect(self.reject)
|
bb.rejected.connect(self.reject)
|
||||||
|
|
||||||
v = QVBoxLayout(self)
|
v = QVBoxLayout(self)
|
||||||
v.addLayout(form)
|
v.addLayout(form)
|
||||||
|
v.addWidget(self.rekey_btn)
|
||||||
v.addWidget(bb)
|
v.addWidget(bb)
|
||||||
|
|
||||||
def _browse(self):
|
def _browse(self):
|
||||||
|
|
@ -67,6 +75,26 @@ class SettingsDialog(QDialog):
|
||||||
save_db_config(self._cfg)
|
save_db_config(self._cfg)
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
def _change_key(self):
|
||||||
|
p1 = KeyPrompt(self, title="Change key", message="Enter new key")
|
||||||
|
if p1.exec() != QDialog.Accepted:
|
||||||
|
return
|
||||||
|
new_key = p1.key()
|
||||||
|
p2 = KeyPrompt(self, title="Change key", message="Re-enter new key")
|
||||||
|
if p2.exec() != QDialog.Accepted:
|
||||||
|
return
|
||||||
|
if new_key != p2.key():
|
||||||
|
QMessageBox.warning(self, "Key mismatch", "The two entries did not match.")
|
||||||
|
return
|
||||||
|
if not new_key:
|
||||||
|
QMessageBox.warning(self, "Empty key", "Key cannot be empty.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._db.rekey(new_key)
|
||||||
|
QMessageBox.information(self, "Key changed", "The database key was updated.")
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Error", f"Could not change key:\n{e}")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self) -> DBConfig:
|
def config(self) -> DBConfig:
|
||||||
return self._cfg
|
return self._cfg
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "bouquin"
|
name = "bouquin"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
||||||
authors = ["Miguel Jacq <mig@mig5.net>"]
|
authors = ["Miguel Jacq <mig@mig5.net>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue