Create a Preference dialog for fonts and audio output devices Turn Preferences and SoundOff into singletons. No matter how many times you request a new one, the same instance is returned. Stop using singals on the parent() to access other instances, such as sound and Preferences.
344 lines
10 KiB
Python
Executable File
344 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# TODO:
|
|
#
|
|
# Add definition to definition
|
|
# Add book import dialog
|
|
# Reading scroll with speed control
|
|
# Move controls out of reading window.
|
|
# Ability to edit text with updates to word-section links
|
|
# Need to be able to place a visible cursor in text.
|
|
# Scroll to cursor
|
|
# XXX:
|
|
# Scrolling is messed up. Need a way of marking current line.
|
|
#
|
|
import os
|
|
import re
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
from typing import cast
|
|
|
|
from PyQt6.QtCore import (
|
|
QModelIndex,
|
|
QResource,
|
|
Qt,
|
|
QTimer,
|
|
pyqtSignal,
|
|
pyqtSlot,
|
|
)
|
|
from PyQt6.QtGui import (
|
|
QAction,
|
|
QFont,
|
|
QTextCharFormat,
|
|
QTextCursor,
|
|
QTextDocument,
|
|
QTextListFormat,
|
|
)
|
|
from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
|
|
from PyQt6.QtWidgets import (
|
|
QApplication,
|
|
QFileDialog,
|
|
QMainWindow,
|
|
QMessageBox,
|
|
QPushButton,
|
|
)
|
|
|
|
from lib import *
|
|
from lib.preferences import Preferences
|
|
from ui.MainWindow import Ui_MainWindow
|
|
|
|
|
|
def query_error(query: QSqlQuery) -> None:
|
|
print(
|
|
"SQL Error:\n{}\n{}\n{}:{}".format(
|
|
query.executedQuery(),
|
|
query.boundValues(),
|
|
query.lastError().type(),
|
|
query.lastError().text(),
|
|
)
|
|
)
|
|
raise Exception("SQL Error")
|
|
|
|
|
|
class MainWindow(QMainWindow, Ui_MainWindow):
|
|
|
|
def __init__(self) -> None:
|
|
super(MainWindow, self).__init__()
|
|
self.setupUi(self)
|
|
#
|
|
# Setup resources
|
|
#
|
|
if not QResource.registerResource(
|
|
os.path.join(os.path.dirname(__file__), "ui/resources.rcc"), "/"
|
|
):
|
|
raise Exception("Unable to register resources.rcc")
|
|
|
|
model = QSqlQueryModel()
|
|
query = QSqlQuery("SELECT * FROM people ORDER BY name")
|
|
model.setQuery(query)
|
|
self.peopleView.setModel(model)
|
|
self.peopleView.setModelColumn(1)
|
|
self.actionEditPerson.setEnabled(False)
|
|
#
|
|
# Connections
|
|
#
|
|
# Action Connections
|
|
#
|
|
self.actionQuit.triggered.connect(self.close)
|
|
self.actionAddBook.triggered.connect(self.addBook)
|
|
self.actionEditBook.triggered.connect(self.editBook)
|
|
self.actionRead.triggered.connect(self.readBook)
|
|
self.actionRead.enabledChanged.connect(self.readBtn.setEnabled)
|
|
self.actionAddPerson.triggered.connect(self.addPerson)
|
|
self.actionEditPerson.triggered.connect(self.editPerson)
|
|
self.actionEditPerson.enabledChanged.connect(
|
|
self.editBtn.setEnabled
|
|
)
|
|
self.actionPreferences.triggered.connect(self.editPreferences)
|
|
self.peopleView.doubleClicked.connect(self.editPerson)
|
|
self.peopleView.clicked.connect(self.selectedPerson)
|
|
self.show()
|
|
return
|
|
|
|
@pyqtSlot()
|
|
def editPreferences(self):
|
|
dlg = Preferences()
|
|
dlg.exec()
|
|
return
|
|
|
|
@pyqtSlot(QModelIndex)
|
|
def selectedPerson(self, index: QModelIndex) -> None:
|
|
person_id = index.siblingAtColumn(0).data()
|
|
self.actionEditPerson.setEnabled(True)
|
|
book_id = index.siblingAtColumn(3).data()
|
|
if not book_id or book_id < 0:
|
|
self.actionRead.setEnabled(False)
|
|
else:
|
|
self.actionRead.setEnabled(True)
|
|
return
|
|
|
|
@pyqtSlot(bool)
|
|
def enablePerson(self, flag: bool) -> None:
|
|
if flag:
|
|
self.editBtn.setEnabled(False)
|
|
self.readBtn.setEnabled(False)
|
|
else:
|
|
self.editBtn.setEnabled(True)
|
|
self.readBtn.setEnabled(True)
|
|
return
|
|
|
|
@pyqtSlot()
|
|
def addPerson(self) -> None:
|
|
dlg = PersonDialog()
|
|
dlg.exec()
|
|
model = self.peopleView.model()
|
|
model.setQuery(model.query())
|
|
return
|
|
|
|
@pyqtSlot()
|
|
@pyqtSlot(QModelIndex)
|
|
def editPerson(self, index=None) -> None:
|
|
if not index:
|
|
indexes = self.peopleView.selectedIndexes()
|
|
if len(indexes) < 1:
|
|
return
|
|
index = indexes[0]
|
|
dlg = PersonDialog(person_id=index.siblingAtColumn(0).data())
|
|
dlg.exec()
|
|
return
|
|
|
|
@pyqtSlot()
|
|
def addBook(self) -> None:
|
|
directory = QFileDialog.getExistingDirectory()
|
|
self.book = Book(directory)
|
|
return
|
|
|
|
@pyqtSlot()
|
|
def editBook(self) -> None:
|
|
print("Edit Book")
|
|
return
|
|
|
|
session = None
|
|
setPerson = pyqtSignal(int)
|
|
|
|
@pyqtSlot()
|
|
def readBook(self) -> None:
|
|
indexes = self.peopleView.selectedIndexes()
|
|
if len(indexes) < 1:
|
|
return
|
|
person_id = indexes[0].siblingAtColumn(0).data()
|
|
if not self.session:
|
|
self.session = SessionDialog()
|
|
self.setPerson.connect(self.session.setPerson)
|
|
self.session.show()
|
|
self.session.raise_()
|
|
self.setPerson.emit(person_id)
|
|
self.dlg = EditDialog(self, self.session, person_id)
|
|
self.dlg.show()
|
|
self.dlg.raise_()
|
|
return
|
|
|
|
|
|
SQL_CMDS = [
|
|
"PRAGMA foreign_keys=ON",
|
|
#
|
|
"CREATE TABLE IF NOT EXISTS words "
|
|
"(word_id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT, definition TEXT)",
|
|
#
|
|
"CREATE TABLE IF NOT EXISTS books "
|
|
"(book_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, author TEXT, "
|
|
"uuid TEXT, level INTEGER)",
|
|
#
|
|
"CREATE TABLE IF NOT EXISTS sections "
|
|
"(section_id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
"sequence INTEGER, content TEXT, "
|
|
"book_id INTEGER REFERENCES books ON DELETE CASCADE)",
|
|
#
|
|
"CREATE TABLE IF NOT EXISTS people "
|
|
"(person_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, "
|
|
"organization TEXT, book_id INTEGER REFERENCES books ON DELETE CASCADE, "
|
|
"email TEXT)",
|
|
#
|
|
"CREATE TABLE IF NOT EXISTS person_book "
|
|
"(person_id INTEGER REFERENCES people ON DELETE CASCADE, "
|
|
"book_id INTEGER REFERENCES books ON DELETE CASCADE, "
|
|
"section_id INTEGER REFERENCES sections, "
|
|
"block INTEGER)",
|
|
#
|
|
"CREATE TABLE IF NOT EXISTS word_block "
|
|
"(word_id INTEGER REFERENCES words ON DELETE CASCADE, "
|
|
"section_id INTEGER REFERENCES sections ON DELETE CASCADE, "
|
|
"block INTEGER NOT NULL, start INTEGER NOT NULL, end INTEGER NOT NULL)",
|
|
#
|
|
"CREATE TABLE IF NOT EXISTS sessions "
|
|
"(session_id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
"person_id INTEGER REFERENCES people ON DELETE CASCADE, "
|
|
"start TEXT DEFAULT '', "
|
|
"stop TEXT DEFAULT '', "
|
|
"total TEXT DEFAULT '', "
|
|
"notes TEXT DEFAULT '')",
|
|
#
|
|
"CREATE TABLE IF NOT EXISTS session_word "
|
|
"(session_id INTEGER REFERENCES sessions ON DELETE CASCADE, "
|
|
"word_id INTEGER REFERENCES words ON DELETE CASCADE, "
|
|
"important INTEGER DEFAULT 0)",
|
|
#
|
|
"CREATE TABLE IF NOT EXISTS session_block "
|
|
"(session_id INTEGER REFERENCES sessions ON DELETE CASCADE, "
|
|
"section_id INTEGER REFERENCES sections ON DELETE CASCADE, "
|
|
"block INTEGER)",
|
|
]
|
|
|
|
|
|
def schema_update(db: QSqlDatabase) -> None:
|
|
query = QSqlQuery()
|
|
|
|
for sql in SQL_CMDS:
|
|
inlower = sql.lower().strip()
|
|
if not inlower.startswith("create table "):
|
|
if not query.exec(sql):
|
|
query_error(query)
|
|
continue
|
|
create_cmd = re.sub(r"IF NOT EXISTS ", "", sql.strip())
|
|
create_cmd = re.sub(r"\s\s*", " ", create_cmd)
|
|
matches = re.search(r"^(CREATE TABLE )([^ ]+)( \(.+)$", create_cmd)
|
|
if matches:
|
|
table_name = matches.group(2)
|
|
create_cmd = (
|
|
matches.group(1)
|
|
+ '"'
|
|
+ matches.group(2)
|
|
+ '"'
|
|
+ matches.group(3)
|
|
)
|
|
else:
|
|
raise AttributeError(f"No match found: {create_cmd}")
|
|
|
|
query.prepare("SELECT sql FROM sqlite_schema WHERE tbl_name = :tbl")
|
|
query.bindValue(":tbl", table_name)
|
|
if not query.exec():
|
|
query_error(query)
|
|
query.next()
|
|
old = query.value(0)
|
|
if not old:
|
|
if not query.exec(sql):
|
|
query_error(query)
|
|
continue
|
|
if old.lower() == create_cmd.lower():
|
|
continue
|
|
print(old.lower())
|
|
print(create_cmd.lower())
|
|
print(f"Updating: {table_name}")
|
|
|
|
# Step 1 turn off foreign key constraints
|
|
if not query.exec("PRAGMA foreign_keys=OFF"):
|
|
query_error(query)
|
|
# Step 2 start a transaction
|
|
db.transaction()
|
|
# Step 3 remember old indexes, triggers, and views
|
|
# Step 4 create new table
|
|
new_table_name = table_name + "_new"
|
|
if not query.exec(matches.group(1) + new_table_name + matches.group(3)):
|
|
query_error(query)
|
|
# step 5 transfer content
|
|
coldefs = re.search(r"\((.+)\)", old).group(1).split(", ") # type: ignore[union-attr]
|
|
cols = [x.split(" ")[0] for x in coldefs]
|
|
cols_str = ", ".join(cols)
|
|
if not query.exec(
|
|
f"INSERT INTO {new_table_name} ({cols_str}) SELECT {cols_str} FROM {table_name}"
|
|
):
|
|
query_error(query)
|
|
|
|
# step 6 Drop old table
|
|
if not query.exec("DROP TABLE " + table_name):
|
|
query_error(query)
|
|
# step 6 rename new table to old table
|
|
if not query.exec(
|
|
"ALTER TABLE " + new_table_name + " RENAME TO " + table_name
|
|
):
|
|
query_error(query)
|
|
|
|
# step 8 create indexes, triggers, and views
|
|
# step 9 rebuild affected views
|
|
# step 10 turn foreign key constrants back on
|
|
if not query.exec("PRAGMA foreign_keys=ON"):
|
|
query_error(query)
|
|
# step 11 commit the changes
|
|
db.commit()
|
|
return
|
|
|
|
|
|
def main() -> int:
|
|
db = QSqlDatabase()
|
|
db = db.addDatabase("QSQLITE")
|
|
db.setDatabaseName("twel.db")
|
|
db.open()
|
|
app = QApplication(sys.argv)
|
|
schema_update(db)
|
|
window: QMainWindow = MainWindow()
|
|
return app.exec()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
outOfDate = []
|
|
for fileName in os.listdir("ui"):
|
|
if not fileName.endswith(".py"):
|
|
continue
|
|
uiName = "ui/" + fileName[:-3] + ".ui"
|
|
rccName = "ui/" + fileName[:-3] + ".qrc"
|
|
if not os.path.isfile(uiName) and not os.path.isfile(rccName):
|
|
outOfDate.append(filenName)
|
|
continue
|
|
if os.path.isfile(uiName) and os.path.getmtime(
|
|
uiName
|
|
) > os.path.getmtime("ui/" + fileName):
|
|
outOfDate.append(fileName)
|
|
if os.path.isfile(rccName) and os.path.getmtime(
|
|
rccName
|
|
) > os.path.getmtime("ui/" + fileName):
|
|
outOfDate.append(fileName)
|
|
if len(outOfDate) > 0:
|
|
print(f"UI out of date: {', '.join(outOfDate)}")
|
|
sys.exit(1)
|
|
sys.exit(main())
|