From 0b02ed2201fc488853698d87c8ac34d33c7c1714 Mon Sep 17 00:00:00 2001 From: "Christopher T. Johnson" Date: Tue, 14 Nov 2023 10:43:50 -0500 Subject: [PATCH] Mostly working Reader --- lib/books.py | 136 +++++++++++++++++++++++++++++-- lib/read.py | 207 +++++++++++++++++++++++++++++++---------------- main.py | 140 +++++++++++++++++--------------- ui/MainWindow.py | 16 +++- ui/MainWindow.ui | 22 ++++- 5 files changed, 374 insertions(+), 147 deletions(-) diff --git a/lib/books.py b/lib/books.py index 05319ca..02e2eb8 100644 --- a/lib/books.py +++ b/lib/books.py @@ -1,15 +1,49 @@ import json import os import xml.dom.minidom +from typing import Dict, List, cast + +from PyQt6.QtSql import QSqlQuery + +from main import query_error class Book: - sections = [] - metadata = {} + sections: List[str] = [] + metadata: Dict[str, str] = {} + words = {} def __init__(self, src: str) -> None: super(Book, self).__init__() self.parse_book(src) + book_id = self.store() # Does nothing if already in database + self.load(book_id) + return + + def load(self, book_id: int) -> None: + query = QSqlQuery() + query.prepare("SELECT * FROM books where book_id = :book_id") + query.bindValue(":book_id", book_id) + if not query.exec(): + query_error(query) + if not query.next(): + raise Exception(f"Missing book? book_id={book_id}") + self.metadata = { + "title": query.value("title"), + "creator": query.value("author"), + "identifier": query.value("uuid"), + "level": query.value("level"), + } + + self.sections = [] + query.prepare( + "SELECT * FORM sections WHERE book_id = :book_id " "ORDER BY sequence" + ) + while query.next(): + self.sections.append(query.value("contents")) + # + # Load words! + # return def parse_book(self, src: str) -> None: @@ -53,15 +87,105 @@ class Book: href = item.getAttribute("href") print(f"{idref}: {href}") self.parse_section(src, href) + # + # "sections" is now loaded + # return + def store(self) -> int: + uuid = self.metadata["identifier"] + query = QSqlQuery() + query.prepare( + "SELECT COUNT(*) AS number, book_id FROM books b " "WHERE b.uuid = :uuid" + ) + query.bindValue(":uuid", uuid) + if not query.exec(): + query_error(query) + query.next() + if query.value("number") > 0: + book_id: int = query.value("book_id") + return book_id + query.prepare( + "INSERT INTO books (title, author, uuid, level) VALUES (" + ":title, :author, :uuid, 0)" + ) + query.bindValue(":title", self.metadata["title"]) + query.bindValue(":author", self.metadata["creator"]) + query.bindValue(":uuid", uuid) + if not query.exec(): + query_error(query) + book_id = query.lastInsertId() + query.prepare( + "INSERT INTO sections (sequence, book_id, content) " + "VALUES (:sequence, :book_id, :content)" + ) + query.bindValue(":book_id", book_id) + for seq, section in enumerate(self.sections): + query.bindValue(":sequence", seq) + query.bindValue(":content", section) + if not query.exec(): + query_error(query) + section_id = query.lastInsertId() + return book_id + def parse_section(self, src: str, href: str) -> None: + newdom = xml.dom.getDOMImplementation().createDocument("", "html", None) + + def strip_node(elm: xml.dom.minidom.Element) -> xml.dom.minidom.Node: + if elm.nodeType == xml.dom.Node.TEXT_NODE: + return cast( + xml.dom.minidom.Node, + newdom.createTextNode(cast(xml.dom.minidom.Text, elm).data), + ) + + newelm = newdom.createElement(elm.localName) + node = elm.firstChild + while node: + if node.nodeType == xml.dom.Node.TEXT_NODE: + text = node.data + if text: + text = text.strip() + if text and len(text) > 0: + newelm.appendChild(newdom.createTextNode(text)) + elif node.localName == "img": + pass + elif node.localName == "a": + a_node = node.firstChild + while a_node: + if a_node.nodeType == xml.dom.Node.TEXT_NODE: + newelm.appendChild(newdom.createTextNode(a_node.data)) + else: + newelm.appendChild(strip_node(a_node)) + a_node = a_node.nextSibling + else: + newelm.appendChild(strip_node(node)) + node = node.nextSibling + return newelm + + def parse_node(parent: xml.dom.Node, elm: xml.dom.Node) -> None: + if elm.nodeType == xml.dom.Node.ELEMENT_NODE: + if elm.localName.startswith("h"): + clone = strip_node(elm) + parent.appendChild(clone) + elif elm.localName == "p": + clone = strip_node(elm) + clone.normalize() + parent.appendChild(clone) + else: + node = elm.firstChild + while node: + parse_node(parent, node) + node = node.nextSibling + return + with open(f"{src}/{href}") as f: dom = xml.dom.minidom.parse(f) title = dom.getElementsByTagName("title")[0].firstChild.data body = dom.getElementsByTagName("body")[0] - paragraphs = [] - for p in body.getElementsByTagName("p"): - paragraphs.append(p.toxml()) - self.sections.append({"title": title, "paragraphs": paragraphs}) + section = newdom.createElement("body") + node = body.firstChild + while node: + parse_node(section, node) + node = node.nextSibling + self.sections.append(section.toxml()) return diff --git a/lib/read.py b/lib/read.py index fde328c..9e7b8db 100644 --- a/lib/read.py +++ b/lib/read.py @@ -1,80 +1,133 @@ import json from PyDictionary import PyDictionary # type: ignore[import-untyped] -from PyQt6.QtCore import Qt, pyqtSlot +from PyQt6.QtCore import QRect, Qt, pyqtSlot from PyQt6.QtGui import ( + QBrush, + QColor, QFont, + QPainter, + QPainterPath, QTextCharFormat, QTextCursor, QTextDocument, QTextListFormat, ) from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel -from PyQt6.QtWidgets import QDialog +from PyQt6.QtWidgets import QDialog, QPushButton from main import query_error from ui.EditDialog import Ui_Dialog class EditDialog(QDialog, Ui_Dialog): - def __init__(self, book_id: int, person_id: int) -> None: + def __init__(self, person_id: int) -> None: super(EditDialog, self).__init__() - self.book_id = book_id self.person_id = person_id self.setupUi(self) - self.current_paragraph(self.person_id) + self.load_book(self.person_id) + blockNumber = self.block self.paraEdit.setReadOnly(True) self.defEdit.setReadOnly(True) + self.show_section(self.section_id) + self.block = blockNumber + self.savePosition() self.stackedWidget.setCurrentIndex(0) + self.nextParaBtn = QPushButton(parent=self.widget) + self.nextParaBtn.setObjectName("nextParaBtn") + self.verticalLayout.addWidget(self.nextParaBtn) + self.nextParaBtn.setText("Next Paragraph") self.defineBtn.clicked.connect(self.defineAction) self.showBtn.clicked.connect(self.showAction) self.nextBtn.clicked.connect(self.nextAction) self.prevBtn.clicked.connect(self.prevAction) + self.nextParaBtn.clicked.connect(self.nextParaAction) + self.paraEdit.verticalScrollBar().valueChanged.connect(self.scrollSlot) return - def current_paragraph(self, person_id: int) -> None: - query = QSqlQuery() - query.prepare("SELECT * FROM people WHERE person_id = :person_id") - query.bindValue(":person_id", person_id) - query.exec() - query.next() - paragraph_id = query.value("paragraph_id") - self.display_paragraph(paragraph_id) + @pyqtSlot(int) + def scrollSlot(self, value): + self.update() return - def display_paragraph(self, paragraph_id: int) -> None: - self.paragraph_id = paragraph_id + def load_book(self, person_id: int) -> None: query = QSqlQuery() - query.prepare("SELECT * FROM paragraphs WHERE paragraph_id = :paragraph_id") - query.bindValue(":paragraph_id", paragraph_id) - query.exec() - query.next() - self.section_id = query.value("section_id") - self.paraSequence = query.value("sequence") - cursor = self.paraEdit.textCursor() - cursor.movePosition(QTextCursor.MoveOperation.Start) - cursor.movePosition( - QTextCursor.MoveOperation.End, QTextCursor.MoveMode.KeepAnchor - ) - cursor.removeSelectedText() - cursor.insertHtml(query.value("content")) query.prepare( - "SELECT * FROM word_paragraph " "WHERE paragraph_id = :paragraph_id" + "SELECT pb.* FROM people p " + "LEFT JOIN person_book pb " + "ON (p.book_id = pb.book_id " + "AND p.person_id = pb.person_id) " + "WHERE p.person_id = :person_id" ) - query.bindValue(":paragraph_id", self.paragraph_id) + query.bindValue(":person_id", person_id) + if not query.exec(): + query_error(query) + if not query.next(): + self.done(0) + self.book_id = query.value("book_id") + self.section_id = query.value("section_id") + self.block = query.value("block") + self.sections = [] + self.section_map = {} + self.sequence_map = {} + query.prepare( + "SELECT * FROM sections " "WHERE book_id = :book_id " "ORDER BY sequence" + ) + query.bindValue(":book_id", self.book_id) if not query.exec(): query_error(query) - def_format = QTextCharFormat() - def_format.setFontUnderline(True) - cursor = QTextCursor(self.paraEdit.document()) while query.next(): - start = query.value("start") - end = query.value("end") - cursor.setPosition(start, QTextCursor.MoveMode.MoveAnchor) - cursor.setPosition(end, QTextCursor.MoveMode.KeepAnchor) - cursor.setCharFormat(def_format) - cursor.setPosition(0) + self.sections.append(query.value("content")) + self.section_map[query.value("section_id")] = query.value("sequence") + self.sequence_map[query.value("sequence")] = query.value("section_id") + return + + def show_section(self, section_id, start=True): + sequence = self.section_map[section_id] + self.setWindowTitle(f"Section {sequence}") + self.paraEdit.clear() + cursor = self.paraEdit.textCursor() + cursor.insertHtml(self.sections[sequence]) + if start: + self.block = 0 + else: + self.block = self.paraEdit.document().blockCount() - 1 + textBlock = self.paraEdit.document().findBlockByNumber(self.block) + cursor.setPosition(textBlock.position()) self.paraEdit.setTextCursor(cursor) + self.paraEdit.ensureCursorVisible() + return + + def mousePressEvent(self, event): + return + + def paintEvent(self, e): + position = self.paraEdit.document().findBlockByNumber(self.block).position() + cursor = self.paraEdit.textCursor() + cursor.setPosition(position) + rect = self.paraEdit.cursorRect(cursor) + # print(rect) + pos = self.paraEdit.mapToParent(self.paraEdit.pos()) + painter = QPainter(self) + brush = QBrush() + brush.setColor(QColor("green")) + brush.setStyle(Qt.BrushStyle.SolidPattern) + path = QPainterPath() + path.moveTo(0, 0) + path.lineTo(pos.x() - 1.0, pos.y() / 2.0) + path.lineTo(0, pos.y()) + path.lineTo(0, 0) + # XXX - Replace the guess with a calculated value + painter.translate(1.0, pos.y() + rect.y() + 12) + painter.fillPath(path, brush) + + @pyqtSlot() + def nextParaAction(self) -> None: + self.block += 1 + if self.block >= self.paraEdit.document().blockCount(): + self.nextAction() + return + self.savePosition() return @pyqtSlot() @@ -234,44 +287,58 @@ class EditDialog(QDialog, Ui_Dialog): self.display_definition() return + def scrollTo(self, position): + cursor = self.paraEdit.textCursor() + cursor.setPosition(position) + rect = self.paraEdit.cursorRect(cursor) + print(rect) + return + + def savePosition(self) -> None: + cursor = self.paraEdit.textCursor() + cursor.setPosition( + self.paraEdit.document().findBlockByNumber(self.block).position() + ) + self.paraEdit.setTextCursor(cursor) + self.scrollTo(cursor.position()) + self.paraEdit.ensureCursorVisible() + self.update() + query = QSqlQuery() + query.prepare( + "UPDATE person_book SET section_id = :section_id, " + "block = :block " + "WHERE book_id = :book_id " + "AND person_id = :person_id" + ) + query.bindValue(":section_id", self.section_id) + query.bindValue(":block", self.block) + query.bindValue(":book_id", self.book_id) + query.bindValue(":person_id", self.person_id) + if not query.exec(): + query_error(query) + return + @pyqtSlot() def nextAction(self) -> None: if self.stackedWidget.currentIndex() == 1: self.nextDefinition() return - paraQuery = QSqlQuery() - paraQuery.prepare( - "SELECT * FROM paragraphs WHERE " - "section_id=:section_id AND sequence = :sequence" - ) - paraQuery.bindValue(":section_id", self.section_id) - paraQuery.bindValue(":sequence", self.paraSequence + 1) - if not paraQuery.exec(): - query_error(paraQuery) - if not paraQuery.next(): - secQuery = QSqlQuery() - secQuery.prepare( - "SELECT * FROM sections WHERE book_id=:book_id " - "AND sequence = " - "(SELECT sequence FROM sections WHERE " - "section_id = :section_id)+1" - ) - secQuery.bindValue(":book_id", self.book_id) - secQuery.bindValue(":section_id", self.section_id) - if not secQuery.exec(): - query_error(secQuery) - if not secQuery.next(): - return - self.paraSequence = 0 - self.section_id = secQuery.value("section_id") - paraQuery.bindValue(":section_id", self.section_id) - paraQuery.bindValue(":sequence", self.paraSequence) - if not paraQuery.exec(): - query_error(paraQuery) - paraQuery.next() - self.display_paragraph(paraQuery.value("paragraph_id")) + sequence = self.section_map[self.section_id] + sequence += 1 + self.section_id = self.sequence_map[sequence] + self.show_section(self.section_id) + self.savePosition() return @pyqtSlot() def prevAction(self) -> None: + if self.stackedWidget.currentIndex() == 1: + return + sequence = self.section_map[self.section_id] + if sequence < 1: + return + sequence -= 1 + self.section_id = self.sequence_map[sequence] + self.show_section(self.section_id, False) + self.savePosition() return diff --git a/main.py b/main.py index 88b8d2b..d48504f 100755 --- a/main.py +++ b/main.py @@ -5,6 +5,7 @@ import sys from PyQt6.QtCore import Qt, pyqtSlot from PyQt6.QtGui import ( + QAction, QFont, QTextCharFormat, QTextCursor, @@ -33,8 +34,6 @@ def query_error(query: QSqlQuery) -> None: class MainWindow(QMainWindow, Ui_MainWindow): - book_id = 0 - def __init__(self) -> None: super(MainWindow, self).__init__() self.setupUi(self) @@ -47,64 +46,77 @@ class MainWindow(QMainWindow, Ui_MainWindow): # self.wordButton.clicked.connect(self.wordAction) self.ReadButton.clicked.connect(self.readAction) self.bookBtn.clicked.connect(self.bookAction) + self.createActions() self.show() return + def createActions(self): + query = QSqlQuery() + query.prepare("SELECT * FROM books ORDER BY title") + if not query.exec(): + query_error(query) + while query.next(): + action = QAction(query.value("title"), self) + action.setData(query.value("book_id")) + action.triggered.connect(self.setBookAction) + self.menuBooks.addAction(action) + return + + @pyqtSlot() + def setBookAction(self): + action = self.sender() + print(action) + book_id = action.data() + print(book_id) + indexes = self.peopleView.selectedIndexes() + if len(indexes) < 1: + return + person_id = indexes[0].siblingAtColumn(0).data() + print(person_id) + query = QSqlQuery() + query.prepare( + "UPDATE people SET book_id = :book_id " "WHERE person_id = :person_id" + ) + query.bindValue(":book_id", book_id) + query.bindValue(":person_id", person_id) + if not query.exec(): + query_error(query) + query.prepare( + "SELECT * FROM person_book " + "WHERE person_id = :person_id " + "AND book_id = :book_id" + ) + query.bindValue(":person_id", person_id) + query.bindValue(":book_id", book_id) + if not query.exec(): + query_error(query) + if query.next(): + return + query.prepare( + "SELECT * FROM sections WHERE sequence = 0 " "AND book_id = :book_id" + ) + query.bindValue(":book_id", book_id) + if not query.exec(): + query_error(query) + if not query.next(): + raise Exception(f"book_id: {book_id} has no section 0!") + section_id = query.value("section_id") + query.prepare( + "INSERT INTO person_book " "VALUES (:person_id, :book_id, :section_id, 0)" + ) + query.bindValue(":person_id", person_id) + query.bindValue(":book_id", book_id) + query.bindValue(":section_id", section_id) + if not query.exec(): + query_error(query) + return + @pyqtSlot() def bookAction(self) -> None: directory = QFileDialog.getExistingDirectory() - book = Book(directory) - self.book_id = self.store_book(book) + self.book = Book(directory) return - def store_book(self, book: Book) -> int: - uuid = book.metadata["identifier"] - query = QSqlQuery() - query.prepare( - "SELECT COUNT(*) AS number, book_id FROM books b " "WHERE b.uuid = :uuid" - ) - query.bindValue(":uuid", uuid) - if not query.exec(): - query_error(query) - query.next() - if query.value("number") > 0: - book_id: int = query.value("book_id") - return book_id - query.prepare( - "INSERT INTO books (title, author, uuid, level) VALUES (" - ":title, :author, :uuid, 0)" - ) - query.bindValue(":title", book.metadata["title"]) - query.bindValue(":author", book.metadata["creator"]) - query.bindValue(":uuid", uuid) - if not query.exec(): - query_error(query) - book_id = query.lastInsertId() - section_query = QSqlQuery() - section_query.prepare( - "INSERT INTO sections (title, sequence, book_id) " - "VALUES (:title, :sequence, :book_id)" - ) - section_query.bindValue(":book_id", book_id) - para_query = QSqlQuery() - para_query.prepare( - "INSERT INTO paragraphs (section_id, sequence, content) " - "VALUES (:section_id, :sequence, :content)" - ) - for seq, section in enumerate(book.sections): - section_query.bindValue(":title", section["title"]) - section_query.bindValue(":sequence", seq) - if not section_query.exec(): - query_error(section_query) - section_id = query.lastInsertId() - para_query.bindValue(":section_id", section_id) - for ps, paragraph in enumerate(section["paragraphs"]): - para_query.bindValue(":sequence", ps) - para_query.bindValue(":content", paragraph) - if not para_query.exec(): - query_error(para_query) - return book_id - def load_definition(self, word: str, definition: dict) -> None: document = None # self.textEdit.document() myCursor = QTextCursor(document) @@ -144,9 +156,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): if len(indexes) < 1: return person_id = indexes[0].siblingAtColumn(0).data() - name = indexes[0].data() - print(person_id, name) - dlg = EditDialog(self.book_id, person_id) + dlg = EditDialog(person_id) dlg.exec() return @@ -161,24 +171,20 @@ SQL_CMDS = [ "uuid TEXT, level INTEGER)", "CREATE TABLE IF NOT EXISTS sections " "(section_id INTEGER PRIMARY KEY AUTOINCREMENT, " - "title TEXT, sequence INTEGER, " + "sequence INTEGER, content TEXT, " "book_id INTEGER REFERENCES books ON DELETE CASCADE) ", - "CREATE TABLE IF NOT EXISTS paragraphs " - "(paragraph_id INTEGER PRIMARY KEY AUTOINCREMENT, " - "section_id INTEGER REFERENCES sections ON DELETE CASCADE, " - "sequence INTEGER NOT NULL DEFAULT 0, " - "content TEXT)", "CREATE TABLE IF NOT EXISTS people " "(person_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, " - "organization TEXT, " - "paragraph_id INTEGER REFERENCES paragraphs ON DELETE CASCADE)", + "organization TEXT, book_id INTEGER REFERENCES books ON DELETE CASCADE) ", "CREATE TABLE IF NOT EXISTS person_book " "(person_id INTEGER REFERENCES people ON DELETE CASCADE, " - "book_id INTEGER REFERENCES books ON DELETE CASCADE)", - "CREATE TABLE IF NOT EXISTS word_paragraph " + "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, " - "paragraph_id INTEGER REFERENCES paragraphs ON DELETE CASCADE, " - "start INTEGER NOT NULL, end INTEGER NOT NULL)", + "section_id INTEGER REFERENCES sections ON DELETE CASCADE, " + "block INTEGER NOT NULL, start INTEGER NOT NULL, end INTEGER NOT NULL)", ] diff --git a/ui/MainWindow.py b/ui/MainWindow.py index 65633e4..4ca2a6d 100644 --- a/ui/MainWindow.py +++ b/ui/MainWindow.py @@ -38,19 +38,31 @@ class Ui_MainWindow(object): self.horizontalLayout.addWidget(self.widget) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(parent=MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 22)) + self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 32)) self.menubar.setObjectName("menubar") + self.menuFile = QtWidgets.QMenu(parent=self.menubar) + self.menuFile.setObjectName("menuFile") + self.menuBooks = QtWidgets.QMenu(parent=self.menubar) + self.menuBooks.setObjectName("menuBooks") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(parent=MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) + self.actionQuit = QtGui.QAction(parent=MainWindow) + self.actionQuit.setObjectName("actionQuit") + self.menuFile.addAction(self.actionQuit) + self.menubar.addAction(self.menuFile.menuAction()) + self.menubar.addAction(self.menuBooks.menuAction()) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + MainWindow.setWindowTitle(_translate("MainWindow", "Reading Helper")) self.WordButton.setText(_translate("MainWindow", "Words")) self.ReadButton.setText(_translate("MainWindow", "Read")) self.bookBtn.setText(_translate("MainWindow", "Add Book")) + self.menuFile.setTitle(_translate("MainWindow", "File")) + self.menuBooks.setTitle(_translate("MainWindow", "Books")) + self.actionQuit.setText(_translate("MainWindow", "Quit")) diff --git a/ui/MainWindow.ui b/ui/MainWindow.ui index 8335de1..b999766 100644 --- a/ui/MainWindow.ui +++ b/ui/MainWindow.ui @@ -11,7 +11,7 @@ - MainWindow + Reading Helper @@ -66,11 +66,29 @@ 0 0 800 - 22 + 32 + + + File + + + + + + Books + + + + + + + Quit + +