Files
esl-reader/lib/read.py
Christopher T. Johnson bb5287743c Add Sound Module
2023-12-19 10:01:09 -05:00

675 lines
22 KiB
Python

import json
import re
from typing import cast
import requests
from PyQt6.QtCore import (
QFile,
QIODeviceBase,
QPoint,
QRect,
QResource,
Qt,
QTimer,
QUrl,
pyqtSignal,
pyqtSlot,
)
from PyQt6.QtGui import (
QBrush,
QColor,
QFont,
QKeyEvent,
QMouseEvent,
QPainter,
QPainterPath,
QPaintEvent,
QTextBlockFormat,
QTextCharFormat,
QTextCursor,
QTextDocument,
QTextListFormat,
)
from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
from PyQt6.QtWidgets import QDialog, QPushButton
from main import query_error
from ui.EditDialog import Ui_Dialog
class EditDialog(QDialog, Ui_Dialog):
block: int
paragraphs = True
sessionSignal = pyqtSignal()
displayedWord = pyqtSignal(int)
newParagraph = pyqtSignal(int, int)
def __init__(self, parent, session, person_id: int) -> None:
self.session = session
super(EditDialog, self).__init__(parent)
print(self.parent())
self.person_id = person_id
styleSheet = QResource(":/display.css").data().decode("utf-8")
self.setupUi(self)
#
# Override UI
#
#
# End overrides
#
self.load_book(self.person_id)
blockNumber = self.block
self.paraEdit.setReadOnly(True)
self.paraEdit.document().setDefaultStyleSheet(styleSheet)
self.defEdit.setReadOnly(True)
self.defEdit.document().setDefaultStyleSheet(styleSheet)
self.show_section(self.section_id)
self.block = blockNumber
self.savePosition()
self.stackedWidget.setCurrentIndex(0)
#
# Connect widgets
#
self.defineBtn.clicked.connect(self.defineAction)
self.playBtn.clicked.connect(self.playAction)
self.scrollBtn.clicked.connect(self.scrollAction)
self.nextBtn.clicked.connect(self.nextAction)
self.prevBtn.clicked.connect(self.prevAction)
# self.sessionSignal.connect(self.session.timerAction)
# self.sessionBtn.clicked.connect(self.sessionAction)
self.sessionBtn.clicked.connect(self.session.timerAction)
self.paraEdit.verticalScrollBar().valueChanged.connect(self.scrollSlot)
self.defEdit.selectionChanged.connect(self.recursiveDef)
#
# Connect signals
#
self.displayedWord.connect(self.session.addWord)
self.newParagraph.connect(self.session.addBlock)
return
#
# Events
#
#
# slots
#
@pyqtSlot()
def recursiveDef(self):
cursor = self.defEdit.textCursor()
selection = cursor.selectedText().strip()
if len(selection) <= 0:
return
query = QSqlQuery()
query.prepare("SELECT * FROM words " "WHERE word = :word")
query.bindValue(":word", selection)
if not query.exec():
query_error()
if not query.next():
response = requests.get(
f"https://api.dictionaryapi.dev/api/v2/entries/en/{selection}"
)
if response.status_code != 200:
return
definitions = json.loads(response.content.decode("utf-8"))
definition = definitions[0]
word_id = None
else:
definition = query.value("definition")
word_id = query.value("word_id")
self.setDefEdit(selection, word_id, definition)
return
@pyqtSlot()
def sessionAction(self) -> None:
self.sessionSignal.emit()
self.session.addParagraph(self.section_id, self.block)
return
@pyqtSlot()
def playAction(self) -> None:
print("playAction")
idx = self.stackedWidget.currentIndex()
if idx == 0: # Reading
# find word
cursor = self.paraEdit.textCursor()
fmt = cursor.charFormat()
if not fmt.fontUnderline():
self.addWord()
cursor = self.paraEdit.textCursor()
textBlock = self.paraEdit.document().findBlock(cursor.position())
blockNum = textBlock.blockNumber()
query = QSqlQuery()
query.prepare(
"SELECT w.* FROM word_block wb "
"LEFT JOIN words w "
"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() - textBlock.position()
)
query.bindValue(":block", blockNum)
query.bindValue(":section_id", self.section_id)
if not query.exec():
query_error(query)
if not query.next():
return
data = json.loads(query.value("definition"))
if "phonetics" in data:
self.phonetics = data["phonetics"]
else:
self.phonetics = None
print("Checking for phonetics")
if not self.phonetics:
return
print("Looking for audio")
for entry in self.phonetics:
if len(entry["audio"]) > 0:
# self.parent().playAlert.emit()
self.parent().playSound.emit(entry["audio"])
return
@pyqtSlot()
def scrollAction(self) -> None:
position = (
self.paraEdit.document().findBlockByNumber(self.block).position()
)
cursor = self.paraEdit.textCursor()
cursor.setPosition(position)
pos = self.paraEdit.mapTo(
self, self.paraEdit.cursorRect(cursor).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
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: int) -> None:
self.update()
return
@pyqtSlot()
def nextAction(self) -> None:
if self.stackedWidget.currentIndex() == 1:
print("Next Definition")
self.nextDefinition()
elif self.paragraphs:
self.nextParagraph()
else:
self.nextSection()
return
@pyqtSlot()
def prevAction(self) -> None:
if self.stackedWidget.currentIndex() == 1:
print("Previous Definition")
self.prevDefinition()
elif self.paragraphs:
self.prevParagraph()
else:
self.prevSection()
return
@pyqtSlot()
def defineAction(self) -> None:
if self.paragraphs:
self.showDefinition()
else:
self.addWord()
return
def defToHtml(self, word: str, definition) -> str:
html = f'<h1 class="def-word">{word}</h1>' + "\n"
try:
words = []
for phonetic in definition["phonetics"]:
if phonetic["text"] in words:
continue
words.append(phonetic["text"])
html += f'<p class="phonetic">{phonetic["text"]}</p>' + "\n"
except Exception:
pass
html += '<ul class="outer">' + "\n"
for meaning in definition["meanings"]:
html += f"<li>{meaning['partOfSpeech']}"
html += '<ul class="inner">'
for a_def in meaning["definitions"]:
html += f"<li>{a_def['definition']}</li>\n"
html += "</ul>\n"
html += "</ul>\n<p/>\n"
return html
def load_book(self, person_id: int) -> None:
query = QSqlQuery()
query.prepare(
"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(":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)
while query.next():
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: int, start: bool = True) -> None:
sequence = self.section_map[section_id]
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()
#
# Mark all the defined words with underlines
#
def_format = QTextCharFormat()
def_format.setFontUnderline(True)
cursor = QTextCursor(self.paraEdit.document())
query = QSqlQuery()
query.prepare(
"SELECT wb.*,w.word,w.definition FROM word_block wb "
"LEFT JOIN words w "
"ON (w.word_id = wb.word_id) "
"WHERE wb.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
)
word = query.value("word")
definition = json.loads(query.value("definition"))
try:
phonetic = definition["phonetic"]
def_format.setToolTip(
f'<font size="24">{word}:<br/><font family="Tex Gyre Heros">{phonetic}</font></font>'
)
cursor.mergeCharFormat(def_format)
except:
pass
return
#
# Event handlers
#
def keyReleaseEvent(self, event: QKeyEvent) -> None:
self.nextBtn.setText("Next Para")
self.prevBtn.setText("Prev Para")
self.defineBtn.setText("Show Def")
self.paragraphs = True
super().keyReleaseEvent(event)
return
def keyPressEvent(self, event: QKeyEvent) -> None:
self.nextBtn.setText("Next Sect")
self.prevBtn.setText("Prev Sect")
self.defineBtn.setText("Add Def")
self.paragraphs = False
super().keyPressEvent(event)
return
def paintEvent(self, e: QPaintEvent | None) -> None:
idx = self.stackedWidget.currentIndex()
if idx > 0:
return
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)
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(18, -rect.height() / 2.0)
path.lineTo(0, -rect.height())
path.lineTo(0, 0)
painter.translate(1.0, c_pt.y())
painter.fillPath(path, brush)
return
def nextParagraph(self) -> None:
self.block += 1
if self.block >= self.paraEdit.document().blockCount():
self.nextSection()
return
self.savePosition()
self.newParagraph.emit(self.section_id, self.block)
return
def prevParagraph(self) -> None:
self.block -= 1
if self.block < 0:
self.prevSection()
return
self.savePosition()
return
def addWord(self) -> None:
#
# Find the word
#
cursor = self.paraEdit.textCursor()
word = cursor.selectedText()
start = cursor.selectionStart()
end = cursor.selectionEnd()
if start != end:
word = word.strip()
if len(word) == 0 or word.find(" ") >= 0:
cursor.select(QTextCursor.SelectionType.WordUnderCursor)
word = cursor.selectedText()
word = word.strip()
start = cursor.selectionStart()
end = cursor.selectionEnd()
if start > end:
tmp = start
start = end
end = tmp
#
# Find the block
#
textBlock = self.paraEdit.document().findBlock(cursor.position())
blockNum = textBlock.blockNumber()
start = start - textBlock.position()
end = end - textBlock.position()
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"), blockNum, start, end)
return
#
# Get the defintion
#
response = requests.get(
f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
)
if response.status_code != 200:
print(f"{word}: {response.content}")
return
definitions = json.loads(response.content.decode("utf-8"))
definition = definitions[0]
query.prepare(
"INSERT INTO words (word, definition) "
"VALUES (:word, :definition)"
)
query.bindValue(":word", word)
query.bindValue(":definition", json.dumps(definition))
if not query.exec():
query_error(query)
self.defined(query.lastInsertId(), blockNum, start, end)
return
def defined(
self, word_id: int, blockNum: int, start: int, end: int
) -> None:
query = QSqlQuery()
query.prepare(
"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(":section_id", self.section_id)
query.bindValue(":block", blockNum)
query.bindValue(":start", start)
query.bindValue(":end", end)
if not query.exec():
query_error(query)
if not query.next():
query.prepare(
"INSERT INTO word_block VALUES "
"( :word_id, :section_id, :block, :start, :end)"
)
query.bindValue(":word_id", word_id)
query.bindValue(":section_id", self.section_id)
query.bindValue(":block", blockNum)
query.bindValue(":start", start)
query.bindValue(":end", end)
if not query.exec():
query_error(query)
def_format = QTextCharFormat()
def_format.setFontUnderline(True)
cursor = QTextCursor(self.paraEdit.document())
textBlock = self.paraEdit.document().findBlockByNumber(blockNum)
cursor.setPosition(
start + textBlock.position(), QTextCursor.MoveMode.MoveAnchor
)
cursor.setPosition(
end + textBlock.position(), QTextCursor.MoveMode.KeepAnchor
)
cursor.mergeCharFormat(def_format)
return
# XXX - rename
def display_definition(self) -> None:
cursor = self.paraEdit.textCursor()
fmt = cursor.charFormat()
if not fmt.fontUnderline():
self.addWord()
cursor = self.paraEdit.textCursor()
textBlock = self.paraEdit.document().findBlock(cursor.position())
blockNum = textBlock.blockNumber()
query = QSqlQuery()
query.prepare(
"SELECT w.* FROM word_block wb "
"LEFT JOIN words w "
"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() - textBlock.position())
query.bindValue(":block", blockNum)
query.bindValue(":section_id", self.section_id)
if not query.exec():
query_error(query)
if not query.next():
return
word = query.value("word")
definition = json.loads(query.value("definition"))
self.setDefEdit(word, query.value("word_id"), definition)
return
def setDefEdit(self, word, word_id, definition):
if "phonetics" in definition:
self.phonetics = definition["phonetics"]
else:
self.phonetics = None
self.defEdit.document().clear()
cursor = self.defEdit.textCursor()
cursor.insertHtml(self.defToHtml(word, definition))
cursor.setPosition(0)
self.defEdit.setTextCursor(cursor)
if word_id:
self.displayedWord.emit(word_id)
return
def showDefinition(self) -> None:
idx = self.stackedWidget.currentIndex()
if idx == 0:
self.defineBtn.setText("Read")
self.nextBtn.setText("Next Def")
self.prevBtn.setText("Prev Def")
self.display_definition()
else:
self.defineBtn.setText("Definition")
if self.paragraphs:
self.nextBtn.setText("Next Para")
self.prevBtn.setText("Prev Para")
else:
self.nextBtn.setText("Next Sect")
self.prevBtn.setText("Prev Sect")
self.stackedWidget.setCurrentIndex(1 - idx)
self.update()
return
def nextDefinition(self) -> None:
cursor = self.paraEdit.textCursor()
formats = self.paraEdit.document().allFormats()
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)
self.display_definition()
return
def scrollTo(self, position: int) -> None:
cursor = self.paraEdit.textCursor()
cursor.setPosition(position)
rect = self.paraEdit.cursorRect(cursor)
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
def nextSection(self) -> None:
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
def prevSection(self) -> None:
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