diff --git a/lib/__init__.py b/lib/__init__.py index 8cbe3d7..3056e3a 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -2,3 +2,4 @@ from .books import Book from .person import PersonDialog from .read import EditDialog from .session import SessionDialog +from .sounds import SoundOff diff --git a/lib/read.py b/lib/read.py index 225af60..f17d455 100644 --- a/lib/read.py +++ b/lib/read.py @@ -1,5 +1,4 @@ import json -import os import re from typing import cast @@ -31,7 +30,6 @@ from PyQt6.QtGui import ( QTextDocument, QTextListFormat, ) -from PyQt6.QtMultimedia import QAudioOutput, QMediaDevices, QMediaPlayer from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel from PyQt6.QtWidgets import QDialog, QPushButton @@ -45,16 +43,12 @@ class EditDialog(QDialog, Ui_Dialog): sessionSignal = pyqtSignal() displayedWord = pyqtSignal(int) newParagraph = pyqtSignal(int, int) - soundEffect = QMediaPlayer() - def __init__(self, session, person_id: int) -> None: + def __init__(self, parent, session, person_id: int) -> None: self.session = session - super(EditDialog, self).__init__() + super(EditDialog, self).__init__(parent) + print(self.parent()) self.person_id = person_id - if not QResource.registerResource( - os.path.join(os.path.dirname(__file__), "../ui/resources.rcc"), "/" - ): - raise Exception("Unable to register resources.rcc") styleSheet = QResource(":/display.css").data().decode("utf-8") self.setupUi(self) # @@ -63,16 +57,6 @@ class EditDialog(QDialog, Ui_Dialog): # # End overrides # - audioOutput = QAudioOutput() - dev = None - for output in QMediaDevices.audioOutputs(): - if output.id().data().decode("UTF-8") == "virt-input": - dev = output - break - if dev: - audioOutput.setDevice(dev) - self.audioOutput = audioOutput - self.soundEffect.setAudioOutput(audioOutput) self.load_book(self.person_id) blockNumber = self.block self.paraEdit.setReadOnly(True) @@ -101,10 +85,6 @@ class EditDialog(QDialog, Ui_Dialog): # self.displayedWord.connect(self.session.addWord) self.newParagraph.connect(self.session.addBlock) - - self.soundEffect.errorOccurred.connect(self.mediaError) - self.soundEffect.playbackStateChanged.connect(self.changedState) - self.soundEffect.mediaStatusChanged.connect(self.changedStatus) return # @@ -140,33 +120,6 @@ class EditDialog(QDialog, Ui_Dialog): self.setDefEdit(selection, word_id, definition) return - @pyqtSlot(QMediaPlayer.MediaStatus) - def changedStatus(self, status): - if status == QMediaPlayer.MediaStatus.LoadedMedia: - self.soundEffect.play() - audioOutput = self.soundEffect.audioOutput() - if not audioOutput: - self.soundEffect.setAudioOutput(self.audioOutput) - audioOutput = self.audioOutput - audioDevice = audioOutput.device() - print(status) - return - - @pyqtSlot(QMediaPlayer.PlaybackState) - def changedState(self, status): - audioOutput = self.soundEffect.audioOutput() - if not audioOutput: - return - audioDevice = audioOutput.device() - print(status) - return - - @pyqtSlot(QMediaPlayer.Error, str) - def mediaError(self, error, string): - print(error) - print(string) - return - @pyqtSlot() def sessionAction(self) -> None: self.sessionSignal.emit() @@ -214,13 +167,8 @@ class EditDialog(QDialog, Ui_Dialog): print("Looking for audio") for entry in self.phonetics: if len(entry["audio"]) > 0: - self.soundEffect.setSource(QUrl(entry["audio"])) - if ( - self.soundEffect.mediaStatus() - == QMediaPlayer.MediaStatus.LoadedMedia - ): - self.soundEffect.play() - return + # self.parent().playAlert.emit() + self.parent().playSound.emit(entry["audio"]) return @pyqtSlot() diff --git a/lib/session.py b/lib/session.py index 5fff91a..d3b0642 100644 --- a/lib/session.py +++ b/lib/session.py @@ -29,6 +29,7 @@ class SessionDialog(QDialog, Ui_Dialog): sessionStart = None sessionEnd = None blocks = QStandardItemModel() + session_id = -1 def __init__(self) -> None: super(SessionDialog, self).__init__() @@ -112,41 +113,14 @@ class SessionDialog(QDialog, Ui_Dialog): self.sessionEnd = datetime.now() query = QSqlQuery() query.prepare( - "INSERT INTO sessions " - "(person_id, start, stop, notes) " - "VALUES (:person_id, :start, :stop, :notes)" + "UPDATE sessions " + "SET start=:start , SET stop=:stop, SET notes=:notes " + "WHERE sesion_id = :session_id" ) - query.bindValue(":person_id", self.person_id) + query.bindValue(":session_id", self.session_id) query.bindValue(":start", self.sessionStart.isoformat()) query.bindValue(":stop", self.sessionEnd.isoformat()) query.bindValue(":notes", self.textEdit.toPlainText()) - if not query.exec(): - query_error(query) - session_id = query.lastInsertId() - model = self.wordView.model() - sql = "INSERT INTO session_word (session_id,word_id, important) VALUES " - parameters = ["(?,?,?)" for x in range(model.rowCount())] - sql += ", ".join(parameters) - query.prepare(sql) - for row in range(model.rowCount()): - query.addBindValue(session_id) - query.addBindValue(model.item(row).data(SessionDialog.WordIdRole)) - query.addBindValue( - model.item(row).data(SessionDialog.WordImportantRole) - ) - if not query.exec(): - query_error(query) - sql = ( - "INSERT INTO session_block (session_id, section_id, block) VALUES " - ) - parameters = ["(?,?,?)" for x in range(self.blocks.rowCount())] - sql += ",".join(parameters) - query.prepare(sql) - for row in range(self.blocks.rowCount()): - query.addBindValue(session_id) - item = self.blocks.item(row) - query.addBindValue(item.data(SessionDialog.SectionIdRole)) - query.addBindValue(item.data(SessionDialog.BlockRole)) if not query.exec(): query_error(query) super().accept() @@ -157,6 +131,19 @@ class SessionDialog(QDialog, Ui_Dialog): if state: if not self.sessionStart: self.sessionStart = datetime.now() + if self.session_id <= 0: + query = QSqlQuery() + query.prepare( + "INSERT INTO sessions " + "(person_id, start) " + "VALUES (:person_id, :start)" + ) + query.bindValue(":person_id", self.person_id) + query.bindValue(":start", self.sessionStart.isoformat()) + if not query.exec(): + query_error(query) + self.session_id = query.lastInsertId() + self.startTime = datetime.now() self.timer.start() else: @@ -197,13 +184,13 @@ class SessionDialog(QDialog, Ui_Dialog): raise Exception(f"Word_id({word_id}) not found in DB") word = QStandardItem() word.setData(query.value("word"), Qt.ItemDataRole.DisplayRole) - word.setData(query.value("word_id"), SessionDialog.WordIdRole) + word.setData(word_id, SessionDialog.WordIdRole) word.setData(0, SessionDialog.WordImportantRole) model = self.wordView.model() matches = model.match( model.createIndex(0, 0), SessionDialog.WordIdRole, - query.value("word_id"), + word_id, 1, Qt.MatchFlag.MatchExactly, ) @@ -211,6 +198,15 @@ class SessionDialog(QDialog, Ui_Dialog): return self.wordView.model().appendRow(word) self.wordView.model().sort(0) + query.prepare( + "INSERT INTO session_word " + "(session_id, word_id, important) " + "VALUES (:session_id, :word_id, 0)" + ) + query.bindValue(":session_id", self.session_id) + query.bindValue(":word_id", word_id) + if not query.exec(): + query_error(query) else: print(f"Not active: {word_id}") return @@ -255,4 +251,14 @@ class SessionDialog(QDialog, Ui_Dialog): ) self.textBrowser.textCursor().insertBlock() self.textBrowser.ensureCursorVisible() + query.prepare( + "INSERT INTO session_block " + "(session_id, section_id, block) " + "VALUES (:session_id, :section_id, :block)" + ) + query.bindValue(":session_id", self.session_id) + query.bindValue(":section_id", section_id) + query.bindValue(":block", block) + if not query.exec(): + query_error(query) return diff --git a/lib/sounds.py b/lib/sounds.py new file mode 100644 index 0000000..bd37cfe --- /dev/null +++ b/lib/sounds.py @@ -0,0 +1,108 @@ +from PyQt6.QtCore import QObject, Qt, QUrl, pyqtSlot +from PyQt6.QtMultimedia import ( + QAudioDevice, + QAudioOutput, + QMediaDevices, + QMediaPlayer, + QSoundEffect, +) + +# from PyQt6.QtWidgets import QWidget + + +class SoundOff(QObject): + def __init__(self): + super().__init__() + # + # Setup devices + # + self.virtualDevice = None + dev = None + for output in QMediaDevices.audioOutputs(): + if output.id().data().decode("utf-8") == "virt-input": + self.virtualDevice = output + if output.isDefault(): + self.localDevice = output + + self.alertEffect = QSoundEffect() + self.alertEffect.setSource(QUrl("qrc:/beep.wav")) + self.alertEffect.setAudioDevice(self.localDevice) + self.alertEffect.setVolume(0.25) + self.alertEffect.setLoopCount(1) + + self.localPlayer = QMediaPlayer() + self.localPlayer.setObjectName("localPlayer") + self.localOutput = QAudioOutput() + self.localOutput.setDevice(self.localDevice) + self.localPlayer.setAudioOutput(self.localOutput) + if self.virtualDevice: + self.virtualPlayer = QMediaPlayer() + self.virtualPlayer.setObjectName("virtualPlayer") + self.virtualOutput = QAudioOutput() + self.virtualOutput.setDevice(self.virtualDevice) + self.virtualPlayer.setAudioOutput(self.virtualOutput) + # + # Connections + # + self.localPlayer.errorOccurred.connect(self.mediaError) + self.localPlayer.mediaStatusChanged.connect(self.mediaStatus) + self.localPlayer.playbackStateChanged.connect(self.playbackState) + if self.virtualDevice: + self.virtualPlayer.errorOccurred.connect(self.mediaError) + self.virtualPlayer.mediaStatusChanged.connect(self.mediaStatus) + self.virtualPlayer.playbackStateChanged.connect(self.playbackState) + + @pyqtSlot() + def alert(self): + self.alertEffect.play() + return + + @pyqtSlot(QMediaPlayer.Error, str) + def mediaError(self, error, string): + print(error) + print(str) + return + + @pyqtSlot(QMediaPlayer.MediaStatus) + def mediaStatus(self, status): + if status == QMediaPlayer.MediaStatus.LoadedMedia: + self.sender().play() + return + + @pyqtSlot(QMediaPlayer.PlaybackState) + def playbackState(self, state): + return + + # + # Communications slots + # + @pyqtSlot() + def soundAlert(self): + self.alertEffect.play() + return + + @pyqtSlot(str) + def playSound(self, url): + src = QUrl(url) + if not self.localPlayer.audioOutput(): + self.localPlayer.setAudioOutput(self.localOutput) + self.localPlayer.setSource(src) + self.localPlayer.setPosition(0) + if ( + self.localPlayer.mediaStatus() + == QMediaPlayer.MediaStatus.LoadedMedia + ): + self.localPlayer.play() + if not self.virtualDevice: + return + return + self.virtualPlayer.setSource(src) + self.virtualPlayer.setPosition(0) + if not self.virtualPlayer.audioOutput(): + self.virtualPlayer.setAudioOutput(self.virtualOutput) + if ( + self.virtualPlayer.mediaStatus() + == QMediaPlayer.MediaStatus.LoadedMedia + ): + self.virtualPlayer.play() + return diff --git a/main.py b/main.py index 071108f..48f59b2 100755 --- a/main.py +++ b/main.py @@ -1,15 +1,9 @@ #!/usr/bin/env python3 # # TODO: -# Record all words examined # # Add definition to definition -# Follow definition links -# Print subset of words, limit to words from this session's paragraphs -# plus defined during session -# Add Note per session # Add book import dialog -# Add person create/edit dialog # Reading scroll with speed control # Move controls out of reading window. # Ability to edit text with updates to word-section links @@ -24,7 +18,14 @@ import sys from datetime import datetime, timedelta from typing import cast -from PyQt6.QtCore import QModelIndex, Qt, QTimer, pyqtSignal, pyqtSlot +from PyQt6.QtCore import ( + QModelIndex, + QResource, + Qt, + QTimer, + pyqtSignal, + pyqtSlot, +) from PyQt6.QtGui import ( QAction, QFont, @@ -59,10 +60,21 @@ def query_error(query: QSqlQuery) -> None: class MainWindow(QMainWindow, Ui_MainWindow): + playAlert = pyqtSignal() + playSound = pyqtSignal(str) + def __init__(self) -> None: super(MainWindow, self).__init__() self.setupUi(self) - # model = ModelOverride() + # + # Setup resources + # + if not QResource.registerResource( + os.path.join(os.path.dirname(__file__), "ui/resources.rcc"), "/" + ): + raise Exception("Unable to register resources.rcc") + self.soundOff = SoundOff() + model = QSqlQueryModel() query = QSqlQuery("SELECT * FROM people ORDER BY name") model.setQuery(query) @@ -86,6 +98,8 @@ class MainWindow(QMainWindow, Ui_MainWindow): ) # Y self.peopleView.doubleClicked.connect(self.editPerson) # Y self.peopleView.clicked.connect(self.selectedPerson) # Y + self.playAlert.connect(self.soundOff.alert) + self.playSound.connect(self.soundOff.playSound) self.show() return @@ -156,7 +170,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.session.show() self.session.raise_() self.setPerson.emit(person_id) - self.dlg = EditDialog(self.session, person_id) + self.dlg = EditDialog(self, self.session, person_id) self.dlg.show() self.dlg.raise_() return diff --git a/ui/resources.qrc b/ui/resources.qrc index 5216053..a34d8e0 100644 --- a/ui/resources.qrc +++ b/ui/resources.qrc @@ -3,6 +3,7 @@ print.css display.css email.css + beep.wav opendyslexic/OpenDyslexic-Regular.otf diff --git a/ui/resources.rcc b/ui/resources.rcc index 374d35d..d584d54 100644 Binary files a/ui/resources.rcc and b/ui/resources.rcc differ