diff --git a/lib/books.py b/lib/books.py index 02e2eb8..0109d8b 100644 --- a/lib/books.py +++ b/lib/books.py @@ -11,7 +11,6 @@ from main import query_error class Book: sections: List[str] = [] metadata: Dict[str, str] = {} - words = {} def __init__(self, src: str) -> None: super(Book, self).__init__() @@ -131,61 +130,66 @@ class Book: 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: + def strip_node(elm: xml.dom.minidom.Element) -> xml.dom.minidom.Element: if elm.nodeType == xml.dom.Node.TEXT_NODE: return cast( - xml.dom.minidom.Node, + xml.dom.minidom.Element, newdom.createTextNode(cast(xml.dom.minidom.Text, elm).data), ) - newelm = newdom.createElement(elm.localName) + newelm: xml.dom.minidom.Element = newdom.createElement(elm.localName) node = elm.firstChild while node: - if node.nodeType == xml.dom.Node.TEXT_NODE: - text = node.data + elm = cast(xml.dom.minidom.Element, node) + if elm.nodeType == xml.dom.Node.TEXT_NODE: + text = cast(xml.dom.minidom.Text, elm).data if text: text = text.strip() if text and len(text) > 0: newelm.appendChild(newdom.createTextNode(text)) - elif node.localName == "img": + elif elm.localName == "img": pass - elif node.localName == "a": - a_node = node.firstChild + elif elm.localName == "a": + a_node = cast(xml.dom.minidom.Element, elm.firstChild) while a_node: if a_node.nodeType == xml.dom.Node.TEXT_NODE: - newelm.appendChild(newdom.createTextNode(a_node.data)) + text = cast(xml.dom.minidom.Text, a_node) + newelm.appendChild(newdom.createTextNode(text.data)) else: newelm.appendChild(strip_node(a_node)) a_node = a_node.nextSibling else: - newelm.appendChild(strip_node(node)) + newelm.appendChild(strip_node(elm)) node = node.nextSibling return newelm - def parse_node(parent: xml.dom.Node, elm: xml.dom.Node) -> None: + def parse_node( + parent: xml.dom.minidom.Element, elm: xml.dom.minidom.Element + ) -> None: if elm.nodeType == xml.dom.Node.ELEMENT_NODE: - if elm.localName.startswith("h"): + tag: str = cast(str, elm.localName) + if tag.startswith("h"): clone = strip_node(elm) parent.appendChild(clone) - elif elm.localName == "p": + elif tag == "p": clone = strip_node(elm) clone.normalize() parent.appendChild(clone) else: node = elm.firstChild while node: - parse_node(parent, node) + parse_node(parent, cast(xml.dom.minidom.Element, 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 + # title = dom.getElementsByTagName("title")[0].firstChild.data body = dom.getElementsByTagName("body")[0] section = newdom.createElement("body") node = body.firstChild while node: - parse_node(section, node) + parse_node(section, cast(xml.dom.minidom.Element, node)) node = node.nextSibling self.sections.append(section.toxml()) return diff --git a/lib/read.py b/lib/read.py index 9e7b8db..5c6682a 100644 --- a/lib/read.py +++ b/lib/read.py @@ -1,13 +1,16 @@ import json +from typing import cast -from PyDictionary import PyDictionary # type: ignore[import-untyped] -from PyQt6.QtCore import QRect, Qt, pyqtSlot +import requests +from PyQt6.QtCore import QPoint, QRect, Qt, QTimer, pyqtSlot from PyQt6.QtGui import ( QBrush, QColor, QFont, + QMouseEvent, QPainter, QPainterPath, + QPaintEvent, QTextCharFormat, QTextCursor, QTextDocument, @@ -21,10 +24,26 @@ from ui.EditDialog import Ui_Dialog class EditDialog(QDialog, Ui_Dialog): + block: int + def __init__(self, person_id: int) -> None: super(EditDialog, self).__init__() self.person_id = person_id self.setupUi(self) + # + # Override UI + # + font = QFont() + font.setFamily("OpenDyslexic") + font.setPointSize(14) + self.paraEdit.setFont(font) + self.nextParaBtn = QPushButton(parent=self.widget) + self.nextParaBtn.setObjectName("nextParaBtn") + self.verticalLayout.addWidget(self.nextParaBtn) + self.nextParaBtn.setText("Next Paragraph") + # + # End overrides + # self.load_book(self.person_id) blockNumber = self.block self.paraEdit.setReadOnly(True) @@ -33,20 +52,60 @@ class EditDialog(QDialog, Ui_Dialog): 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.wordsBtn.clicked.connect(self.wordAction) self.paraEdit.verticalScrollBar().valueChanged.connect(self.scrollSlot) return + @pyqtSlot() + def wordAction(self) -> None: + pos = self.paraEdit.mapTo(self, self.paraEdit.cursorRect().topLeft()) + top = self.paraEdit.mapTo(self, QPoint(0, 0)) + value = self.paraEdit.verticalScrollBar().value() + # + # XXX - Where does "4" come from? + # + delta = pos.y() - top.y() - 4 + self.pxPerTick = int(1000 / delta) + if self.pxPerTick < 1: + if delta < 0: + self.pxPerTick = -1 + else: + self.pxPerTick = 1 + steps = abs(delta / self.pxPerTick) + msPerTick = int(1000 / steps) + if msPerTick < 3: + msPerTick = 3 + self.target = value + delta + print(f"delta: {delta}, pixels: {self.pxPerTick}, ms: {msPerTick}") + timer = QTimer(self) + timer.timeout.connect(self.softTick) + timer.start(msPerTick) + return + + @pyqtSlot() + def softTick(self) -> None: + value = self.paraEdit.verticalScrollBar().value() + sender: QTimer = cast(QTimer, self.sender()) + if self.pxPerTick < 0: # moving content up + if value < self.target: + sender.stop() + return + else: + if value > self.target: + sender.stop() + return + value += self.pxPerTick + self.paraEdit.verticalScrollBar().setValue(value) + self.update() + return + @pyqtSlot(int) - def scrollSlot(self, value): + def scrollSlot(self, value: int) -> None: self.update() return @@ -82,7 +141,7 @@ class EditDialog(QDialog, Ui_Dialog): self.sequence_map[query.value("sequence")] = query.value("section_id") return - def show_section(self, section_id, start=True): + def show_section(self, section_id: int, start: bool = True) -> None: sequence = self.section_map[section_id] self.setWindowTitle(f"Section {sequence}") self.paraEdit.clear() @@ -96,29 +155,57 @@ class EditDialog(QDialog, Ui_Dialog): cursor.setPosition(textBlock.position()) self.paraEdit.setTextCursor(cursor) self.paraEdit.ensureCursorVisible() + # + # Mark all the defined words with underlines + # + def_format = QTextCharFormat() + def_format.setFontUnderline(True) + cursor = QTextCursor(self.paraEdit.document()) + query = QSqlQuery() + query.prepare("SELECT * FROM word_block " "WHERE section_id = :section_id") + query.bindValue(":section_id", section_id) + if not query.exec(): + query_error(query) + while query.next(): + # + # Define these variables so that the code matches + # the defining action + # + blockNum = query.value("block") + start = query.value("start") + end = query.value("end") + textBlock = self.paraEdit.document().findBlockByNumber(blockNum) + cursor.setPosition( + start + textBlock.position(), QTextCursor.MoveMode.MoveAnchor + ) + cursor.setPosition( + end + textBlock.position(), QTextCursor.MoveMode.KeepAnchor + ) + cursor.setCharFormat(def_format) + return - def mousePressEvent(self, event): + def mousePressEvent(self, event: QMouseEvent | None) -> None: return - def paintEvent(self, e): + def paintEvent(self, e: QPaintEvent | None) -> None: position = self.paraEdit.document().findBlockByNumber(self.block).position() cursor = self.paraEdit.textCursor() cursor.setPosition(position) + # + # rect is position in viewport coordenates. rect = self.paraEdit.cursorRect(cursor) - # print(rect) - pos = self.paraEdit.mapToParent(self.paraEdit.pos()) + c_pt = self.paraEdit.mapTo(self, rect.bottomLeft()) 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(18, -rect.height() / 2.0) + path.lineTo(0, -rect.height()) path.lineTo(0, 0) - # XXX - Replace the guess with a calculated value - painter.translate(1.0, pos.y() + rect.y() + 12) + painter.translate(1.0, c_pt.y()) painter.fillPath(path, brush) @pyqtSlot() @@ -132,6 +219,9 @@ class EditDialog(QDialog, Ui_Dialog): @pyqtSlot() def defineAction(self) -> None: + # + # Find the word + # cursor = self.paraEdit.textCursor() word = cursor.selectedText() start = cursor.selectionStart() @@ -144,41 +234,54 @@ class EditDialog(QDialog, Ui_Dialog): tmp = start start = end end = tmp - + # + # Find the block + # + textBlock = self.paraEdit.document().findBlock(cursor.position()) + blockNum = textBlock.blockNumber() query = QSqlQuery() query.prepare("SELECT * FROM words WHERE word = :word") query.bindValue(":word", word) if not query.exec(): query_error(query) if query.next(): # we have something - self.defined(query.value("word_id"), start, end) + self.defined(query.value("word_id"), blockNum, start, end) return - dictionary = PyDictionary() - meaning = json.dumps(dictionary.meaning(word)) - por = dictionary.translate(word, "pt") + # + # Get the defintion + # + response = requests.get( + f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}" + ) + if response.status_code != 200: + print(response.content) + return + definitions = json.loads(response.content.decode("utf-8")) + definition = definitions[0] query.prepare( - "INSERT INTO words (word, definition, translation) " - "VALUES (:word, :definition, :translation)" + "INSERT INTO words (word, definition) " "VALUES (:word, :definition)" ) query.bindValue(":word", word) - query.bindValue(":definition", meaning) - query.bindValue(":translation", por) + query.bindValue(":definition", json.dumps(definition)) if not query.exec(): query_error(query) - self.defined(query.lastInsertId(), start, end) + start = start - textBlock.position() + end = end - textBlock.position() + self.defined(query.lastInsertId(), blockNum, start, end) return - def defined(self, word_id: int, start: int, end: int) -> None: + def defined(self, word_id: int, blockNum: int, start: int, end: int) -> None: query = QSqlQuery() query.prepare( - "SELECT * FROM word_paragraph " - "WHERE word_id = :word_id " - "AND paragraph_id = :paragraph_id " + "SELECT * FROM word_block " + "WHERE section_id = :section_id " + "AND block = :block " "AND start = :start " "AND end = :end" ) query.bindValue(":word_id", word_id) - query.bindValue(":paragraph_id", self.paragraph_id) + query.bindValue(":section_id", self.section_id) + query.bindValue(":block", blockNum) query.bindValue(":start", start) query.bindValue(":end", end) if not query.exec(): @@ -186,11 +289,12 @@ class EditDialog(QDialog, Ui_Dialog): if query.next(): return query.prepare( - "INSERT INTO word_paragraph VALUES " - "( :word_id, :paragraph_id, :start, :end)" + "INSERT INTO word_block VALUES " + "( :word_id, :section_id, :block, :start, :end)" ) query.bindValue(":word_id", word_id) - query.bindValue(":paragraph_id", self.paragraph_id) + query.bindValue(":section_id", self.section_id) + query.bindValue(":block", blockNum) query.bindValue(":start", start) query.bindValue(":end", end) if not query.exec(): @@ -198,24 +302,29 @@ class EditDialog(QDialog, Ui_Dialog): def_format = QTextCharFormat() def_format.setFontUnderline(True) cursor = QTextCursor(self.paraEdit.document()) - cursor.setPosition(start, QTextCursor.MoveMode.MoveAnchor) - cursor.setPosition(end, QTextCursor.MoveMode.KeepAnchor) + textBlock = self.paraEdit.document().findBlockByNumber(blockNum) + cursor.setPosition( + start + textBlock.position(), QTextCursor.MoveMode.MoveAnchor + ) + cursor.setPosition(end + textBlock.position(), QTextCursor.MoveMode.KeepAnchor) cursor.setCharFormat(def_format) return def display_definition(self) -> None: cursor = self.paraEdit.textCursor() + textBlock = self.paraEdit.document().findBlock(cursor.position()) + blockNum = textBlock.blockNumber() query = QSqlQuery() query.prepare( - "SELECT w.* FROM word_paragraph wp " + "SELECT w.* FROM word_block wb " "LEFT JOIN words w " - "ON (w.word_id = wp.word_id) " - "WHERE :position BETWEEN wp.start AND wp.end " - "AND wp.paragraph_id = :paragraph_id" + "ON (w.word_id = wb.word_id) " + "WHERE :position BETWEEN wb.start AND wb.end " + "AND wb.block = :block AND wb.section_id = :section_id" ) - query.bindValue(":position", cursor.position()) - query.bindValue(":paragraph_id", self.paragraph_id) - print("display_definition()", cursor.position()) + query.bindValue(":position", cursor.position() - textBlock.position()) + query.bindValue(":block", blockNum) + query.bindValue(":section_id", self.section_id) if not query.exec(): query_error(query) if not query.next(): @@ -237,15 +346,22 @@ class EditDialog(QDialog, Ui_Dialog): word_format.setFontWeight(QFont.Weight.Normal) word_format.setFontPointSize(16) cursor.setCharFormat(word_format) - for key in definition.keys(): + try: + cursor.insertBlock() + cursor.insertText(definition["phonetic"]) + except Exception: + pass + for meaning in definition["meanings"]: cursor.insertList(typeFormat) - cursor.insertText(key) + cursor.insertText(meaning["partOfSpeech"]) cursor.insertList(defFormat) first = True - for a_def in definition[key]: + for a_def in meaning["definitions"]: if not first: cursor.insertBlock() - cursor.insertText(a_def) + cursor.insertText(a_def["definition"]) + # TODO: synonyms + # TODO: antonyms first = False return @@ -257,41 +373,42 @@ class EditDialog(QDialog, Ui_Dialog): self.stackedWidget.setCurrentIndex(1 - idx) return - def nextDefinition(self): + def nextDefinition(self) -> None: cursor = self.paraEdit.textCursor() position = cursor.position() - print(position) - formats = cursor.block().textFormats() + block = cursor.block() found = None - for f in formats: - wc = QTextCursor(cursor) - wc.setPosition(f.start) - wc.movePosition( - QTextCursor.MoveOperation.Right, - QTextCursor.MoveMode.KeepAnchor, - f.length, - ) - word = wc.selectedText() - cf = wc.charFormat() - if f.start <= position: - continue - if not cf.fontUnderline(): - continue - if not found: - found = f - elif f.start < found.start: - found = f - if found: - cursor.setPosition(found.start) - self.paraEdit.setTextCursor(cursor) + while block and not found: + formats = block.textFormats() + for f in formats: + wc = QTextCursor(cursor) + wc.setPosition(f.start) + wc.movePosition( + QTextCursor.MoveOperation.Right, + QTextCursor.MoveMode.KeepAnchor, + f.length, + ) + word = wc.selectedText() + cf = wc.charFormat() + if f.start <= position: + continue + if not cf.fontUnderline(): + continue + if not found: + found = f + elif f.start < found.start: + found = f + if found: + cursor.setPosition(found.start) + self.paraEdit.setTextCursor(cursor) + block = block.next() self.display_definition() return - def scrollTo(self, position): + def scrollTo(self, position: int) -> None: cursor = self.paraEdit.textCursor() cursor.setPosition(position) rect = self.paraEdit.cursorRect(cursor) - print(rect) return def savePosition(self) -> None: diff --git a/main.py b/main.py index d48504f..d07d285 100755 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import os import re import sys +from typing import cast from PyQt6.QtCore import Qt, pyqtSlot from PyQt6.QtGui import ( @@ -42,15 +43,13 @@ class MainWindow(QMainWindow, Ui_MainWindow): model.setQuery(query) self.peopleView.setModel(model) self.peopleView.setModelColumn(1) - # self.load_definition(word, PyDictionary.meaning(word)) - # 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): + def createActions(self) -> None: query = QSqlQuery() query.prepare("SELECT * FROM books ORDER BY title") if not query.exec(): @@ -63,11 +62,9 @@ class MainWindow(QMainWindow, Ui_MainWindow): return @pyqtSlot() - def setBookAction(self): - action = self.sender() - print(action) + def setBookAction(self) -> None: + action = cast(QAction, self.sender()) book_id = action.data() - print(book_id) indexes = self.peopleView.selectedIndexes() if len(indexes) < 1: return @@ -164,8 +161,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): SQL_CMDS = [ "PRAGMA foreign_keys=ON", "CREATE TABLE IF NOT EXISTS words " - "(word_id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT, definition TEXT, " - "translation TEXT)", + "(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)", @@ -236,7 +232,7 @@ def schema_update(db: QSqlDatabase) -> None: 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(", ") + coldefs = re.search(r"\((.+)\)", old).group(1).split(", ") # type: ignore[union-attr] cols = [x.split(" ")[0] for x in coldefs] if not query.exec( "INSERT INTO " diff --git a/ui/EditDialog.py b/ui/EditDialog.py index 15f008b..9928e78 100644 --- a/ui/EditDialog.py +++ b/ui/EditDialog.py @@ -24,7 +24,7 @@ class Ui_Dialog(object): self.paraEdit = QtWidgets.QTextEdit(parent=self.page) font = QtGui.QFont() font.setFamily("OpenDyslexic") - font.setPointSize(16) + font.setPointSize(11) self.paraEdit.setFont(font) self.paraEdit.setObjectName("paraEdit") self.verticalLayout_2.addWidget(self.paraEdit) diff --git a/ui/EditDialog.ui b/ui/EditDialog.ui index e6b68cf..9605f79 100644 --- a/ui/EditDialog.ui +++ b/ui/EditDialog.ui @@ -26,7 +26,7 @@ OpenDyslexic - 16 + 11