From 2baffbf3a394528bce494a7886a6ada64de9faee Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Wed, 14 Feb 2024 09:00:32 +0200 Subject: [PATCH 01/51] Refactoring: create Verse class and extract classes from MainWindow --- CMakeLists.txt | 12 +- resources/dark/dark.qss | 2 +- resources/light/light.qss | 2 +- resources/light/sepia.qss | 2 +- src/core/bookmarksdialog.cpp | 24 +- src/core/bookmarksdialog.h | 1 + src/core/copydialog.cpp | 17 +- src/core/copydialog.h | 7 +- src/core/khatmahdialog.cpp | 33 +- src/core/khatmahdialog.h | 5 +- src/core/mainwindow.cpp | 1112 +++-------------- src/core/mainwindow.h | 334 +---- src/core/mainwindow.ui | 594 +-------- src/core/playercontrols.cpp | 195 +++ src/core/playercontrols.h | 111 ++ src/core/playercontrols.ui | 317 +++++ src/core/quranreader.cpp | 582 +++++++++ src/core/quranreader.h | 248 ++++ src/core/quranreader.ui | 210 ++++ src/core/searchdialog.cpp | 26 +- src/core/searchdialog.h | 1 + src/core/tafsirdialog.cpp | 45 +- src/core/tafsirdialog.h | 1 + src/core/tafsirdialog.ui | 21 +- src/globals.h | 35 - src/utils/dbmanager.cpp | 101 +- src/utils/dbmanager.h | 38 +- ...notificationmanager.cpp => systemtray.cpp} | 33 +- .../{notificationmanager.h => systemtray.h} | 14 +- src/utils/verse.cpp | 172 +++ src/utils/verse.h | 57 + src/utils/verseplayer.cpp | 35 +- src/utils/verseplayer.h | 22 +- src/widgets/notificationpopup.cpp | 2 +- src/widgets/notificationpopup.h | 2 +- src/widgets/versedialog.cpp | 18 +- src/widgets/versedialog.h | 1 + 37 files changed, 2331 insertions(+), 2101 deletions(-) create mode 100644 src/core/playercontrols.cpp create mode 100644 src/core/playercontrols.h create mode 100644 src/core/playercontrols.ui create mode 100644 src/core/quranreader.cpp create mode 100644 src/core/quranreader.h create mode 100644 src/core/quranreader.ui rename src/utils/{notificationmanager.cpp => systemtray.cpp} (56%) rename src/utils/{notificationmanager.h => systemtray.h} (86%) create mode 100644 src/utils/verse.cpp create mode 100644 src/utils/verse.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 37645d5a..7aac7449 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,12 @@ set(PROJECT_SOURCES src/core/mainwindow.cpp src/core/mainwindow.h src/core/mainwindow.ui + src/core/quranreader.h + src/core/quranreader.cpp + src/core/quranreader.ui + src/core/playercontrols.h + src/core/playercontrols.cpp + src/core/playercontrols.ui src/core/searchdialog.h src/core/searchdialog.cpp src/core/searchdialog.ui @@ -79,6 +85,8 @@ set(PROJECT_SOURCES src/widgets/versedialog.h src/widgets/versedialog.cpp src/widgets/versedialog.ui + src/utils/verse.h + src/utils/verse.cpp src/utils/shortcuthandler.h src/utils/shortcuthandler.cpp src/utils/dbmanager.h @@ -87,8 +95,8 @@ set(PROJECT_SOURCES src/utils/verseplayer.cpp src/utils/downloadmanager.h src/utils/downloadmanager.cpp - src/utils/notificationmanager.h - src/utils/notificationmanager.cpp + src/utils/systemtray.h + src/utils/systemtray.cpp src/utils/logger.h src/utils/logger.cpp resources.qrc diff --git a/resources/dark/dark.qss b/resources/dark/dark.qss index e04d7aff..479bcdc5 100644 --- a/resources/dark/dark.qss +++ b/resources/dark/dark.qss @@ -78,7 +78,7 @@ QAbstractScrollArea#tedTafsir { /* --------- Player Styles --------- */ -QFrame#framePlayer { +QWidget#PlayerControls QFrame#playerFrame { background-color: #202020; border-radius: 8px; } diff --git a/resources/light/light.qss b/resources/light/light.qss index df428ee1..ba262853 100644 --- a/resources/light/light.qss +++ b/resources/light/light.qss @@ -79,7 +79,7 @@ QTextEdit#tedTafsir { /* --------- Player Styles --------- */ -QFrame#framePlayer { +QWidget#PlayerControls QFrame#playerFrame { background-color: #fafafa; border-radius: 8px; border: 1px solid #e3e3e3; diff --git a/resources/light/sepia.qss b/resources/light/sepia.qss index a766d56f..caac9edb 100644 --- a/resources/light/sepia.qss +++ b/resources/light/sepia.qss @@ -75,7 +75,7 @@ QTextEdit#tedTafsir { /* --------- Player Styles --------- */ -QFrame#framePlayer { +QWidget#PlayerControls QFrame#playerFrame { background-color: #f7ebd5; border-radius: 8px; border: 1px solid #d9cdb8; diff --git a/src/core/bookmarksdialog.cpp b/src/core/bookmarksdialog.cpp index 780bdcfa..bea4682e 100644 --- a/src/core/bookmarksdialog.cpp +++ b/src/core/bookmarksdialog.cpp @@ -79,7 +79,7 @@ BookmarksDialog::loadBookmarks(int surah) { if (m_shownSurah != surah) { m_shownSurah = surah; - m_shownVerses = m_dbMgr->bookmarkedVerses(surah); + m_shownVerses = Verse::fromList(m_dbMgr->bookmarkedVerses(surah)); if (m_shownSurah == -1) m_allBookmarked = m_shownVerses; } @@ -102,9 +102,9 @@ BookmarksDialog::loadBookmarks(int surah) } for (int i = m_startIdx; i < end; i++) { - Verse verse = m_shownVerses.at(i); + const Verse& verse = m_shownVerses.at(i); QString fontName = - Globals::verseFontname(m_dbMgr->getVerseType(), verse.page); + Globals::verseFontname(m_dbMgr->getVerseType(), verse.page()); QFrame* frame = new QFrame(ui->scrlBookmarks); frame->setProperty("bookmark", true); @@ -128,13 +128,13 @@ BookmarksDialog::loadBookmarks(int surah) removeFromFav, &QPushButton::clicked, this, &BookmarksDialog::btnRemove); QString info = tr("Surah: ") + - m_dbMgr->surahNameList().at(verse.surah - 1) + " - " + - tr("Verse: ") + QString::number(verse.number); + m_dbMgr->surahNameList().at(verse.surah() - 1) + " - " + + tr("Verse: ") + QString::number(verse.number()); lbMeta->setText(info); lbMeta->setAlignment(Qt::AlignLeft); verseLb->setFont(QFont(fontName, 15)); - verseLb->setText(m_dbMgr->getVerseGlyphs(verse.surah, verse.number)); + verseLb->setText(m_dbMgr->getVerseGlyphs(verse.surah(), verse.number())); verseLb->setAlignment(Qt::AlignLeft); verseLb->setWordWrap(true); verseLb->setMargin(5); @@ -154,9 +154,9 @@ BookmarksDialog::loadBookmarks(int surah) frmLayout->addItem(lbLayout); frame->setLayout(frmLayout); - frame->setObjectName(QString::number(verse.page) + '-' + - QString::number(verse.surah) + '-' + - QString::number(verse.number)); + frame->setObjectName(QString::number(verse.page()) + '-' + + QString::number(verse.surah()) + '-' + + QString::number(verse.number())); ui->layoutFavorites->addWidget(frame); m_frames.append(frame); @@ -175,7 +175,7 @@ BookmarksDialog::loadSurahs() std::set surahs; foreach (const Verse& v, m_allBookmarked) { - surahs.insert(v.surah); + surahs.insert(v.surah()); } for (int s : surahs) { @@ -210,7 +210,7 @@ BookmarksDialog::btnGoToVerse() { QStringList info = sender()->parent()->objectName().split('-'); Verse verse{ info.at(0).toInt(), info.at(1).toInt(), info.at(2).toInt() }; - emit navigateToVerse(verse); + emit navigateToVerse(verse.toList()); } void @@ -219,7 +219,7 @@ BookmarksDialog::btnRemove() QStringList info = sender()->parent()->objectName().split('-'); Verse verse{ info.at(0).toInt(), info.at(1).toInt(), info.at(2).toInt() }; - if (m_dbMgr->removeBookmark(verse)) { + if (m_dbMgr->removeBookmark(verse.toList())) { QFrame* frm = qobject_cast(sender()->parent()); int idx = m_frames.indexOf(frm); if (idx != -1) diff --git a/src/core/bookmarksdialog.h b/src/core/bookmarksdialog.h index 58fe2838..bcf7ecbc 100644 --- a/src/core/bookmarksdialog.h +++ b/src/core/bookmarksdialog.h @@ -8,6 +8,7 @@ #include "../globals.h" #include "../utils/dbmanager.h" +#include "../utils/verse.h" #include #include #include diff --git a/src/core/copydialog.cpp b/src/core/copydialog.cpp index d57b425a..ff359fad 100644 --- a/src/core/copydialog.cpp +++ b/src/core/copydialog.cpp @@ -13,23 +13,22 @@ CopyDialog::CopyDialog(QWidget* parent) } void -CopyDialog::show(const Verse& curr) +CopyDialog::show() { ui->cmbCopyFrom->clear(); ui->cmbCopyTo->clear(); - ui->lbCopySurahName->setText(m_dbMgr->getSurahName(curr.surah)); + ui->lbCopySurahName->setText(m_dbMgr->getSurahName(m_currVerse->surah())); - m_surah = curr.surah; - m_surahCnt = m_dbMgr->getSurahVerseCount(curr.surah); - for (int i = 1; i <= m_surahCnt; i++) { + m_surah = m_currVerse->surah(); + for (int i = 1; i <= m_currVerse->surahCount(); i++) { ui->cmbCopyFrom->addItem(QString::number(i)); ui->cmbCopyTo->addItem(QString::number(i)); } - int idx = curr.number ? curr.number - 1 : 0; + int idx = m_currVerse->number() ? m_currVerse->number() - 1 : 0; ui->cmbCopyFrom->setCurrentIndex(idx); ui->cmbCopyTo->setCurrentIndex(idx); m_verseValidator->setBottom(1); - m_verseValidator->setTop(m_surahCnt); + m_verseValidator->setTop(m_currVerse->surahCount()); QDialog::show(); } @@ -39,8 +38,8 @@ CopyDialog::copyRange() { int from = ui->cmbCopyFrom->currentText().toInt(); int to = ui->cmbCopyTo->currentText().toInt(); - if (to < from || from <= 0 || from > m_surahCnt || to <= 0 || - to > m_surahCnt) { + if (to < from || from <= 0 || from > m_currVerse->surahCount() || to <= 0 || + to > m_currVerse->surahCount()) { QMessageBox::warning( this, tr("Invalid range"), tr("The entered verse range is invalid")); return; diff --git a/src/core/copydialog.h b/src/core/copydialog.h index 7fede8d0..160e1091 100644 --- a/src/core/copydialog.h +++ b/src/core/copydialog.h @@ -3,6 +3,7 @@ #include "../globals.h" #include "../utils/dbmanager.h" +#include "../utils/verse.h" #include #include #include @@ -18,18 +19,20 @@ class CopyDialog : public QDialog public: explicit CopyDialog(QWidget* parent); - void show(const Verse& curr); ~CopyDialog(); + void show(); + protected: void closeEvent(QCloseEvent* event); private: Ui::CopyDialog* ui; + Verse* m_currVerse = Verse::current(); DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); void copyRange(); + int m_surah = -1; - int m_surahCnt = 0; QIntValidator* m_verseValidator = new QIntValidator(this); }; diff --git a/src/core/khatmahdialog.cpp b/src/core/khatmahdialog.cpp index b314df08..0952aa85 100644 --- a/src/core/khatmahdialog.cpp +++ b/src/core/khatmahdialog.cpp @@ -3,9 +3,8 @@ #include #include -KhatmahDialog::KhatmahDialog(const Verse& curr, QWidget* parent) +KhatmahDialog::KhatmahDialog(QWidget* parent) : QDialog(parent) - , m_currVerse(curr) , ui(new Ui::KhatmahDialog) { ui->setupUi(this); @@ -22,8 +21,8 @@ KhatmahDialog::KhatmahDialog(const Verse& curr, QWidget* parent) InputField* KhatmahDialog::loadKhatmah(const int id) { - Verse v; - m_dbMgr->getKhatmahPos(id, v); + QList vInfo; + m_dbMgr->getKhatmahPos(id, vInfo); QFrame* frame = new QFrame(ui->scrlDialogContent); // property used for styling frame->setProperty("khatmah", true); @@ -55,8 +54,8 @@ KhatmahDialog::loadKhatmah(const int id) remove->setDisabled(true); } - QString info = tr("Surah: ") + m_dbMgr->surahNameList().at(v.surah - 1) + - " - " + tr("Verse: ") + QString::number(v.number); + QString info = tr("Surah: ") + m_dbMgr->surahNameList().at(vInfo[1] - 1) + + " - " + tr("Verse: ") + QString::number(vInfo[2]); lbPosition->setText(info); activate->setFocusPolicy(Qt::NoFocus); @@ -105,7 +104,7 @@ KhatmahDialog::loadAll() void KhatmahDialog::startNewKhatmah() { - int id = m_dbMgr->addKhatmah(m_currVerse, "new"); + int id = m_dbMgr->addKhatmah(m_currVerse->toList(), "new"); QString gen = tr("Khatmah ") + QString::number(id); m_dbMgr->editKhatmahName(id, gen); InputField* inpField = loadKhatmah(id); @@ -144,14 +143,14 @@ KhatmahDialog::removeKhatmah() void KhatmahDialog::setActiveKhatmah() { - Verse v; + QList vInfo; QFrame* newActive = qobject_cast(sender()->parent()); QVariant id = newActive->objectName(); m_settings->setValue("Reader/Khatmah", id); - m_dbMgr->saveActiveKhatmah(m_currVerse); + m_dbMgr->saveActiveKhatmah(m_currVerse->toList()); m_dbMgr->setActiveKhatmah(id.toInt()); - m_dbMgr->getKhatmahPos(id.toInt(), v); + m_dbMgr->getKhatmahPos(id.toInt(), vInfo); newActive->findChild("activate")->setEnabled(false); newActive->findChild("remove")->setEnabled(false); @@ -160,13 +159,7 @@ KhatmahDialog::setActiveKhatmah() m_currActive = newActive; ui->lbCurrKhatmah->setText(m_dbMgr->getKhatmahName(id.toInt())); - emit navigateToVerse(v); -} - -void -KhatmahDialog::closeEvent(QCloseEvent* event) -{ - this->hide(); + emit navigateToVerse(vInfo); } void @@ -178,6 +171,12 @@ KhatmahDialog::show() QDialog::show(); } +void +KhatmahDialog::closeEvent(QCloseEvent* event) +{ + this->hide(); +} + KhatmahDialog::~KhatmahDialog() { delete ui; diff --git a/src/core/khatmahdialog.h b/src/core/khatmahdialog.h index ea64fad0..bdf802c7 100644 --- a/src/core/khatmahdialog.h +++ b/src/core/khatmahdialog.h @@ -3,6 +3,7 @@ #include "../globals.h" #include "../utils/dbmanager.h" +#include "../utils/verse.h" #include "../widgets/inputfield.h" #include #include @@ -26,7 +27,7 @@ class KhatmahDialog : public QDialog * @param curr - reference to the current active verse * @param parent - pointer to the parent widget */ - explicit KhatmahDialog(const Verse& curr, QWidget* parent = nullptr); + explicit KhatmahDialog(QWidget* parent = nullptr); ~KhatmahDialog(); /** * @brief reload shown khatmah entries and show the dialog @@ -72,7 +73,7 @@ private slots: void setActiveKhatmah(); private: - const Verse& m_currVerse; + const Verse* m_currVerse = Verse::current(); const bool m_darkmode = Globals::themeId == 2; QSettings* m_settings = Globals::settings; DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index b48c184c..afc43241 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -5,7 +5,6 @@ #include "mainwindow.h" #include "../widgets/aboutdialog.h" -#include "../widgets/clickablelabel.h" #include "khatmahdialog.h" #include "ui_mainwindow.h" using namespace fa; @@ -17,9 +16,8 @@ MainWindow::MainWindow(QWidget* parent) , m_shortcutHandler(new ShortcutHandler(this)) { ui->setupUi(this); - ui->frmCenteralCont->setLayoutDirection(Qt::LeftToRight); + loadCurrent(); loadIcons(); - loadSettings(); init(); if (m_settings->value("WindowState").isNull()) @@ -34,19 +32,12 @@ MainWindow::MainWindow(QWidget* parent) setupMenubarToggle(); this->show(); - m_notifyMgr->setTooltip(tr("Quran Companion")); - if (m_settings->value("VOTD").toBool()) - m_verseDlg->showVOTD(true); - - m_popup->dockLocationChanged(dockWidgetArea(ui->sideDock)); + m_popup->setDockArea(dockWidgetArea(ui->sideDock)); } void MainWindow::loadIcons() { - ui->btnNext->setIcon(m_fa->icon(fa_solid, fa_arrow_left)); - ui->btnPrev->setIcon(m_fa->icon(fa_solid, fa_arrow_right)); - ui->actionKhatmah->setIcon(m_fa->icon(fa_solid, fa_list)); ui->actionDownloadManager->setIcon(m_fa->icon(fa_solid, fa_download)); ui->actionExit->setIcon(m_fa->icon(fa_solid, fa_xmark)); @@ -58,81 +49,20 @@ MainWindow::loadIcons() ui->actionAdvancedCopy->setIcon(m_fa->icon(fa_solid, fa_clipboard)); ui->actionReaderViewToggle->setIcon(m_fa->icon(fa_solid, fa_columns)); ui->actionUpdates->setIcon(m_fa->icon(fa_solid, fa_arrow_rotate_right)); - - ui->btnPlay->setIcon(m_fa->icon(fa_solid, fa_play)); - ui->btnPause->setIcon(m_fa->icon(fa_solid, fa_pause)); - ui->btnStop->setIcon(m_fa->icon(fa_solid, fa_stop)); - - ui->lbSpeaker->setText(QString(fa_volume_high)); - ui->lbSpeaker->setFont(m_fa->font(fa_solid, 16)); } void -MainWindow::loadSettings() +MainWindow::loadCurrent() { int id = m_settings->value("Reader/Khatmah").toInt(); + QList vInfo = m_currVerse->toList(); m_dbMgr->setActiveKhatmah(id); - if (!m_dbMgr->getKhatmahPos(id, m_currVerse)) { + if (!m_dbMgr->getKhatmahPos(id, vInfo)) { QString name = id ? tr("Khatmah ") + QString::number(id) : tr("Default"); - m_dbMgr->addKhatmah(m_currVerse, name, id); + m_dbMgr->addKhatmah(vInfo, name, id); } -} - -void -MainWindow::loadReader() -{ - if (m_readerMode == ReaderMode::SinglePage) { - m_activeQuranBrowser = m_quranBrowsers[0] = - new QuranPageBrowser(ui->frmPageContent, m_currVerse.page); - - QWidget* scrollWidget = new QWidget(); - scrollWidget->setObjectName("scrollWidget"); - QVBoxLayout* vbl = new QVBoxLayout(); - vbl->setDirection(QBoxLayout::BottomToTop); - scrollWidget->setLayout(vbl); - - m_scrlVerseByVerse = new QScrollArea; - m_scrlVerseByVerse->setWidget(scrollWidget); - m_scrlVerseByVerse->setWidgetResizable(true); - m_scrlVerseByVerse->setStyleSheet( - "QLabel, QAbstractScrollArea, QWidget#scrollWidget { " - "background: " - "transparent }"); - - QHBoxLayout* lyt = qobject_cast(ui->frmReader->layout()); - ui->frmSidePanel->layout()->addWidget(m_scrlVerseByVerse); - lyt->setStretch(0, 1); - lyt->setStretch(1, 0); - ui->frmCenteralCont->setMinimumWidth(900); - } - - else { - // even Quran pages are always on the left side - if (m_currVerse.page % 2 == 0) { - m_quranBrowsers[0] = - new QuranPageBrowser(ui->frmPageContent, m_currVerse.page - 1); - m_activeQuranBrowser = m_quranBrowsers[1] = - new QuranPageBrowser(ui->frmSidePanel, m_currVerse.page); - - } else { - m_activeQuranBrowser = m_quranBrowsers[0] = - new QuranPageBrowser(ui->frmPageContent, m_currVerse.page); - m_quranBrowsers[1] = - new QuranPageBrowser(ui->frmSidePanel, m_currVerse.page + 1); - } - - ui->frmSidePanel->layout()->addWidget(m_quranBrowsers[1]); - QHBoxLayout* lyt = qobject_cast(ui->frmReader->layout()); - lyt->insertSpacerItem(0, new QSpacerItem(20, 20, QSizePolicy::Expanding)); - lyt->addSpacerItem(new QSpacerItem(20, 20, QSizePolicy::Expanding)); - lyt->setStretch(0, 1); - lyt->setStretch(1, 0); - lyt->setStretch(2, 0); - lyt->setStretch(3, 1); - } - - ui->frmPageContent->layout()->addWidget(m_quranBrowsers[0]); + m_currVerse->update(vInfo); } void @@ -162,7 +92,7 @@ MainWindow::updateProcessCallback() if (this->isVisible()) QMessageBox::information(this, tr("Update info"), displayText); else - m_notifyMgr->notify(tr("Update info"), displayText); + m_systemTray->notify(tr("Update info"), displayText); } else { @@ -175,7 +105,7 @@ MainWindow::updateProcessCallback() } else { - m_notifyMgr->notify( + m_systemTray->notify( tr("Update info"), tr("Updates are available, use the maintainance tool to install " "the latest updates.")); @@ -186,14 +116,6 @@ MainWindow::updateProcessCallback() void MainWindow::setupShortcuts() { - connect(m_shortcutHandler, - &ShortcutHandler::togglePlayerControls, - this, - &MainWindow::togglePlayerControls); - connect(m_shortcutHandler, - &ShortcutHandler::toggleReaderView, - this, - &MainWindow::toggleReaderView); connect(m_shortcutHandler, &ShortcutHandler::toggleMenubar, this, @@ -202,50 +124,10 @@ MainWindow::setupShortcuts() &ShortcutHandler::toggleNavDock, this, &MainWindow::toggleNavDock); - connect(m_shortcutHandler, - &ShortcutHandler::togglePlayback, - this, - &MainWindow::togglePlayback); - connect(m_shortcutHandler, - &ShortcutHandler::incrementVolume, - this, - &MainWindow::incrementVolume); - connect(m_shortcutHandler, - &ShortcutHandler::decrementVolume, - this, - &MainWindow::decrementVolume); connect(m_shortcutHandler, &ShortcutHandler::bookmarkCurrent, this, &MainWindow::addCurrentToBookmarks); - connect(m_shortcutHandler, - &ShortcutHandler::nextPage, - this, - &MainWindow::btnNextClicked); - connect(m_shortcutHandler, - &ShortcutHandler::prevPage, - this, - &MainWindow::btnPrevClicked); - connect(m_shortcutHandler, - &ShortcutHandler::nextVerse, - this, - &MainWindow::nextVerse); - connect(m_shortcutHandler, - &ShortcutHandler::prevVerse, - this, - &MainWindow::prevVerse); - connect( - m_shortcutHandler, &ShortcutHandler::nextJuz, this, &MainWindow::nextJuz); - connect( - m_shortcutHandler, &ShortcutHandler::prevJuz, this, &MainWindow::prevJuz); - connect(m_shortcutHandler, - &ShortcutHandler::nextSurah, - this, - &MainWindow::nextSurah); - connect(m_shortcutHandler, - &ShortcutHandler::prevSurah, - this, - &MainWindow::prevSurah); connect(m_shortcutHandler, &ShortcutHandler::openDownloads, this, @@ -274,19 +156,26 @@ MainWindow::setupShortcuts() &ShortcutHandler::openAdvancedCopy, this, &MainWindow::actionAdvancedCopyTriggered); - - for (int i = 0; i <= 1; i++) { - if (m_quranBrowsers[i]) { - connect(m_shortcutHandler, - &ShortcutHandler::zoomIn, - m_quranBrowsers[i], - &QuranPageBrowser::actionZoomIn); - connect(m_shortcutHandler, - &ShortcutHandler::zoomOut, - m_quranBrowsers[i], - &QuranPageBrowser::actionZoomOut); - } - } + connect(m_shortcutHandler, + &ShortcutHandler::nextVerse, + this, + &MainWindow::nextVerse); + connect(m_shortcutHandler, + &ShortcutHandler::prevVerse, + this, + &MainWindow::prevVerse); + connect(m_shortcutHandler, + &ShortcutHandler::nextSurah, + this, + &MainWindow::nextSurah); + connect(m_shortcutHandler, + &ShortcutHandler::prevSurah, + this, + &MainWindow::prevSurah); + connect( + m_shortcutHandler, &ShortcutHandler::nextJuz, this, &MainWindow::nextJuz); + connect( + m_shortcutHandler, &ShortcutHandler::prevJuz, this, &MainWindow::prevJuz); } void @@ -339,30 +228,14 @@ MainWindow::setupConnections() &MainWindow::actionAboutTriggered); connect(ui->actionReaderViewToggle, &QAction::triggered, - this, - &MainWindow::toggleReaderView); + m_reader, + &QuranReader::toggleReaderView); connect(m_verseDlg, &VerseDialog::navigateToVerse, - this, - &MainWindow::navigateToVerse); - - // ########## Quran page ########## // - connect(m_quranBrowsers[0], - &QTextBrowser::anchorClicked, - this, - &MainWindow::verseAnchorClicked); - if (m_quranBrowsers[1]) { - connect(m_quranBrowsers[1], - &QTextBrowser::anchorClicked, - this, - &MainWindow::verseAnchorClicked); - } + m_reader, + &QuranReader::navigateToVerse); // ########## page controls ########## // - connect( - ui->btnNext, &QPushButton::clicked, this, &MainWindow::btnNextClicked); - connect( - ui->btnPrev, &QPushButton::clicked, this, &MainWindow::btnPrevClicked); connect(ui->cmbPage, &QComboBox::currentIndexChanged, this, @@ -375,10 +248,14 @@ MainWindow::setupConnections() &QComboBox::currentIndexChanged, this, &MainWindow::cmbJuzChanged); - connect(m_player, - &VersePlayer::missingVerseFile, + connect(m_reader, + &QuranReader::currentVerseChanged, this, - &MainWindow::missingRecitationFileWarn); + &MainWindow::currentVerseChanged); + connect(m_reader, + &QuranReader::currentSurahChanged, + this, + &MainWindow::currentSurahChanged); // ########## navigation dock ########## // connect(ui->lineEditSearchSurah, @@ -390,60 +267,24 @@ MainWindow::setupConnections() this, &MainWindow::listSurahNameClicked); - // ########## audio slider ########## // - connect(m_player, - &QMediaPlayer::positionChanged, - this, - &MainWindow::mediaPosChanged); - connect(m_player, - &QMediaPlayer::playbackStateChanged, - this, - &MainWindow::mediaStateChanged); - connect(ui->sldrAudioPlayer, - &QSlider::sliderMoved, - m_player, - &QMediaPlayer::setPosition); - connect(ui->sldrVolume, - &QSlider::valueChanged, - this, - &MainWindow::volumeSliderValueChanged); - - // ########## player control ########## // - connect(m_player, - &QMediaPlayer::mediaStatusChanged, - this, - &MainWindow::mediaStatusChanged); - connect( - ui->btnPlay, &QPushButton::clicked, this, &MainWindow::btnPlayClicked); - connect( - ui->btnPause, &QPushButton::clicked, this, &MainWindow::btnPauseClicked); - connect( - ui->btnStop, &QPushButton::clicked, this, &MainWindow::btnStopClicked); - connect(ui->cmbReciter, - &QComboBox::currentIndexChanged, - m_player, - &VersePlayer::changeReciter); - // ########## system tray ########## // - connect(m_notifyMgr, &NotificationManager::exit, this, &QApplication::exit); - connect(m_notifyMgr, - &NotificationManager::togglePlayback, - this, - &MainWindow::togglePlayback); - connect( - m_notifyMgr, &NotificationManager::showWindow, this, &MainWindow::show); - connect( - m_notifyMgr, &NotificationManager::hideWindow, this, &MainWindow::hide); - connect(m_notifyMgr, - &NotificationManager::checkForUpdates, + connect(m_systemTray, &SystemTray::exit, this, &QApplication::exit); + connect(m_systemTray, + &SystemTray::togglePlayback, + m_playerControls, + &PlayerControls::togglePlayback); + connect(m_systemTray, &SystemTray::showWindow, this, &MainWindow::show); + connect(m_systemTray, &SystemTray::hideWindow, this, &MainWindow::hide); + connect(m_systemTray, + &SystemTray::checkForUpdates, this, &MainWindow::checkForUpdates); - connect(m_notifyMgr, - &NotificationManager::openAbout, + connect(m_systemTray, + &SystemTray::openAbout, this, &MainWindow::actionAboutTriggered); - connect(m_notifyMgr, - &NotificationManager::openPrefs, + connect(m_systemTray, + &SystemTray::openPrefs, this, &MainWindow::actionPrefTriggered); @@ -451,7 +292,7 @@ MainWindow::setupConnections() connect(ui->sideDock, &QDockWidget::dockLocationChanged, m_popup, - &NotificationPopup::dockLocationChanged); + &NotificationPopup::setDockArea); connect(m_downManPtr, &DownloadManager::downloadCompleted, m_popup, @@ -464,6 +305,16 @@ MainWindow::setupConnections() &DownloadManager::latestVersionFound, m_popup, &NotificationPopup::checkUpdate); + connect(m_reader, + &QuranReader::notifyBookmarkAdded, + m_popup, + &NotificationPopup::bookmarkAdded); + connect(m_reader, + &QuranReader::notifyBookmarkRemoved, + m_popup, + &NotificationPopup::bookmarkRemoved); + connect( + m_reader, &QuranReader::copyVerseText, this, &MainWindow::copyVerseText); // ########## Settings Dialog ########## // // Restart signal @@ -475,98 +326,97 @@ MainWindow::setupConnections() // Quran page signals connect(m_settingsDlg, &SettingsDialog::redrawQuranPage, - this, - &MainWindow::redrawQuranPage); + m_reader, + &QuranReader::redrawQuranPage); connect(m_settingsDlg, &SettingsDialog::highlightLayerChanged, - this, - &MainWindow::updateHighlight); - for (int i = 0; i <= 1; i++) { - if (m_quranBrowsers[i]) { - connect(m_settingsDlg, - &SettingsDialog::quranFontChanged, - m_quranBrowsers[i], - &QuranPageBrowser::updateFontSize); - } - } - + m_reader, + &QuranReader::updateHighlight); + connect(m_settingsDlg, + &SettingsDialog::quranFontChanged, + m_reader, + &QuranReader::updatePageFontSize); // Side panel signals connect(m_settingsDlg, &SettingsDialog::redrawSideContent, - this, - &MainWindow::addSideContent); + m_reader, + &QuranReader::addSideContent); connect(m_settingsDlg, &SettingsDialog::tafsirChanged, - this, - &MainWindow::updateLoadedTafsir); + m_reader, + &QuranReader::updateLoadedTafsir); connect(m_settingsDlg, &SettingsDialog::translationChanged, - this, - &MainWindow::updateLoadedTranslation); + m_reader, + &QuranReader::updateLoadedTranslation); connect(m_settingsDlg, &SettingsDialog::sideFontChanged, - this, - &MainWindow::updateSideFont); + m_reader, + &QuranReader::updateSideFont); connect(m_settingsDlg, &SettingsDialog::verseTypeChanged, - this, - &MainWindow::updateVerseType); - + m_reader, + &QuranReader::updateVerseType); // audio device signals connect(m_settingsDlg, &SettingsDialog::usedAudioDeviceChanged, m_player, &VersePlayer::changeUsedAudioDevice); - // shortcut change connect(m_settingsDlg, &SettingsDialog::shortcutChanged, m_shortcutHandler, &ShortcutHandler::shortcutChanged); + + connect(m_player, + &VersePlayer::missingVerseFile, + this, + &MainWindow::missingRecitationFileWarn); + connect(m_player, + &QMediaPlayer::playbackStateChanged, + this, + &MainWindow::updateTrayTooltip); } void MainWindow::init() { - m_player = - new VersePlayer(this, m_currVerse, m_settings->value("Reciter", 0).toInt()); + m_player = new VersePlayer(this, m_settings->value("Reciter", 0).toInt()); + m_reader = new QuranReader(this, m_player, m_shortcutHandler); + m_playerControls = + new PlayerControls(this, m_player, m_reader, m_shortcutHandler); m_popup = new NotificationPopup(this); m_betaqaViewer = new BetaqaViewer(this); m_verseDlg = new VerseDialog(this); m_downManPtr = new DownloadManager(this); m_settingsDlg = new SettingsDialog(this, m_player); - loadReader(); - updateHighlight(); + QHBoxLayout* controls = new QHBoxLayout(); + controls->setAlignment(Qt::AlignCenter); + controls->setContentsMargins(0, 0, 0, 0); + controls->setSpacing(0); + controls->addStretch(1); + controls->addWidget(m_playerControls); + controls->addStretch(1); + QFrame* frm = new QFrame(this); + frm->setLayout(controls); + ui->scrollAreaWidgetContents->layout()->addWidget(frm); + ui->scrollAreaWidgetContents->layout()->addWidget(m_reader); ui->cmbPage->setValidator(new QIntValidator(1, 604, this)); - m_notifyMgr = new NotificationManager(this); - - updateLoadedTafsir(); - updateLoadedTranslation(); - updateSideFont(); - updateVerseType(); + m_systemTray = new SystemTray(this); - redrawQuranPage(true); setVerseComboBoxRange(true); - if (m_readerMode == ReaderMode::SinglePage) - addSideContent(); - for (int i = 1; i < 605; i++) { ui->cmbPage->addItem(QString::number(i)); } - foreach (const Reciter& r, m_recitersList) { - ui->cmbReciter->addItem(r.displayName); - } - // sets without emitting signal - setCmbVerseIdx(m_currVerse.number - 1); - setCmbJuzIdx(m_dbMgr->getJuzOfPage(m_currVerse.page) - 1); + setCmbVerseIdx(m_currVerse->number() - 1); + setCmbJuzIdx(m_dbMgr->getJuzOfPage(m_currVerse->page()) - 1); - ui->cmbPage->setCurrentIndex(m_currVerse.page - 1); - ui->cmbReciter->setCurrentIndex(m_settings->value("Reciter", 0).toInt()); + ui->cmbPage->setCurrentIndex(m_currVerse->page() - 1); } void @@ -582,10 +432,10 @@ MainWindow::setupSurahsDock() ui->listViewSurahs->setModel(&m_surahListModel); QItemSelectionModel* selector = ui->listViewSurahs->selectionModel(); - selector->select(m_surahListModel.index(m_currVerse.surah - 1), + selector->select(m_surahListModel.index(m_currVerse->surah() - 1), QItemSelectionModel::Rows | QItemSelectionModel::Select); - ui->listViewSurahs->scrollTo(m_surahListModel.index(m_currVerse.surah - 1), + ui->listViewSurahs->scrollTo(m_surahListModel.index(m_currVerse->surah() - 1), QAbstractItemView::PositionAtCenter); } @@ -621,79 +471,12 @@ MainWindow::setupMenubarToggle() }); } -bool -MainWindow::areNeighbors(int page1, int page2) -{ - return page1 % 2 != 0 && page2 % 2 == 0 && page2 == page1 + 1; -} - -void -MainWindow::switchActivePage() -{ - if (!m_quranBrowsers[1]) - return; - - m_activeQuranBrowser->resetHighlight(); - if (m_activeQuranBrowser == m_quranBrowsers[0]) { - m_activeQuranBrowser = m_quranBrowsers[1]; - m_activeVList = &m_vLists[1]; - } else { - m_activeQuranBrowser = m_quranBrowsers[0]; - m_activeVList = &m_vLists[0]; - } -} - -void -MainWindow::btnNextClicked() -{ - if (m_readerMode == ReaderMode::SinglePage || m_currVerse.page % 2 == 0) - nextPage(1); - else - nextPage(2); -} - -void -MainWindow::btnPrevClicked() -{ - if (m_readerMode == ReaderMode::SinglePage || m_currVerse.page % 2 != 0) - prevPage(1); - else - prevPage(2); -} - -void -MainWindow::selectVerse(int browserIdx, int IdxInPage) -{ - if (m_activeQuranBrowser != m_quranBrowsers[browserIdx]) - switchActivePage(); - - const Verse& v = m_vLists[browserIdx].at(IdxInPage); - int currSurah = m_currVerse.surah; - m_currVerse = v; - m_player->setVerse(m_currVerse); - if (currSurah != v.surah) { - setVerseComboBoxRange(); - syncSelectedSurah(); - } else - setCmbVerseIdx(v.number - 1); - - setCmbPageIdx(v.page - 1); - setCmbJuzIdx(m_dbMgr->getJuzOfPage(v.page) - 1); -} - -void -MainWindow::updateSurahVerseCount() -{ - - m_surahCount = m_dbMgr->getSurahVerseCount(m_currVerse.surah); -} - QModelIndex MainWindow::syncSelectedSurah() { QItemSelectionModel* select = ui->listViewSurahs->selectionModel(); select->clearSelection(); - QModelIndex surah = m_surahListModel.index(m_currVerse.surah - 1); + QModelIndex surah = m_surahListModel.index(m_currVerse->surah() - 1); select->select(surah, QItemSelectionModel::Rows | QItemSelectionModel::Select); ui->listViewSurahs->scrollTo(surah, QAbstractItemView::PositionAtCenter); @@ -702,32 +485,18 @@ MainWindow::syncSelectedSurah() } void -MainWindow::updatePageVerseInfoList() +MainWindow::currentVerseChanged() { - if (m_activeQuranBrowser == m_quranBrowsers[0]) { - m_vLists[0] = m_dbMgr->getVerseInfoList(m_currVerse.page); - if (m_readerMode == DoublePage) - m_vLists[1] = m_dbMgr->getVerseInfoList(m_currVerse.page + 1); - - m_activeVList = &m_vLists[0]; - - } else { - m_vLists[0] = m_dbMgr->getVerseInfoList(m_currVerse.page - 1); - m_vLists[1] = m_dbMgr->getVerseInfoList(m_currVerse.page); - - m_activeVList = &m_vLists[1]; - } + setCmbVerseIdx(m_currVerse->number() - 1); + setCmbPageIdx(m_currVerse->page() - 1); + setCmbJuzIdx(m_dbMgr->getJuzOfPage(m_currVerse->page()) - 1); } void -MainWindow::setVerseToStartOfPage() +MainWindow::currentSurahChanged() { - // set the current verse to the verse at the top of the page - m_currVerse = m_activeVList->at(0); - // update the player active verse - m_player->setVerse(m_currVerse); - // open newly set verse recitation file - m_player->loadActiveVerse(); + setVerseComboBoxRange(); + syncSelectedSurah(); } void @@ -761,134 +530,17 @@ MainWindow::setCmbJuzIdx(int idx) void MainWindow::setVerseComboBoxRange(bool forceUpdate) { - int oldCount = m_surahCount; - updateSurahVerseCount(); - - if (m_surahCount != oldCount || forceUpdate) { - m_verseValidator->setTop(m_surahCount); - - // updates values in the combobox with the current surah verses - ui->cmbVerse->clear(); - m_internalVerseChange = true; - for (int i = 1; i <= m_surahCount; i++) - ui->cmbVerse->addItem(QString::number(i), i); - m_internalVerseChange = false; - - ui->cmbVerse->setValidator(m_verseValidator); - } - - setCmbVerseIdx(m_currVerse.number - 1); -} - -void -MainWindow::gotoPage(int page, bool updateElements, bool automaticFlip) -{ - m_activeQuranBrowser->resetHighlight(); - - if (!automaticFlip) - m_player->stop(); - - if (m_activeQuranBrowser->page() != page) { - if (m_readerMode == ReaderMode::SinglePage) - gotoSinglePage(page); - else - gotoDoublePage(page); - } - - if (updateElements) { - setVerseToStartOfPage(); - syncSelectedSurah(); - setVerseComboBoxRange(); - setCmbJuzIdx(m_dbMgr->getJuzOfPage(m_currVerse.page) - 1); - setCmbPageIdx(m_currVerse.page - 1); - } -} - -void -MainWindow::gotoSinglePage(int page) -{ - m_currVerse.page = page; - redrawQuranPage(); - - m_currVerse = m_activeVList->at(0); - addSideContent(); -} - -void -MainWindow::gotoDoublePage(int page) -{ - int currPage = m_currVerse.page; - m_currVerse.page = page; - - int pageBrowerIdx = page % 2 == 0; - - if (areNeighbors(currPage, page) || areNeighbors(page, currPage)) - switchActivePage(); - else { - m_activeQuranBrowser = m_quranBrowsers[pageBrowerIdx]; - redrawQuranPage(); - } -} - -void -MainWindow::nextPage(int step) -{ - bool keepPlaying = m_player->playbackState() == QMediaPlayer::PlayingState; - if (m_currVerse.page + step <= 604) { - setCmbPageIdx(m_currVerse.page + step - 1); - - gotoPage(m_currVerse.page + step, true, true); - - if (m_readerMode == ReaderMode::SinglePage) - m_scrlVerseByVerse->verticalScrollBar()->setValue(0); + m_verseValidator->setTop(m_currVerse->surahCount()); - // if the page is flipped automatically, resume playback - if (keepPlaying) { - m_player->play(); - highlightCurrentVerse(); - } - } -} - -void -MainWindow::prevPage(int step) -{ - bool keepPlaying = m_player->playbackState() == QMediaPlayer::PlayingState; - if (m_currVerse.page - step >= 1) { - setCmbPageIdx(m_currVerse.page - step - 1); - - gotoPage(m_currVerse.page - step, true, true); - - if (m_readerMode == ReaderMode::SinglePage) - m_scrlVerseByVerse->verticalScrollBar()->setValue(0); - - if (keepPlaying) { - m_player->play(); - highlightCurrentVerse(); - } - } -} - -void -MainWindow::gotoSurah(int surahIdx) -{ - // getting surah index - int page = m_dbMgr->getSurahStartPage(surahIdx); - gotoPage(page, false); - - m_currVerse.page = page; - m_currVerse.surah = surahIdx; - m_currVerse.number = surahIdx == 9 || surahIdx == 1 ? 1 : 0; - - // syncing the player & playing basmalah - m_player->setVerse(m_currVerse); - m_player->playCurrentVerse(); + // updates values in the combobox with the current surah verses + ui->cmbVerse->clear(); + m_internalVerseChange = true; + for (int i = 1; i <= m_currVerse->surahCount(); i++) + ui->cmbVerse->addItem(QString::number(i), i); + m_internalVerseChange = false; - highlightCurrentVerse(); - setCmbPageIdx(m_currVerse.page - 1); - setCmbJuzIdx(m_dbMgr->getJuzOfPage(m_currVerse.page) - 1); - setVerseComboBoxRange(); - syncSelectedSurah(); + ui->cmbVerse->setValidator(m_verseValidator); + setCmbVerseIdx(m_currVerse->number() - 1); } void @@ -911,7 +563,7 @@ void MainWindow::listSurahNameClicked(const QModelIndex& index) { int s = m_surahList.indexOf(index.data().toString()) + 1; - gotoSurah(s); + m_reader->gotoSurah(s); } void @@ -922,7 +574,7 @@ MainWindow::cmbPageChanged(int newIdx) return; } - gotoPage(newIdx + 1); + m_reader->gotoPage(newIdx + 1); } void @@ -937,20 +589,19 @@ MainWindow::cmbVerseChanged(int newVerseIdx) } int verse = newVerseIdx + 1; - int page = m_dbMgr->getVersePage(m_currVerse.surah, verse); + int page = m_dbMgr->getVersePage(m_currVerse->surah(), verse); - if (page != m_currVerse.page) - gotoPage(page, false); + if (page != m_currVerse->page()) + m_reader->gotoPage(page, false); - m_currVerse.page = page; - m_currVerse.number = verse; - highlightCurrentVerse(); + m_currVerse->setPage(page); + m_currVerse->setNumber(verse); + m_reader->highlightCurrentVerse(); setCmbPageIdx(page - 1); setCmbJuzIdx(m_dbMgr->getJuzOfPage(page) - 1); // open newly set verse recitation file - m_player->setVerse(m_currVerse); m_player->loadActiveVerse(); } @@ -962,112 +613,37 @@ MainWindow::cmbJuzChanged(int newJuzIdx) return; } int page = m_dbMgr->getJuzStartPage(newJuzIdx + 1); - gotoPage(page); -} - -void -MainWindow::togglePlayback() -{ - if (m_player->playbackState() == QMediaPlayer::PlayingState) { - btnPauseClicked(); - } else { - btnPlayClicked(); - } -} - -void -MainWindow::btnPauseClicked() -{ - m_player->pause(); -} - -void -MainWindow::btnPlayClicked() -{ - highlightCurrentVerse(); - m_player->play(); -} - -void -MainWindow::btnStopClicked() -{ - m_player->stop(); - setVerseToStartOfPage(); - syncSelectedSurah(); - setVerseComboBoxRange(); + m_reader->gotoPage(page); } void MainWindow::mediaStatusChanged(QMediaPlayer::MediaStatus status) { - if (status == QMediaPlayer::EndOfMedia) { + if (status == QMediaPlayer::EndOfMedia) nextVerse(); - } -} - -void -MainWindow::mediaStateChanged(QMediaPlayer::PlaybackState state) -{ - if (state == QMediaPlayer::PlayingState) { - ui->btnPlay->setEnabled(false); - ui->btnPause->setEnabled(true); - ui->btnStop->setEnabled(true); - } else if (state == QMediaPlayer::PausedState) { - ui->btnPlay->setEnabled(true); - ui->btnPause->setEnabled(false); - ui->btnStop->setEnabled(true); - } else if (state == QMediaPlayer::StoppedState) { - ui->btnPlay->setEnabled(true); - ui->btnPause->setEnabled(false); - ui->btnStop->setEnabled(false); - } - - updateTrayTooltip(state); } void MainWindow::updateTrayTooltip(QMediaPlayer::PlaybackState state) { if (state == QMediaPlayer::PlayingState) { - m_notifyMgr->setTooltip(tr("Now playing: ") + m_player->reciterName() + - " - " + tr("Surah ") + - m_dbMgr->getSurahName(m_currVerse.surah)); + m_systemTray->setTooltip(tr("Now playing: ") + m_player->reciterName() + + " - " + tr("Surah ") + + m_dbMgr->getSurahName(m_currVerse->surah())); } else - m_notifyMgr->setTooltip(tr("Quran Companion")); + m_systemTray->setTooltip(tr("Quran Companion")); } void MainWindow::addCurrentToBookmarks() { - if (!m_dbMgr->isBookmarked(m_currVerse)) { - m_dbMgr->addBookmark(m_currVerse); + QList vInfo = m_currVerse->toList(); + if (!m_dbMgr->isBookmarked(vInfo)) { + m_dbMgr->addBookmark(vInfo); m_popup->bookmarkAdded(); } } -void -MainWindow::mediaPosChanged(qint64 position) -{ - if (ui->sldrAudioPlayer->maximum() != m_player->duration()) - ui->sldrAudioPlayer->setMaximum(m_player->duration()); - - if (!ui->sldrAudioPlayer->isSliderDown()) - ui->sldrAudioPlayer->setValue(position); -} - -void -MainWindow::volumeSliderValueChanged(int position) -{ - qreal linearVolume = - QAudio::convertVolume(ui->sldrVolume->value() / qreal(100.0), - QAudio::LogarithmicVolumeScale, - QAudio::LinearVolumeScale); - if (linearVolume != m_volume) { - m_volume = linearVolume; - m_player->setPlayerVolume(m_volume); - } -} - void MainWindow::missingRecitationFileWarn(int reciterIdx, int surah) { @@ -1114,80 +690,6 @@ MainWindow::missingTafsir(int idx) } } -void -MainWindow::verseClicked() -{ - // object = clickable label, parent = verse frame, verse frame name scheme = - // 'surah_verse' - QStringList data = sender()->parent()->objectName().split('_'); - int surah = data.at(0).toInt(); - int verse = data.at(1).toInt(); - - m_currVerse.number = verse; - m_player->setVerse(m_currVerse); - - if (m_currVerse.surah != surah) { - m_currVerse.surah = surah; - m_player->setVerse(m_currVerse); - setVerseComboBoxRange(); - syncSelectedSurah(); - } - - setCmbVerseIdx(verse - 1); - m_player->loadActiveVerse(); - btnPlayClicked(); -} - -void -MainWindow::verseAnchorClicked(const QUrl& hrefUrl) -{ - if (hrefUrl.toString().at(1) == 'F') { - int surah = hrefUrl.toString().remove("#F").toInt(); - qDebug() << "SURAH CARD:" << surah; - m_betaqaViewer->showSurah(surah); - return; - } - - QuranPageBrowser* senderBrowser = qobject_cast(sender()); - int browerIdx = senderBrowser == m_quranBrowsers[1]; - int idx = hrefUrl.toString().remove('#').toInt(); - Verse v = m_vLists[browerIdx].at(idx); - - QuranPageBrowser::Action chosenAction = - senderBrowser->lmbVerseMenu(m_dbMgr->isBookmarked(v)); - - switch (chosenAction) { - case QuranPageBrowser::play: - selectVerse(browerIdx, idx); - m_player->loadActiveVerse(); - btnPlayClicked(); - break; - case QuranPageBrowser::select: - selectVerse(browerIdx, idx); - m_player->setSource(QUrl()); - highlightCurrentVerse(); - break; - - case QuranPageBrowser::tafsir: - showVerseTafsir(v); - break; - case QuranPageBrowser::copy: - copyVerseText(v); - m_popup->copiedToClipboard(); - break; - case QuranPageBrowser::addBookmark: - m_dbMgr->addBookmark(v); - m_popup->bookmarkAdded(); - break; - case QuranPageBrowser::removeBookmark: - if (m_dbMgr->removeBookmark(v)) - m_popup->bookmarkRemoved(); - break; - default: - break; - } -} - void MainWindow::actionPrefTriggered() { @@ -1210,8 +712,8 @@ MainWindow::actionBookmarksTriggered() m_bookmarksDlg = new BookmarksDialog(this); connect(m_bookmarksDlg, &BookmarksDialog::navigateToVerse, - this, - &MainWindow::navigateToVerse); + m_reader, + &QuranReader::navigateToVerse); } m_bookmarksDlg->showWindow(); @@ -1221,14 +723,14 @@ void MainWindow::actionKhatmahTriggered() { if (m_khatmahDlg == nullptr) { - m_khatmahDlg = new KhatmahDialog(m_currVerse, this); + m_khatmahDlg = new KhatmahDialog(this); connect(m_khatmahDlg, &KhatmahDialog::navigateToVerse, - this, - &MainWindow::navigateToVerse); + m_reader, + &QuranReader::navigateToVerse); } - m_dbMgr->saveActiveKhatmah(m_currVerse); + m_dbMgr->saveActiveKhatmah(m_currVerse->toList()); m_khatmahDlg->show(); } @@ -1238,13 +740,13 @@ MainWindow::actionAdvancedCopyTriggered() if (m_cpyDlg == nullptr) m_cpyDlg = new CopyDialog(this); - m_cpyDlg->show(m_currVerse); + m_cpyDlg->show(); } void MainWindow::actionTafsirTriggered() { - showVerseTafsir(m_currVerse); + showVerseTafsir(*m_currVerse); } void @@ -1273,137 +775,28 @@ MainWindow::actionSearchTriggered() m_searchDlg = new SearchDialog(this); connect(m_searchDlg, &SearchDialog::navigateToVerse, - this, - &MainWindow::navigateToVerse); + m_reader, + &QuranReader::navigateToVerse); } m_searchDlg->show(); } -void -MainWindow::toggleReaderView() -{ - if (ui->frmPageContent->isVisible() && ui->frmSidePanel->isVisible()) { - ui->frmSidePanel->setVisible(false); - } else if (ui->frmPageContent->isVisible()) { - ui->frmSidePanel->setVisible(true); - ui->frmPageContent->setVisible(false); - } else - ui->frmPageContent->setVisible(true); -} - -void -MainWindow::togglePlayerControls() -{ - ui->frmTopControls->setVisible(!ui->frmTopControls->isVisible()); -} - -void -MainWindow::navigateToVerse(Verse v) -{ - gotoPage(v.page, false); - - m_currVerse = v; - setVerseComboBoxRange(); - - setCmbPageIdx(m_currVerse.page - 1); - setCmbVerseIdx(m_currVerse.number - 1); - setCmbJuzIdx(m_dbMgr->getJuzOfPage(m_currVerse.page) - 1); - - syncSelectedSurah(); - highlightCurrentVerse(); - - m_player->setVerse(m_currVerse); - m_player->loadActiveVerse(); -} - void MainWindow::navigateToSurah(QModelIndex& index) { int s = index.row() + 1; - gotoSurah(s); -} - -void -MainWindow::updateLoadedTafsir() -{ - int currTafsir = m_settings->value("Reader/Tafsir").toInt(); - - m_dbMgr->setCurrentTafsir(currTafsir); -} - -void -MainWindow::updateLoadedTranslation() -{ - int currTrans = m_settings->value("Reader/Translation").toInt(); - - m_dbMgr->setCurrentTranslation(currTrans); -} - -void -MainWindow::updateSideFont() -{ - m_sideFont = - qvariant_cast(m_settings->value("Reader/SideContentFont")); -} - -void -MainWindow::updateVerseType() -{ - VerseType type = - qvariant_cast(m_settings->value("Reader/VerseType")); - m_versesFont.setFamily(Globals::verseFontname(type, m_currVerse.page)); - m_versesFont.setPointSize(m_settings->value("Reader/VerseFontSize").toInt()); - m_dbMgr->setVerseType(type); -} - -Verse -MainWindow::incrementVerse() -{ - Verse v = m_currVerse; - if (v == m_activeVList->last()) { - v.page++; - } - - if (v.number < m_surahCount) { - v.number++; - } else if (v.number == m_surahCount) { - v.surah++; - v.number = v.surah == 9 || v.surah == 1 ? 1 : 0; - } - - return v; -} - -Verse -MainWindow::decrementVerse() -{ - Verse v = m_currVerse; - if (v.number == 0) - v.number = 1; - - if (v == m_activeVList->at(0)) { - v.page--; - } - - if (v.number > 1) { - v.number--; - } else if (v.number <= 1) { - v.surah--; - v.number = m_dbMgr->getSurahVerseCount(v.surah); - } - - return v; + m_reader->gotoSurah(s); } void MainWindow::nextVerse() { - if (m_currVerse.surah == 114 && m_currVerse.number == 6) + if (m_currVerse->surah() == 114 && m_currVerse->number() == 6) return; bool keepPlaying = m_player->isOn(); - navigateToVerse(incrementVerse()); + m_reader->navigateToVerse(m_currVerse->next()); if (keepPlaying) m_player->play(); } @@ -1411,11 +804,11 @@ MainWindow::nextVerse() void MainWindow::prevVerse() { - if (m_currVerse.surah == 1 && m_currVerse.number == 1) + if (m_currVerse->surah() == 1 && m_currVerse->number() == 1) return; bool keepPlaying = m_player->isOn(); - navigateToVerse(decrementVerse()); + m_reader->navigateToVerse(m_currVerse->prev()); if (keepPlaying) m_player->play(); } @@ -1437,29 +830,15 @@ MainWindow::prevJuz() void MainWindow::nextSurah() { - if (m_currVerse.surah < 114) - gotoSurah(m_currVerse.surah + 1); + if (m_currVerse->surah() < 114) + m_reader->gotoSurah(m_currVerse->surah() + 1); } void MainWindow::prevSurah() { - if (m_currVerse.surah > 1) - gotoSurah(m_currVerse.surah - 1); -} - -void -MainWindow::incrementVolume() -{ - int val = ui->sldrVolume->value() + 5; - ui->sldrVolume->setValue(val > 100 ? 100 : val); -} - -void -MainWindow::decrementVolume() -{ - int val = ui->sldrVolume->value() - 5; - ui->sldrVolume->setValue(val < 0 ? 0 : val); + if (m_currVerse->surah() > 1) + m_reader->gotoSurah(m_currVerse->surah() - 1); } void @@ -1477,133 +856,12 @@ MainWindow::toggleNavDock() ui->sideDock->toggleViewAction()->toggle(); } -void -MainWindow::updateHighlight() -{ - for (int i = 0; i <= 1; i++) { - if (m_quranBrowsers[i]) { - m_quranBrowsers[i]->updateHighlightLayer(); - } - } -} - -void -MainWindow::redrawQuranPage(bool manualSz) -{ - if (m_activeQuranBrowser == m_quranBrowsers[0]) { - m_quranBrowsers[0]->constructPage(m_currVerse.page, manualSz); - if (m_readerMode == DoublePage && m_quranBrowsers[1]) - m_quranBrowsers[1]->constructPage(m_currVerse.page + 1, manualSz); - } else { - m_quranBrowsers[0]->constructPage(m_currVerse.page - 1, manualSz); - m_quranBrowsers[1]->constructPage(m_currVerse.page, manualSz); - } - - updatePageVerseInfoList(); -} - -void -MainWindow::highlightCurrentVerse() -{ - if (m_currVerse.number == 0) - return; - - // idx may be -1 if verse number is 0 (basmallah) - int idx = m_activeVList->indexOf(m_currVerse); - if (idx < 0) - idx = 0; - - m_activeQuranBrowser->highlightVerse(idx); - - if (m_readerMode == ReaderMode::SinglePage) - setHighlightedFrame(); -} - -void -MainWindow::setHighlightedFrame() -{ - if (m_highlightedFrm != nullptr) - m_highlightedFrm->setSelected(false); - - VerseFrame* verseFrame = - m_scrlVerseByVerse->widget()->findChild(QString("%0_%1").arg( - QString::number(m_currVerse.surah), QString::number(m_currVerse.number))); - - verseFrame->setSelected(true); - - m_scrlVerseByVerse->ensureWidgetVisible(verseFrame); - m_highlightedFrm = verseFrame; -} - -void -MainWindow::addSideContent() -{ - if (m_readerMode != ReaderMode::SinglePage) - return; - - if (!m_verseFrameList.isEmpty()) { - qDeleteAll(m_verseFrameList); - m_verseFrameList.clear(); - m_highlightedFrm = nullptr; - } - - ClickableLabel* verselb; - QLabel* contentLb; - VerseFrame* verseContFrame; - QString prevLbContent, currLbContent; - if (m_dbMgr->getVerseType() == qcf) - m_versesFont.setFamily(Globals::pageFontname(m_currVerse.page)); - - for (int i = m_activeVList->size() - 1; i >= 0; i--) { - Verse vInfo = m_activeVList->at(i); - - verseContFrame = new VerseFrame(m_scrlVerseByVerse->widget()); - verselb = new ClickableLabel(verseContFrame); - contentLb = new QLabel(verseContFrame); - - verseContFrame->setObjectName( - QString("%0_%1").arg(vInfo.surah).arg(vInfo.number)); - - verselb->setFont(m_versesFont); - verselb->setText(m_dbMgr->getVerseGlyphs(vInfo.surah, vInfo.number)); - verselb->setAlignment(Qt::AlignCenter); - verselb->setWordWrap(true); - - currLbContent = m_dbMgr->getTranslation(vInfo.surah, vInfo.number); - - if (currLbContent == prevLbContent) { - currLbContent = '-'; - } else { - prevLbContent = currLbContent; - } - - contentLb->setText(currLbContent); - contentLb->setTextFormat(Qt::RichText); - contentLb->setTextInteractionFlags(Qt::TextSelectableByMouse); - contentLb->setAlignment(Qt::AlignCenter); - contentLb->setWordWrap(true); - contentLb->setFont(m_sideFont); - - verseContFrame->layout()->addWidget(verselb); - verseContFrame->layout()->addWidget(contentLb); - m_scrlVerseByVerse->widget()->layout()->addWidget(verseContFrame); - m_verseFrameList.insert(0, verseContFrame); - - // connect clicked signal for each label - connect(verselb, &ClickableLabel::clicked, this, &MainWindow::verseClicked); - } - - if (m_player->playbackState() == QMediaPlayer::PlayingState) { - setHighlightedFrame(); - } -} - void MainWindow::showVerseTafsir(Verse v) { static bool reload = false; if (reload) { - updateLoadedTafsir(); + m_reader->updateLoadedTafsir(); reload = false; } @@ -1629,41 +887,15 @@ void MainWindow::copyVerseText(const Verse v) { QClipboard* clip = QApplication::clipboard(); - QString text = m_dbMgr->getVerseText(v.surah, v.number); - QString vNum = QString::number(v.number); + QString text = m_dbMgr->getVerseText(v.surah(), v.number()); + QString vNum = QString::number(v.number()); text.remove(text.size() - 1, 1); text = text.trimmed(); text = "{" + text + "}"; text += ' '; - text += "[" + m_dbMgr->surahNameList().at(v.surah - 1) + ":" + vNum + "]"; + text += "[" + m_dbMgr->surahNameList().at(v.surah() - 1) + ":" + vNum + "]"; clip->setText(text); -} - -void -MainWindow::showVOTDmessage(QPair votd) -{ - QPointer mbox = new QDialog(this); - mbox->setMinimumSize(600, 300); - mbox->setObjectName("dlgVOTD"); - mbox->setLayout(new QVBoxLayout); - mbox->setWindowIcon(ui->actionVOTD->icon()); - mbox->setWindowTitle(tr("Verse Of The Day")); - ClickableLabel* lb = new ClickableLabel(mbox); - lb->setText(votd.second); - lb->setTextFormat(Qt::RichText); - lb->setAlignment(Qt::AlignCenter); - lb->setFont(QFont(qApp->font().families(), 15)); - lb->setCursor(Qt::PointingHandCursor); - if (votd.second.length() > 200) - lb->setWordWrap(true); - - connect(lb, &ClickableLabel::clicked, this, [votd, this, mbox]() { - mbox->close(); - navigateToVerse(votd.first); - }); - - mbox->layout()->addWidget(lb); - mbox->show(); + m_popup->copiedToClipboard(); } void @@ -1679,10 +911,10 @@ void MainWindow::saveReaderState() { m_settings->setValue("WindowState", saveState()); - m_settings->setValue("Reciter", ui->cmbReciter->currentIndex()); + m_settings->setValue("Reciter", m_playerControls->currentReciter()); m_settings->sync(); - m_dbMgr->saveActiveKhatmah(m_currVerse); + m_dbMgr->saveActiveKhatmah(m_currVerse->toList()); } void diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 75a11e48..985b3b6b 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -8,18 +8,19 @@ #include "../globals.h" #include "../utils/dbmanager.h" -#include "../utils/notificationmanager.h" #include "../utils/shortcuthandler.h" +#include "../utils/systemtray.h" +#include "../utils/verse.h" #include "../utils/verseplayer.h" #include "../widgets/betaqaviewer.h" #include "../widgets/notificationpopup.h" -#include "../widgets/quranpagebrowser.h" #include "../widgets/versedialog.h" -#include "../widgets/verseframe.h" #include "bookmarksdialog.h" #include "copydialog.h" #include "downloaderdialog.h" #include "khatmahdialog.h" +#include "playercontrols.h" +#include "quranreader.h" #include "searchdialog.h" #include "settingsdialog.h" #include "tafsirdialog.h" @@ -58,22 +59,9 @@ class MainWindow : public QMainWindow explicit MainWindow(QWidget* parent = nullptr); ~MainWindow(); - /** - * @brief highlight the currently active ::Verse m_currVerse in the - * active QuranPageBrowser and the side panel depending on the ::ReaderMode - */ - void highlightCurrentVerse(); - /** - * @brief highlight the currently active VerseFrame - */ - void setHighlightedFrame(); - public slots: - /** - * @brief show the verse of the day dialog - * @param votd - QPair of the ::Verse of the day and the verse text - */ - void showVOTDmessage(QPair votd); + void currentVerseChanged(); + void currentSurahChanged(); /** * @brief check if there are updates using the maintainence tool if available, * otherwise check latest version on github @@ -103,43 +91,6 @@ public slots: void resizeEvent(QResizeEvent* event); private slots: - /** - * @brief navigates to the next page relative to the current page - */ - void nextPage(int step = 1); - /** - * @brief navigates to the previous page relative to the current page - */ - void prevPage(int step = 1); - /** - * @brief ensure the page given is visible and update other members to match - * the properties of the first verse in the page, calls the appropriate - * navigation function according to the ::ReaderMode - * @param page - page to navigate to - * @param updateElements - boolean flag to indicate whether to update other - * elements or not - * @param automaticFlip - boolean indicating whether the function was called - * by internal signal to automatically flip the page - */ - void gotoPage(int page, - bool updateElements = true, - bool automaticFlip = false); - /** - * @brief single page mode navigation - * @param page - page to navigate to - */ - void gotoSinglePage(int page); - /** - * @brief double page mode navigation - * @param page - page to navigate to - */ - void gotoDoublePage(int page); - /** - * @brief gets the page of the 1st verse in this surah, moves to that page, - * and starts playback of the surah - * @param surahIdx - surah number (1-114) - */ - void gotoSurah(int surahIdx); /** * @brief sets the verse combobox values according to the current surah verse * count (m_surahCount), sets the current verse as the visible index @@ -147,19 +98,6 @@ private slots: * verse count */ void setVerseComboBoxRange(bool forceUpdate = false); - /** - * @brief continues playback of the current verse - */ - void btnPlayClicked(); - /** - * @brief pause playback of the current verse - */ - void btnPauseClicked(); - /** - * @brief stops playback, sets the current verse to the 1st in the page and - * update verses combobox & selected surah - */ - void btnStopClicked(); /** * @brief slot for updating the reader page as the user selects a different * page from the combobox @@ -194,18 +132,6 @@ private slots: * @param idx - index of tafsir in Globals::tafasirList */ void missingTafsir(int idx); - /** - * @brief sets the current position in the audio file as the position of the - * slider - * @param position - position in audio file in milliseconds - */ - void mediaPosChanged(qint64 position); - /** - * @brief disables/enables control buttons according to the media player state - * and update the systray tooltip - * @param state - playback state of the current audio file - */ - void mediaStateChanged(QMediaPlayer::PlaybackState state); /** * @brief move to the next verse as the playback of the current verse ends * @param status - the status of the current verse recitation media @@ -217,19 +143,10 @@ private slots: * @param state - playback state of the current verse */ void updateTrayTooltip(QMediaPlayer::PlaybackState state); - /** - * @brief change the player volume level as the volume slider changes - * @param position - position in the slider (0 - 100) - */ - void volumeSliderValueChanged(int position); /** * @brief adds the current ::Verse to the bookmarks */ void addCurrentToBookmarks(); - /** - * @brief toggle play/pause of the current verse - */ - void togglePlayback(); /** * @brief open the SettingsDialog and connect settings change slots */ @@ -262,14 +179,6 @@ private slots: * @brief open the SearchDialog, create instance if not set */ void actionSearchTriggered(); - /** - * @brief toggle the main reader view by hiding one of the panels - */ - void toggleReaderView(); - /** - * @brief toggle the visibility of the player controls in the main window - */ - void togglePlayerControls(); /** * @brief open the about messagebox for the application */ @@ -279,65 +188,44 @@ private slots: */ void on_actionAboutQt_triggered(); /** - * @brief slot to navigate to the clicked verse in the side panel and update - * UI elements - */ - void verseClicked(); - /** - * @brief navigate to the surah with the given QModelIndex in the surahs list - * model - */ - void navigateToSurah(QModelIndex& index); - /** - * @brief open TafsirDialog with the shown verse set to the given ::Verse - * @param v - ::Verse to show the tafsir of - */ - void showVerseTafsir(Verse v); - /** - * @brief navigate to the given ::Verse and update UI elements accordingly - * @param v - ::Verse to navigate to + * @brief move to the next ::Verse relative to m_currVerse */ - void navigateToVerse(Verse v); + void nextVerse(); /** - * @brief callback function for clicking verses in the QuranPageBrowser that - * takes actions based on the chosen option in the menu - * @param hrefUrl - "#idx" where idx is the verse index relative to the start - * of the page (=index in the page ::Verse QList) + * @brief move to the previous ::Verse relative to m_currVerse */ - void verseAnchorClicked(const QUrl& hrefUrl); + void prevVerse(); /** - * @brief copy to clipboard the text of the verse with the given index - * @param IdxInPage - verse index relative to the start of the page + * @brief move to the next Juz */ - void copyVerseText(const Verse v); + void nextJuz(); /** - * @brief redraw the current Quran page - * @param manualSz - boolean flag to force the use of the manually set - * fontsize + * @brief move to the previous Juz */ - void redrawQuranPage(bool manualSz = false); + void prevJuz(); /** - * @brief updates the side panel with the translation of the current page - * verses + * @brief move to the next surah */ - void addSideContent(); + void nextSurah(); /** - * @brief set tafsir to the one in the settings, update the selected db + * @brief move to the previous surah */ - void updateLoadedTafsir(); + void prevSurah(); /** - * @brief set translation to the one in the settings, update the selected db + * @brief navigate to the surah with the given QModelIndex in the surahs list + * model */ - void updateLoadedTranslation(); + void navigateToSurah(QModelIndex& index); /** - * @brief set side content font to the one in the settings + * @brief open TafsirDialog with the shown verse set to the given ::Verse + * @param v - ::Verse to show the tafsir of */ - void updateSideFont(); + void showVerseTafsir(Verse v); /** - * @brief Updates the type of the verses shown and reload the font family and - * size + * @brief copy to clipboard the text of the verse with the given index + * @param IdxInPage - verse index relative to the start of the page */ - void updateVerseType(); + void copyVerseText(const Verse v); /** * @brief search for the surahs with the given argument when the text in the * side dock search box is changed @@ -350,48 +238,6 @@ private slots: * @param index - QModelIndex of the clicked surah */ void listSurahNameClicked(const QModelIndex& index); - /** - * @brief increment the current active ::Verse appropriately - * @return the new incremented ::Verse - */ - Verse incrementVerse(); - /** - * @brief decrement the current active ::Verse appropriately - * @return the new decremented ::Verse - */ - Verse decrementVerse(); - /** - * @brief move to the next ::Verse relative to m_currVerse - */ - void nextVerse(); - /** - * @brief move to the previous ::Verse relative to m_currVerse - */ - void prevVerse(); - /** - * @brief move to the next Juz - */ - void nextJuz(); - /** - * @brief move to the previous Juz - */ - void prevJuz(); - /** - * @brief move to the next surah - */ - void nextSurah(); - /** - * @brief move to the previous surah - */ - void prevSurah(); - /** - * @brief utility to increment the VersePlayer playback volume by steps of 5 - */ - void incrementVolume(); - /** - * @brief utility to decrement the VersePlayer playback volume by steps of 5 - */ - void decrementVolume(); /** * @brief toggles visiblity of menubar */ @@ -400,21 +246,16 @@ private slots: * @brief toggles visiblity of the navigation dock */ void toggleNavDock(); - /** - * @brief update the highlight layer (fg/bg) used by the Quran browser(s) - */ - void updateHighlight(); private: - const bool m_darkMode = Globals::darkMode; - const QLocale::Language& m_language = Globals::language; + Verse* m_currVerse = Verse::current(); QSettings* const m_settings = Globals::settings; const QList& m_recitersList = Globals::recitersList; const QList& m_tafasirList = Globals::tafasirList; const QString& m_updateToolPath = Globals::updateToolPath; - const ReaderMode& m_readerMode = Globals::readerMode; DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); fa::QtAwesome* m_fa = Globals::awesome; + Ui::MainWindow* ui; /** * @brief initalizes different parts used by the app */ @@ -426,45 +267,7 @@ private slots: /** * @brief set the current ::Verse from settings */ - void loadSettings(); - /** - * @brief setup the reader layout and create widgets according to the current - * ::ReaderMode - */ - void loadReader(); - /** - * @brief utility function to check whether 2 pages are beside each other in - * 2-page mode - * @details 2 pages are considered neighbors if the right page is odd and the - * left page is directly after the right page - * @param page1 - the right side page - * @param page2 - the left side page - * @return boolean value indicating whether the given pages are neighbors - */ - bool areNeighbors(int page1, int page2); - /** - * @brief 2-page mode utility to switch the active page & verse info list to - * the opposite side of the current - * @details the currently active page is recogonized through - * m_activeQuranBrowser and m_activeVList pointers - */ - void switchActivePage(); - /** - * @brief flip the current page/2-pages to the next page/2-pages - */ - void btnNextClicked(); - /** - * @brief flip the current page/2-pages to the previous page/2-pages - */ - void btnPrevClicked(); - /** - * @brief selects one of the verses in the currently displayed page(s) - * @param browserIdx - index of the QuranPageBrowser which contains the target - * verse - * @param IdxInPage - index of the verse relative to the start of the - * page - */ - void selectVerse(int browserIdx, int IdxInPage); + void loadCurrent(); /** * @brief connect ShortcutHandler signals to their corresponding slots */ @@ -490,20 +293,6 @@ private slots: * @return QModelIndex of the currently selected surah */ QModelIndex syncSelectedSurah(); - /** - * @brief updates m_surahCount to match the verse count of the currently - * active verse - */ - void updateSurahVerseCount(); - /** - * @brief updates the list that contains::Verse instances for verses in the - * current page - */ - void updatePageVerseInfoList(); - /** - * @brief sets m_currVerse to the first verse in m_vInfoList - */ - void setVerseToStartOfPage(); /** * @brief set the index of the page combobox without signalling other slots * @param idx - new index to set the combobox to @@ -539,41 +328,16 @@ private slots: * change of juz combobox index */ bool m_internalJuzChange = false; - /** - * @brief verse count for the surah of the current active ::Verse - */ - int m_surahCount = 0; - /** - * @brief float value of the current playback volume (0 - 1.0) - */ - qreal m_volume = 1; - /** - * @brief Pointer to access ui elements generated from .ui files - */ - Ui::MainWindow* ui; + QuranReader* m_reader = nullptr; + PlayerControls* m_playerControls = nullptr; /** * @brief ShortcutHandler instance for handling shortcuts */ ShortcutHandler* m_shortcutHandler = nullptr; /** - * @brief QScrollArea used in single page mode to display verses & - * translation - */ - QScrollArea* m_scrlVerseByVerse = nullptr; - /** - * @brief pointer to currently active QuranPageBrowser instance, must be one - * of the values in m_quranBrowsers array - */ - QuranPageBrowser* m_activeQuranBrowser = nullptr; - /** - * @brief array of QuranPageBrowser instances used in different modes, index 0 - * is used in both modes - */ - QuranPageBrowser* m_quranBrowsers[2]{}; - /** - * @brief pointer to NotificationManager instance + * @brief pointer to SystemTray instance */ - NotificationManager* m_notifyMgr = nullptr; + SystemTray* m_systemTray = nullptr; /** * @brief pointer to NotificationPopup instance */ @@ -614,10 +378,6 @@ private slots: * @brief pointer to DownloadManager instance */ DownloadManager* m_downManPtr = nullptr; - /** - * @brief pointer to the currently highlighted VerseFrame in the side panel - */ - VerseFrame* m_highlightedFrm = nullptr; /** * @brief pointer to the surah card (betaqa) widget */ @@ -626,24 +386,6 @@ private slots: * @brief pointer to the votd dialog */ VerseDialog* m_verseDlg = nullptr; - /** - * @brief the currently selected ::Verse - */ - Verse m_currVerse{ 1, 1, 1 }; - /** - * @brief QList of QFrame pointers to VerseFrame elements in the single page - * mode side panel - */ - QList m_verseFrameList; - /** - * @brief pointer to the currently active page ::Verse list - */ - const QList* m_activeVList; - /** - * @brief array of 2 QLists of ::Verse instances for the verses in the - * displayed page(s), index 0 is used in both reader modes - */ - QList m_vLists[2]; /** * @brief pointer to the QProcess instance of the maintainence tool that * checks for updates @@ -663,13 +405,5 @@ private slots: * list of surahs */ QStringListModel m_surahListModel; - /** - * @brief QFont used in the side panel translation - */ - QFont m_sideFont; - /** - * @brief QFont used in displaying Quranic verse - */ - QFont m_versesFont; }; #endif // MAINWINDOW_H diff --git a/src/core/mainwindow.ui b/src/core/mainwindow.ui index b82a513e..f6129741 100644 --- a/src/core/mainwindow.ui +++ b/src/core/mainwindow.ui @@ -25,14 +25,7 @@ false - QDialogButtonBox { -dialogbuttonbox-buttons-have-icons: true; -dialog-apply-icon: off; -dialog-cancel-icon: off; -dialog-ok-icon: off; -dialog-yes-icon: off; -dialog-no-icon: off; -} + QDialogButtonBox { dialogbuttonbox-buttons-have-icons: true; dialog-apply-icon: off; dialog-cancel-icon: off; dialog-ok-icon: off; dialog-yes-icon: off; dialog-no-icon: off; } false @@ -77,595 +70,22 @@ dialog-no-icon: off; 933 - + - 9 + 0 0 - 9 + 4 0 - 9 + 4 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 66 - 0 - - - - - - - - - 0 - 0 - - - - - 900 - 16777215 - - - - - 20 - - - 6 - - - 20 - - - 6 - - - - - - 50 - 16777215 - - - - Reciter - - - - - - - - 0 - 0 - - - - - 150 - 0 - - - - - 150 - 16777215 - - - - PointingHandCursor - - - - - - - - 0 - 0 - - - - - 200 - 0 - - - - - 350 - 16777215 - - - - PointingHandCursor - - - Qt::Horizontal - - - - - - - - 0 - 0 - - - - - 42 - 27 - - - - - 52 - 27 - - - - PointingHandCursor - - - QPushButton { - min-width: 40px; - min-height: 25px; - max-width: 50px; - max-height: 25px; -} - - - - - - - :/resources/play.png - - - - false - - - - - - - - 0 - 0 - - - - - 42 - 27 - - - - - 52 - 27 - - - - - Noto Sans Display - - - - PointingHandCursor - - - QPushButton { - min-width: 40px; - min-height: 25px; - max-width: 50px; - max-height: 25px; -} - - - - - - - :/resources/pause.png - - - - - - - - - 0 - 0 - - - - - 42 - 27 - - - - - 52 - 27 - - - - PointingHandCursor - - - QPushButton { - min-width: 40px; - min-height: 25px; - max-width: 50px; - max-height: 25px; -} - - - - - - - :/resources/stop.png - - - - - - - - - 0 - 0 - - - - - 160 - 0 - - - - - 200 - 16777215 - - - - PointingHandCursor - - - - - - 100 - - - 5 - - - 100 - - - Qt::Horizontal - - - false - - - false - - - - - - - - 0 - 0 - - - - - 25 - 25 - - - - - 25 - 25 - - - - - Noto Sans Display - 12 - - - - - - - Qt::PlainText - - - true - - - Qt::AlignCenter - - - 0 - - - - - - - - - - Qt::Horizontal - - - - 66 - 0 - - - - - - - - - - - - 0 - 0 - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 60 - 30 - - - - - Noto Sans Display - 12 - true - - - - PointingHandCursor - - - next - - - QPushButton { - min-width: 60px; - text-align: center; -} - - - - - - false - - - - - - - - 2 - - - 4 - - - 4 - - - 4 - - - 4 - - - - - QFrame#frmSidePanel -{ - background: transparent; -} - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - 0 - 0 - - - - - 550 - 0 - - - - *.a { - background-color:palette(base); -} - - - QFrame::NoFrame - - - QFrame::Raised - - - 0 - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - - 0 - 0 - - - - - 62 - 0 - - - - - 60 - 30 - - - - - Noto Sans Display - 12 - - - - PointingHandCursor - - - previous - - - QPushButton { - min-width: 60px; - text-align: center; -} - - - - - - - - - @@ -965,9 +385,7 @@ dialog-no-icon: off; - QLineEdit{ -placeholder-text-color: #c0c0c0; -} + QLineEdit{ placeholder-text-color: #c0c0c0; } Search surah diff --git a/src/core/playercontrols.cpp b/src/core/playercontrols.cpp new file mode 100644 index 00000000..edac9d48 --- /dev/null +++ b/src/core/playercontrols.cpp @@ -0,0 +1,195 @@ +#include "playercontrols.h" +#include "ui_playercontrols.h" +using namespace fa; + +PlayerControls::PlayerControls(QWidget* parent, + VersePlayer* player, + QuranReader* reader, + ShortcutHandler* handler) + : QWidget(parent) + , ui(new Ui::PlayerControls) + , m_player(player) + , m_reader(reader) +{ + ui->setupUi(this); + loadIcons(); + setupShortcuts(handler); + + foreach (const Reciter& r, m_recitersList) + ui->cmbReciter->addItem(r.displayName); + + ui->cmbReciter->setCurrentIndex(m_settings->value("Reciter", 0).toInt()); +} + +void +PlayerControls::loadIcons() +{ + ui->btnPlay->setIcon(m_fa->icon(fa_solid, fa_play)); + ui->btnPause->setIcon(m_fa->icon(fa_solid, fa_pause)); + ui->btnStop->setIcon(m_fa->icon(fa_solid, fa_stop)); + + ui->lbSpeaker->setText(QString(fa_volume_high)); + ui->lbSpeaker->setFont(m_fa->font(fa_solid, 16)); +} + +void +PlayerControls::setupConnections() +{ +} + +void +PlayerControls::setupShortcuts(ShortcutHandler* handler) +{ + + connect(handler, + &ShortcutHandler::togglePlayerControls, + this, + &PlayerControls::toggleVisibility); + connect(handler, + &ShortcutHandler::togglePlayback, + this, + &PlayerControls::togglePlayback); + connect(handler, + &ShortcutHandler::incrementVolume, + this, + &PlayerControls::incrementVolume); + connect(handler, + &ShortcutHandler::decrementVolume, + this, + &PlayerControls::decrementVolume); + + connect(m_player, + &QMediaPlayer::positionChanged, + this, + &PlayerControls::mediaPosChanged); + connect(m_player, + &QMediaPlayer::playbackStateChanged, + this, + &PlayerControls::mediaStateChanged); + + connect(ui->sldrAudioPlayer, + &QSlider::sliderMoved, + m_player, + &QMediaPlayer::setPosition); + connect(ui->sldrVolume, + &QSlider::valueChanged, + this, + &PlayerControls::volumeSliderValueChanged); + + connect( + ui->btnPlay, &QPushButton::clicked, this, &PlayerControls::btnPlayClicked); + connect(ui->btnPause, + &QPushButton::clicked, + this, + &PlayerControls::btnPauseClicked); + connect( + ui->btnStop, &QPushButton::clicked, this, &PlayerControls::btnStopClicked); + + connect(ui->cmbReciter, + &QComboBox::currentIndexChanged, + m_player, + &VersePlayer::changeReciter); +} + +void +PlayerControls::togglePlayback() +{ + if (m_player->playbackState() == QMediaPlayer::PlayingState) { + btnPauseClicked(); + } else { + btnPlayClicked(); + } +} + +void +PlayerControls::mediaPosChanged(qint64 position) +{ + if (ui->sldrAudioPlayer->maximum() != m_player->duration()) + ui->sldrAudioPlayer->setMaximum(m_player->duration()); + + if (!ui->sldrAudioPlayer->isSliderDown()) + ui->sldrAudioPlayer->setValue(position); +} + +void +PlayerControls::btnPlayClicked() +{ + m_reader->highlightCurrentVerse(); + m_player->play(); +} + +void +PlayerControls::btnPauseClicked() +{ + m_player->pause(); +} + +void +PlayerControls::btnStopClicked() +{ + m_player->stop(); + m_reader->setVerseToStartOfPage(); + emit currentSurahChanged(); +} + +void +PlayerControls::mediaStateChanged(QMediaPlayer::PlaybackState state) +{ + if (state == QMediaPlayer::PlayingState) { + ui->btnPlay->setEnabled(false); + ui->btnPause->setEnabled(true); + ui->btnStop->setEnabled(true); + } else if (state == QMediaPlayer::PausedState) { + ui->btnPlay->setEnabled(true); + ui->btnPause->setEnabled(false); + ui->btnStop->setEnabled(true); + } else if (state == QMediaPlayer::StoppedState) { + ui->btnPlay->setEnabled(true); + ui->btnPause->setEnabled(false); + ui->btnStop->setEnabled(false); + } +} + +void +PlayerControls::volumeSliderValueChanged(int position) +{ + qreal linearVolume = + QAudio::convertVolume(ui->sldrVolume->value() / qreal(100.0), + QAudio::LogarithmicVolumeScale, + QAudio::LinearVolumeScale); + if (linearVolume != m_volume) { + m_volume = linearVolume; + m_player->setPlayerVolume(m_volume); + } +} + +void +PlayerControls::toggleVisibility() +{ + setVisible(!isVisible()); +} + +void +PlayerControls::incrementVolume() +{ + int val = ui->sldrVolume->value() + 5; + ui->sldrVolume->setValue(val > 100 ? 100 : val); +} + +void +PlayerControls::decrementVolume() +{ + int val = ui->sldrVolume->value() - 5; + ui->sldrVolume->setValue(val < 0 ? 0 : val); +} + +int +PlayerControls::currentReciter() const +{ + return ui->cmbReciter->currentIndex(); +} + +PlayerControls::~PlayerControls() +{ + delete ui; +} diff --git a/src/core/playercontrols.h b/src/core/playercontrols.h new file mode 100644 index 00000000..c99f0744 --- /dev/null +++ b/src/core/playercontrols.h @@ -0,0 +1,111 @@ +#ifndef PLAYERCONTROLS_H +#define PLAYERCONTROLS_H + +#include "../globals.h" +#include "../utils/shortcuthandler.h" +#include "../utils/verse.h" +#include "../utils/verseplayer.h" +#include "quranreader.h" +#include + +namespace Ui { +class PlayerControls; +} + +class PlayerControls : public QWidget +{ + Q_OBJECT + +public: + explicit PlayerControls(QWidget* parent, + VersePlayer* player, + QuranReader* reader, + ShortcutHandler* handler); + ~PlayerControls(); + + int currentReciter() const; + +public slots: + /** + * @brief toggle play/pause of the current verse + */ + void togglePlayback(); + +signals: + void currentSurahChanged(); + +private slots: + /** + * @brief sets the current position in the audio file as the position of the + * slider + * @param position - position in audio file in milliseconds + */ + void mediaPosChanged(qint64 position); + /** + * @brief continues playback of the current verse + */ + void btnPlayClicked(); + /** + * @brief pause playback of the current verse + */ + void btnPauseClicked(); + /** + * @brief stops playback, sets the current verse to the 1st in the page and + * update verses combobox & selected surah + */ + void btnStopClicked(); + /** + * @brief disables/enables control buttons according to the media player state + * and update the systray tooltip + * @param state - playback state of the current audio file + */ + void mediaStateChanged(QMediaPlayer::PlaybackState state); + /** + * @brief change the player volume level as the volume slider changes + * @param position - position in the slider (0 - 100) + */ + void volumeSliderValueChanged(int position); + /** + * @brief toggle the visibility of the player controls in the main window + */ + void toggleVisibility(); + /** + * @brief utility to increment the VersePlayer playback volume by steps of 5 + */ + void incrementVolume(); + /** + * @brief utility to decrement the VersePlayer playback volume by steps of 5 + */ + void decrementVolume(); + +private: + Verse* m_currVerse = Verse::current(); + QSettings* const m_settings = Globals::settings; + const QList& m_recitersList = Globals::recitersList; + DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + fa::QtAwesome* m_fa = Globals::awesome; + Ui::PlayerControls* ui; + /** + * @brief load icons for different UI elements + */ + void loadIcons(); + /** + * @brief connects signals and slots for different UI components + */ + void setupConnections(); + /** + * @brief connect ShortcutHandler signals to their corresponding slots + */ + void setupShortcuts(ShortcutHandler* handler); + /** + * @brief float value of the current playback volume (0 - 1.0) + */ + qreal m_volume = 1; + /** + * @brief pointer to VersePlayer instance + */ + VersePlayer* m_player = nullptr; + QuranReader* m_reader = nullptr; +}; + +#endif // PLAYERCONTROLS_H diff --git a/src/core/playercontrols.ui b/src/core/playercontrols.ui new file mode 100644 index 00000000..88c0681b --- /dev/null +++ b/src/core/playercontrols.ui @@ -0,0 +1,317 @@ + + + PlayerControls + + + + 0 + 0 + 900 + 80 + + + + + 0 + 0 + + + + + 900 + 16777215 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 4 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 25 + + + 5 + + + 25 + + + 5 + + + + + + 50 + 16777215 + + + + Reciter + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + 150 + 16777215 + + + + PointingHandCursor + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + + 350 + 16777215 + + + + PointingHandCursor + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + + 42 + 27 + + + + + 52 + 27 + + + + PointingHandCursor + + + QPushButton { min-width: 40px; min-height: 25px; max-width: 50px; max-height: 25px; } + + + + + + + + 0 + 0 + + + + + 42 + 27 + + + + + 52 + 27 + + + + + Noto Sans Display + 10 + + + + PointingHandCursor + + + QPushButton { min-width: 40px; min-height: 25px; max-width: 50px; max-height: 25px; } + + + + + + + + 0 + 0 + + + + + 42 + 27 + + + + + 52 + 27 + + + + PointingHandCursor + + + QPushButton { min-width: 40px; min-height: 25px; max-width: 50px; max-height: 25px; } + + + + + + + + 0 + 0 + + + + + 160 + 0 + + + + + 200 + 16777215 + + + + PointingHandCursor + + + + + + 100 + + + 5 + + + 100 + + + Qt::Horizontal + + + false + + + false + + + + + + + + 0 + 0 + + + + + 25 + 25 + + + + + 25 + 25 + + + + + Noto Sans Display + 12 + + + + + + + Qt::PlainText + + + true + + + Qt::AlignCenter + + + 0 + + + + + + + + + + + diff --git a/src/core/quranreader.cpp b/src/core/quranreader.cpp new file mode 100644 index 00000000..c8736c27 --- /dev/null +++ b/src/core/quranreader.cpp @@ -0,0 +1,582 @@ +#include "quranreader.h" +#include "../widgets/clickablelabel.h" +#include "ui_quranreader.h" +using namespace fa; + +QuranReader::QuranReader(QWidget* parent, + VersePlayer* player, + ShortcutHandler* handler) + : QWidget(parent) + , ui(new Ui::QuranReader) + , m_player(player) +{ + ui->setupUi(this); + loadIcons(); + loadReader(); + updateHighlight(); + updateLoadedTafsir(); + updateLoadedTranslation(); + updateSideFont(); + updateVerseType(); + redrawQuranPage(true); + if (m_readerMode == ReaderMode::SinglePage) + addSideContent(); + + setupConnections(); + setupShortcuts(handler); +} + +void +QuranReader::loadIcons() +{ + ui->btnNext->setIcon(m_fa->icon(fa_solid, fa_arrow_left)); + ui->btnPrev->setIcon(m_fa->icon(fa_solid, fa_arrow_right)); +} + +void +QuranReader::loadReader() +{ + if (m_readerMode == ReaderMode::SinglePage) { + m_activeQuranBrowser = m_quranBrowsers[0] = + new QuranPageBrowser(ui->frmPageContent, m_currVerse->page()); + + QWidget* scrollWidget = new QWidget(); + scrollWidget->setObjectName("scrollWidget"); + QVBoxLayout* vbl = new QVBoxLayout(); + vbl->setDirection(QBoxLayout::BottomToTop); + scrollWidget->setLayout(vbl); + + m_scrlVerseByVerse = new QScrollArea; + m_scrlVerseByVerse->setWidget(scrollWidget); + m_scrlVerseByVerse->setWidgetResizable(true); + m_scrlVerseByVerse->setStyleSheet( + "QLabel, QAbstractScrollArea, QWidget#scrollWidget { " + "background: " + "transparent }"); + + QHBoxLayout* lyt = qobject_cast(ui->frmReader->layout()); + ui->frmSidePanel->layout()->addWidget(m_scrlVerseByVerse); + lyt->setStretch(0, 1); + lyt->setStretch(1, 0); + this->setMinimumWidth(900); + } + + else { + // even Quran pages are always on the left side + if (m_currVerse->page() % 2 == 0) { + m_quranBrowsers[0] = + new QuranPageBrowser(ui->frmPageContent, m_currVerse->page() - 1); + m_activeQuranBrowser = m_quranBrowsers[1] = + new QuranPageBrowser(ui->frmSidePanel, m_currVerse->page()); + + } else { + m_activeQuranBrowser = m_quranBrowsers[0] = + new QuranPageBrowser(ui->frmPageContent, m_currVerse->page()); + m_quranBrowsers[1] = + new QuranPageBrowser(ui->frmSidePanel, m_currVerse->page() + 1); + } + + ui->frmSidePanel->layout()->addWidget(m_quranBrowsers[1]); + QHBoxLayout* lyt = qobject_cast(ui->frmReader->layout()); + lyt->insertSpacerItem(0, new QSpacerItem(20, 20, QSizePolicy::Expanding)); + lyt->addSpacerItem(new QSpacerItem(20, 20, QSizePolicy::Expanding)); + + lyt->setStretch(0, 1); + lyt->setStretch(1, 0); + lyt->setStretch(2, 0); + lyt->setStretch(3, 1); + } + + ui->frmPageContent->layout()->addWidget(m_quranBrowsers[0]); +} + +void +QuranReader::setupConnections() +{ + connect( + ui->btnNext, &QPushButton::clicked, this, &QuranReader::btnNextClicked); + connect( + ui->btnPrev, &QPushButton::clicked, this, &QuranReader::btnPrevClicked); + for (int i = 0; i <= 1; i++) + if (m_quranBrowsers[i]) + connect(m_quranBrowsers[i], + &QTextBrowser::anchorClicked, + this, + &QuranReader::verseAnchorClicked); + connect(this, + &QuranReader::currentVerseChanged, + m_player, + &VersePlayer::loadActiveVerse); +} + +void +QuranReader::setupShortcuts(ShortcutHandler* handler) +{ + connect(handler, + &ShortcutHandler::toggleReaderView, + this, + &QuranReader::toggleReaderView); + + connect( + handler, &ShortcutHandler::nextPage, this, &QuranReader::btnNextClicked); + connect( + handler, &ShortcutHandler::prevPage, this, &QuranReader::btnPrevClicked); + + for (int i = 0; i <= 1; i++) { + if (m_quranBrowsers[i]) { + connect(handler, + &ShortcutHandler::zoomIn, + m_quranBrowsers[i], + &QuranPageBrowser::actionZoomIn); + connect(handler, + &ShortcutHandler::zoomOut, + m_quranBrowsers[i], + &QuranPageBrowser::actionZoomOut); + } + } +} + +void +QuranReader::toggleReaderView() +{ + if (ui->frmPageContent->isVisible() && ui->frmSidePanel->isVisible()) { + ui->frmSidePanel->setVisible(false); + } else if (ui->frmPageContent->isVisible()) { + ui->frmSidePanel->setVisible(true); + ui->frmPageContent->setVisible(false); + } else + ui->frmPageContent->setVisible(true); +} + +void +QuranReader::updateLoadedTafsir() +{ + int currTafsir = m_settings->value("Reader/Tafsir").toInt(); + m_dbMgr->setCurrentTafsir(currTafsir); +} + +void +QuranReader::updateLoadedTranslation() +{ + int currTrans = m_settings->value("Reader/Translation").toInt(); + m_dbMgr->setCurrentTranslation(currTrans); +} + +void +QuranReader::updateSideFont() +{ + m_sideFont = + qvariant_cast(m_settings->value("Reader/SideContentFont")); +} + +void +QuranReader::updateVerseType() +{ + VerseType type = + qvariant_cast(m_settings->value("Reader/VerseType")); + m_versesFont.setFamily(Globals::verseFontname(type, m_currVerse->page())); + m_versesFont.setPointSize(m_settings->value("Reader/VerseFontSize").toInt()); + m_dbMgr->setVerseType(type); +} + +void +QuranReader::updateHighlight() +{ + for (int i = 0; i <= 1; i++) { + if (m_quranBrowsers[i]) { + m_quranBrowsers[i]->updateHighlightLayer(); + } + } +} + +void +QuranReader::updatePageFontSize() +{ + for (int i = 0; i <= 1; i++) + if (m_quranBrowsers[i]) + m_quranBrowsers[i]->updateFontSize(); +} + +void +QuranReader::redrawQuranPage(bool manualSz) +{ + if (m_activeQuranBrowser == m_quranBrowsers[0]) { + m_quranBrowsers[0]->constructPage(m_currVerse->page(), manualSz); + if (m_readerMode == DoublePage && m_quranBrowsers[1]) + m_quranBrowsers[1]->constructPage(m_currVerse->page() + 1, manualSz); + } else { + m_quranBrowsers[0]->constructPage(m_currVerse->page() - 1, manualSz); + m_quranBrowsers[1]->constructPage(m_currVerse->page(), manualSz); + } + + updatePageVerseInfoList(); +} + +void +QuranReader::addSideContent() +{ + if (m_readerMode != ReaderMode::SinglePage) + return; + + if (!m_verseFrameList.isEmpty()) { + qDeleteAll(m_verseFrameList); + m_verseFrameList.clear(); + m_highlightedFrm = nullptr; + } + + ClickableLabel* verselb; + QLabel* contentLb; + VerseFrame* verseContFrame; + QString prevLbContent, currLbContent; + if (m_dbMgr->getVerseType() == qcf) + m_versesFont.setFamily(Globals::pageFontname(m_currVerse->page())); + + for (int i = m_activeVList->size() - 1; i >= 0; i--) { + const Verse* vInfo = &(m_activeVList->at(i)); + + verseContFrame = new VerseFrame(m_scrlVerseByVerse->widget()); + verselb = new ClickableLabel(verseContFrame); + contentLb = new QLabel(verseContFrame); + + verseContFrame->setObjectName( + QString("%0_%1").arg(vInfo->surah()).arg(vInfo->number())); + + verselb->setFont(m_versesFont); + verselb->setText(m_dbMgr->getVerseGlyphs(vInfo->surah(), vInfo->number())); + verselb->setAlignment(Qt::AlignCenter); + verselb->setWordWrap(true); + + currLbContent = m_dbMgr->getTranslation(vInfo->surah(), vInfo->number()); + + if (currLbContent == prevLbContent) { + currLbContent = '-'; + } else { + prevLbContent = currLbContent; + } + + contentLb->setText(currLbContent); + contentLb->setTextInteractionFlags(Qt::TextSelectableByMouse); + contentLb->setAlignment(Qt::AlignCenter); + contentLb->setWordWrap(true); + contentLb->setFont(m_sideFont); + + verseContFrame->layout()->addWidget(verselb); + verseContFrame->layout()->addWidget(contentLb); + m_scrlVerseByVerse->widget()->layout()->addWidget(verseContFrame); + m_verseFrameList.insert(0, verseContFrame); + + // connect clicked signal for each label + connect( + verselb, &ClickableLabel::clicked, this, &QuranReader::verseClicked); + } + + if (m_player->playbackState() == QMediaPlayer::PlayingState) { + setHighlightedFrame(); + } +} + +void +QuranReader::highlightCurrentVerse() +{ + if (m_currVerse->number() == 0) + return; + + // idx may be -1 if verse number is 0 (basmallah) + int idx = m_activeVList->indexOf(*m_currVerse); + if (idx < 0) + idx = 0; + + m_activeQuranBrowser->highlightVerse(idx); + + if (m_readerMode == ReaderMode::SinglePage) + setHighlightedFrame(); +} + +void +QuranReader::setHighlightedFrame() +{ + if (m_highlightedFrm != nullptr) + m_highlightedFrm->setSelected(false); + + VerseFrame* verseFrame = m_scrlVerseByVerse->widget()->findChild( + QString("%0_%1").arg(QString::number(m_currVerse->surah()), + QString::number(m_currVerse->number()))); + + verseFrame->setSelected(true); + + m_scrlVerseByVerse->ensureWidgetVisible(verseFrame); + m_highlightedFrm = verseFrame; +} + +void +QuranReader::navigateToVerse(Verse v) +{ + gotoPage(v.page(), false); + + m_currVerse->update(v); + emit currentSurahChanged(); + emit currentVerseChanged(); + highlightCurrentVerse(); +} + +void +QuranReader::updatePageVerseInfoList() +{ + if (m_activeQuranBrowser == m_quranBrowsers[0]) { + m_vLists[0] = + Verse::fromList(m_dbMgr->getVerseInfoList(m_currVerse->page())); + if (m_readerMode == DoublePage) + m_vLists[1] = + Verse::fromList(m_dbMgr->getVerseInfoList(m_currVerse->page() + 1)); + + m_activeVList = &m_vLists[0]; + + } else { + m_vLists[0] = + Verse::fromList(m_dbMgr->getVerseInfoList(m_currVerse->page() - 1)); + m_vLists[1] = + Verse::fromList(m_dbMgr->getVerseInfoList(m_currVerse->page())); + + m_activeVList = &m_vLists[1]; + } +} + +void +QuranReader::btnNextClicked() +{ + if (m_readerMode == ReaderMode::SinglePage || m_currVerse->page() % 2 == 0) + nextPage(1); + else + nextPage(2); +} + +void +QuranReader::btnPrevClicked() +{ + if (m_readerMode == ReaderMode::SinglePage || m_currVerse->page() % 2 != 0) + prevPage(1); + else + prevPage(2); +} + +bool +QuranReader::areNeighbors(int page1, int page2) +{ + return page1 % 2 != 0 && page2 % 2 == 0 && page2 == page1 + 1; +} + +void +QuranReader::switchActivePage() +{ + if (!m_quranBrowsers[1]) + return; + + m_activeQuranBrowser->resetHighlight(); + if (m_activeQuranBrowser == m_quranBrowsers[0]) { + m_activeQuranBrowser = m_quranBrowsers[1]; + m_activeVList = &m_vLists[1]; + } else { + m_activeQuranBrowser = m_quranBrowsers[0]; + m_activeVList = &m_vLists[0]; + } +} + +void +QuranReader::selectVerse(int browserIdx, int IdxInPage) +{ + if (m_activeQuranBrowser != m_quranBrowsers[browserIdx]) + switchActivePage(); + + const Verse& v = m_vLists[browserIdx].at(IdxInPage); + int currSurah = m_currVerse->surah(); + m_currVerse->update(v); + emit currentVerseChanged(); + + if (currSurah != v.surah()) + emit currentSurahChanged(); +} + +void +QuranReader::setVerseToStartOfPage() +{ + // set the current verse to the verse at the top of the page + m_currVerse->update(m_activeVList->at(0)); + m_player->loadActiveVerse(); +} + +void +QuranReader::verseAnchorClicked(const QUrl& hrefUrl) +{ + if (hrefUrl.toString().at(1) == 'F') { + int surah = hrefUrl.toString().remove("#F").toInt(); + qDebug() << "SURAH CARD:" << surah; + emit showBetaqa(surah); + return; + } + + QuranPageBrowser* senderBrowser = qobject_cast(sender()); + int browerIdx = senderBrowser == m_quranBrowsers[1]; + int idx = hrefUrl.toString().remove('#').toInt(); + Verse v(m_vLists[browerIdx].at(idx)); + + QuranPageBrowser::Action chosenAction = + senderBrowser->lmbVerseMenu(m_dbMgr->isBookmarked(v.toList())); + + switch (chosenAction) { + case QuranPageBrowser::play: + selectVerse(browerIdx, idx); + highlightCurrentVerse(); + m_player->playCurrentVerse(); + break; + case QuranPageBrowser::select: + selectVerse(browerIdx, idx); + m_player->loadActiveVerse(); + highlightCurrentVerse(); + break; + case QuranPageBrowser::tafsir: + emit showVerseTafsir(v); + break; + case QuranPageBrowser::copy: + emit copyVerseText(v); + break; + case QuranPageBrowser::addBookmark: + m_dbMgr->addBookmark(v.toList()); + emit notifyBookmarkAdded(); + break; + case QuranPageBrowser::removeBookmark: + if (m_dbMgr->removeBookmark(v.toList())) + emit notifyBookmarkRemoved(); + break; + default: + break; + } +} + +void +QuranReader::verseClicked() +{ + // object = clickable label, parent = verse frame, verse frame name scheme = + // 'surah_verse' + QStringList data = sender()->parent()->objectName().split('_'); + int surah = data.at(0).toInt(); + int verse = data.at(1).toInt(); + + m_currVerse->setNumber(verse); + emit currentVerseChanged(); + + if (m_currVerse->surah() != surah) { + m_currVerse->setSurah(surah); + emit currentSurahChanged(); + } + + m_player->loadActiveVerse(); + highlightCurrentVerse(); + m_player->play(); +} + +void +QuranReader::gotoPage(int page, bool updateElements, bool automaticFlip) +{ + m_activeQuranBrowser->resetHighlight(); + + if (!automaticFlip) + m_player->stop(); + + if (m_activeQuranBrowser->page() != page) { + if (m_readerMode == ReaderMode::SinglePage) + gotoSinglePage(page); + else + gotoDoublePage(page); + } + + if (updateElements) { + setVerseToStartOfPage(); + emit currentVerseChanged(); + emit currentSurahChanged(); + } +} + +void +QuranReader::gotoSinglePage(int page) +{ + m_currVerse->setPage(page); + redrawQuranPage(); + + m_currVerse->update(m_activeVList->at(0)); + addSideContent(); +} + +void +QuranReader::gotoDoublePage(int page) +{ + int currPage = m_currVerse->page(); + m_currVerse->setPage(page); + + int pageBrowerIdx = page % 2 == 0; + + if (areNeighbors(currPage, page) || areNeighbors(page, currPage)) + switchActivePage(); + else { + m_activeQuranBrowser = m_quranBrowsers[pageBrowerIdx]; + redrawQuranPage(); + } +} + +void +QuranReader::nextPage(int step) +{ + bool keepPlaying = m_player->playbackState() == QMediaPlayer::PlayingState; + if (m_currVerse->page() + step <= 604) { + gotoPage(m_currVerse->page() + step, true, true); + + if (m_readerMode == ReaderMode::SinglePage) + m_scrlVerseByVerse->verticalScrollBar()->setValue(0); + + // if the page is flipped automatically, resume playback + if (keepPlaying) { + m_player->play(); + highlightCurrentVerse(); + } + } +} + +void +QuranReader::prevPage(int step) +{ + bool keepPlaying = m_player->playbackState() == QMediaPlayer::PlayingState; + if (m_currVerse->page() - step >= 1) { + gotoPage(m_currVerse->page() - step, true, true); + + if (m_readerMode == ReaderMode::SinglePage) + m_scrlVerseByVerse->verticalScrollBar()->setValue(0); + + if (keepPlaying) { + m_player->play(); + highlightCurrentVerse(); + } + } +} + +void +QuranReader::gotoSurah(int surahIdx) +{ + // getting surah index + int page = m_dbMgr->getSurahStartPage(surahIdx); + gotoPage(page, false); + + m_currVerse->setPage(page); + m_currVerse->setSurah(surahIdx); + m_currVerse->setNumber((surahIdx == 9 || surahIdx == 1) ? 1 : 0); + + // syncing the player & playing basmalah + m_player->playCurrentVerse(); + + highlightCurrentVerse(); + emit currentVerseChanged(); + emit currentSurahChanged(); +} + +QuranReader::~QuranReader() +{ + delete ui; +} diff --git a/src/core/quranreader.h b/src/core/quranreader.h new file mode 100644 index 00000000..534b539f --- /dev/null +++ b/src/core/quranreader.h @@ -0,0 +1,248 @@ +#ifndef QURANREADER_H +#define QURANREADER_H + +#include "../globals.h" +#include "../utils/shortcuthandler.h" +#include "../utils/verse.h" +#include "../utils/verseplayer.h" +#include "../widgets/quranpagebrowser.h" +#include "../widgets/verseframe.h" +#include +#include +#include + +namespace Ui { +class QuranReader; +} + +class QuranReader : public QWidget +{ + Q_OBJECT + +public: + explicit QuranReader(QWidget* parent, + VersePlayer* player, + ShortcutHandler* handler); + ~QuranReader(); + +public slots: + void updatePageFontSize(); + /** + * @brief highlight the currently active ::Verse m_currVerse in the + * active QuranPageBrowser and the side panel depending on the ::ReaderMode + */ + void highlightCurrentVerse(); + /** + * @brief highlight the currently active VerseFrame + */ + void setHighlightedFrame(); + /** + * @brief navigate to the given Verse and update UI elements accordingly + * @param v - Verse to navigate to + */ + void navigateToVerse(Verse v); + /** + * @brief toggle the main reader view by hiding one of the panels + */ + void toggleReaderView(); + /** + * @brief set tafsir to the one in the settings, update the selected db + */ + void updateLoadedTafsir(); + /** + * @brief set translation to the one in the settings, update the selected db + */ + void updateLoadedTranslation(); + /** + * @brief redraw the current Quran page + * @param manualSz - boolean flag to force the use of the manually set + * fontsize + */ + void redrawQuranPage(bool manualSz = false); + /** + * @brief updates the side panel with the translation of the current page + * verses + */ + void addSideContent(); + /** + * @brief set side content font to the one in the settings + */ + void updateSideFont(); + /** + * @brief Updates the type of the verses shown and reload the font family and + * size + */ + void updateVerseType(); + /** + * @brief update the highlight layer (fg/bg) used by the Quran browser(s) + */ + void updateHighlight(); + /** + * @brief gets the page of the 1st verse in this surah, moves to that page, + * and starts playback of the surah + * @param surahIdx - surah number (1-114) + */ + void gotoSurah(int surahIdx); + /** + * @brief ensure the page given is visible and update other members to match + * the properties of the first verse in the page, calls the appropriate + * navigation function according to the ::ReaderMode + * @param page - page to navigate to + * @param updateElements - boolean flag to indicate whether to update other + * elements or not + * @param automaticFlip - boolean indicating whether the function was called + * by internal signal to automatically flip the page + */ + void gotoPage(int page, + bool updateElements = true, + bool automaticFlip = false); + /** + * @brief sets m_currVerse to the first verse in m_vInfoList + */ + void setVerseToStartOfPage(); + +signals: + void showBetaqa(int surah); + void showVerseTafsir(const Verse& v); + void copyVerseText(const Verse& v); + void currentVerseChanged(); + void currentSurahChanged(); + void notifyBookmarkAdded(); + void notifyBookmarkRemoved(); + +private slots: + /** + * @brief callback function for clicking verses in the QuranPageBrowser that + * takes actions based on the chosen option in the menu + * @param hrefUrl - "#idx" where idx is the verse index relative to the start + * of the page (=index in the page ::Verse QList) + */ + void verseAnchorClicked(const QUrl& hrefUrl); + /** + * @brief slot to navigate to the clicked verse in the side panel and update + * UI elements + */ + void verseClicked(); + /** + * @brief navigates to the next page relative to the current page + */ + void nextPage(int step = 1); + /** + * @brief navigates to the previous page relative to the current page + */ + void prevPage(int step = 1); + /** + * @brief single page mode navigation + * @param page - page to navigate to + */ + void gotoSinglePage(int page); + /** + * @brief double page mode navigation + * @param page - page to navigate to + */ + void gotoDoublePage(int page); + +private: + Verse* m_currVerse = Verse::current(); + QSettings* const m_settings = Globals::settings; + const QList& m_recitersList = Globals::recitersList; + const QList& m_tafasirList = Globals::tafasirList; + const QString& m_updateToolPath = Globals::updateToolPath; + const ReaderMode& m_readerMode = Globals::readerMode; + DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + fa::QtAwesome* m_fa = Globals::awesome; + Ui::QuranReader* ui; + void setupConnections(); + void setupShortcuts(ShortcutHandler* handler); + /** + * @brief load icons for different UI elements + */ + void loadIcons(); + /** + * @brief setup the reader layout and create widgets according to the current + * ::ReaderMode + */ + void loadReader(); + /** + * @brief utility function to check whether 2 pages are beside each other in + * 2-page mode + * @details 2 pages are considered neighbors if the right page is odd and the + * left page is directly after the right page + * @param page1 - the right side page + * @param page2 - the left side page + * @return boolean value indicating whether the given pages are neighbors + */ + bool areNeighbors(int page1, int page2); + /** + * @brief 2-page mode utility to switch the active page & verse info list to + * the opposite side of the current + * @details the currently active page is recogonized through + * m_activeQuranBrowser and m_activeVList pointers + */ + void switchActivePage(); + /** + * @brief flip the current page/2-pages to the next page/2-pages + */ + void btnNextClicked(); + /** + * @brief flip the current page/2-pages to the previous page/2-pages + */ + void btnPrevClicked(); + /** + * @brief selects one of the verses in the currently displayed page(s) + * @param browserIdx - index of the QuranPageBrowser which contains the target + * verse + * @param IdxInPage - index of the verse relative to the start of the + * page + */ + void selectVerse(int browserIdx, int IdxInPage); + /** + * @brief updates the list that contains::Verse instances for verses in the + * current page + */ + void updatePageVerseInfoList(); + /** + * @brief QScrollArea used in single page mode to display verses & + * translation + */ + QScrollArea* m_scrlVerseByVerse = nullptr; + /** + * @brief pointer to currently active QuranPageBrowser instance, must be one + * of the values in m_quranBrowsers array + */ + QuranPageBrowser* m_activeQuranBrowser = nullptr; + /** + * @brief array of QuranPageBrowser instances used in different modes, index 0 + * is used in both modes + */ + QuranPageBrowser* m_quranBrowsers[2]{}; + /** + * @brief QList of QFrame pointers to VerseFrame elements in the single page + * mode side panel + */ + QList m_verseFrameList; + /** + * @brief pointer to the currently active page ::Verse list + */ + const QList* m_activeVList; + /** + * @brief array of 2 QLists of ::Verse instances for the verses in the + * displayed page(s), index 0 is used in both reader modes + */ + QList m_vLists[2]; + /** + * @brief pointer to the currently highlighted VerseFrame in the side panel + */ + VerseFrame* m_highlightedFrm = nullptr; + VersePlayer* m_player = nullptr; + /** + * @brief QFont used in the side panel translation + */ + QFont m_sideFont; + /** + * @brief QFont used in displaying Quranic verse + */ + QFont m_versesFont; +}; + +#endif // QURANREADER_H diff --git a/src/core/quranreader.ui b/src/core/quranreader.ui new file mode 100644 index 00000000..abdea7c7 --- /dev/null +++ b/src/core/quranreader.ui @@ -0,0 +1,210 @@ + + + QuranReader + + + + 0 + 0 + 942 + 700 + + + + Form + + + + 6 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 60 + 30 + + + + + Noto Sans Display + 12 + true + + + + PointingHandCursor + + + next + + + QPushButton { min-width: 60px; text-align: center; } + + + + + + + + + false + + + + + + + + 2 + + + 4 + + + 0 + + + 4 + + + 0 + + + + + QFrame#frmSidePanel { background: transparent; } + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + 0 + + + + + 550 + 0 + + + + *.a { background-color:palette(base); } + + + QFrame::NoFrame + + + QFrame::Raised + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + 0 + 0 + + + + + 62 + 0 + + + + + 60 + 30 + + + + + Noto Sans Display + 12 + + + + PointingHandCursor + + + previous + + + QPushButton { min-width: 60px; text-align: center; } + + + + + + + + + + + + + + diff --git a/src/core/searchdialog.cpp b/src/core/searchdialog.cpp index b74441ab..0251cf27 100644 --- a/src/core/searchdialog.cpp +++ b/src/core/searchdialog.cpp @@ -70,11 +70,13 @@ SearchDialog::getResults() ui->spnEndPage->setValue(range[0]); range[1] = ui->spnEndPage->value(); - m_currResults = - m_dbMgr->searchVerses(m_searchText, range, ui->chkWholeWord->isChecked()); + m_currResults = Verse::fromList(m_dbMgr->searchVerses( + m_searchText, range, ui->chkWholeWord->isChecked())); } else { - m_currResults = m_dbMgr->searchSurahs( - m_searchText, m_selectedSurahMap.values(), ui->chkWholeWord->isChecked()); + m_currResults = + Verse::fromList(m_dbMgr->searchSurahs(m_searchText, + m_selectedSurahMap.values(), + ui->chkWholeWord->isChecked())); } ui->lbResultCount->setText(QString::number(m_currResults.size()) + tr(" Search results")); @@ -86,7 +88,7 @@ void SearchDialog::verseClicked() { QStringList data = sender()->objectName().split('-'); - Verse selected{ data.at(0).toInt(), data.at(1).toInt(), data.at(2).toInt() }; + Verse selected(data.at(0).toInt(), data.at(1).toInt(), data.at(2).toInt()); emit navigateToVerse(selected); } @@ -107,25 +109,25 @@ SearchDialog::showResults() for (int i = m_startResult; i < endIdx; i++) { Verse v = m_currResults.at(i); - QString fontName = Globals::verseFontname(m_dbMgr->getVerseType(), v.page); + QString fontName = Globals::verseFontname(m_dbMgr->getVerseType(), v.page()); VerseFrame* vFrame = new VerseFrame(ui->srclResults); QLabel* lbInfo = new QLabel(vFrame); ClickableLabel* clkLb = new ClickableLabel(vFrame); - QString info = tr("Surah: ") + m_surahNames.at(v.surah - 1) + " - " + - tr("Verse: ") + QString::number(v.number); + QString info = tr("Surah: ") + m_surahNames.at(v.surah() - 1) + " - " + + tr("Verse: ") + QString::number(v.number()); lbInfo->setText(info); lbInfo->setMaximumHeight(50); lbInfo->setAlignment(Qt::AlignLeft); lbInfo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); clkLb->setMargin(5); - clkLb->setObjectName(QString::number(v.page) + '-' + - QString::number(v.surah) + '-' + - QString::number(v.number)); + clkLb->setObjectName(QString::number(v.page()) + '-' + + QString::number(v.surah()) + '-' + + QString::number(v.number())); clkLb->setFont(QFont(fontName, 15)); - clkLb->setText(m_dbMgr->getVerseGlyphs(v.surah, v.number)); + clkLb->setText(m_dbMgr->getVerseGlyphs(v.surah(), v.number())); clkLb->setAlignment(Qt::AlignLeft); clkLb->setWordWrap(true); diff --git a/src/core/searchdialog.h b/src/core/searchdialog.h index d03aaafe..d94107a5 100644 --- a/src/core/searchdialog.h +++ b/src/core/searchdialog.h @@ -8,6 +8,7 @@ #include "../globals.h" #include "../utils/dbmanager.h" +#include "../utils/verse.h" #include "../widgets/verseframe.h" #include #include diff --git a/src/core/tafsirdialog.cpp b/src/core/tafsirdialog.cpp index c8cb5c1c..801a0638 100644 --- a/src/core/tafsirdialog.cpp +++ b/src/core/tafsirdialog.cpp @@ -16,6 +16,9 @@ TafsirDialog::TafsirDialog(QWidget* parent) ui->btnPrev->setIcon( Globals::awesome->icon(fa::fa_solid, fa::fa_arrow_right)); + foreach (const Tafsir& t, Globals::tafasirList) { + ui->cmbTafsir->addItem(t.displayName); + } setTafsirAsTitle(); setLayoutDirection(Qt::LeftToRight); if (m_qcfVer == 1) @@ -30,32 +33,20 @@ TafsirDialog::TafsirDialog(QWidget* parent) void TafsirDialog::btnNextClicked() { - if (m_shownVerse.number == m_dbMgr->getSurahVerseCount(m_shownVerse.surah) && - m_shownVerse.surah < 114) { - m_shownVerse.number = 1; - m_shownVerse.surah++; - } else { - m_shownVerse.number++; + if (m_shownVerse.number() == m_shownVerse.surahCount() && + m_shownVerse.surah() < 114) { + m_shownVerse = m_shownVerse.next(); + loadVerseTafsir(); } - - m_shownVerse.page = - m_dbMgr->getVersePage(m_shownVerse.surah, m_shownVerse.number); - loadVerseTafsir(); } void TafsirDialog::btnPrevClicked() { - if (m_shownVerse.number == 1 && m_shownVerse.surah > 1) { - m_shownVerse.surah--; - m_shownVerse.number = m_dbMgr->getSurahVerseCount(m_shownVerse.surah); - } else { - m_shownVerse.number--; + if (m_shownVerse.number() == 1 && m_shownVerse.surah() > 1) { + m_shownVerse = m_shownVerse.prev(); + loadVerseTafsir(); } - - m_shownVerse.page = - m_dbMgr->getVersePage(m_shownVerse.surah, m_shownVerse.number); - loadVerseTafsir(); } void @@ -77,12 +68,12 @@ TafsirDialog::setTafsirAsTitle() void TafsirDialog::loadVerseTafsir() { - QString title = tr("Surah: ") + m_dbMgr->getSurahName(m_shownVerse.surah) + - " - " + tr("Verse: ") + QString::number(m_shownVerse.number); + QString title = tr("Surah: ") + m_dbMgr->getSurahName(m_shownVerse.surah()) + + " - " + tr("Verse: ") + QString::number(m_shownVerse.number()); QString glyphs = - m_dbMgr->getVerseGlyphs(m_shownVerse.surah, m_shownVerse.number); + m_dbMgr->getVerseGlyphs(m_shownVerse.surah(), m_shownVerse.number()); QString fontFamily = - Globals::verseFontname(m_dbMgr->getVerseType(), m_shownVerse.page); + Globals::verseFontname(m_dbMgr->getVerseType(), m_shownVerse.page()); ui->lbVerseInfo->setText(title); ui->lbVerseText->setWordWrap(true); @@ -95,14 +86,14 @@ TafsirDialog::loadVerseTafsir() if (m_dbMgr->currTafsir()->text) ui->tedTafsir->setText( - m_dbMgr->getTafsir(m_shownVerse.surah, m_shownVerse.number)); + m_dbMgr->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); else ui->tedTafsir->setHtml( - m_dbMgr->getTafsir(m_shownVerse.surah, m_shownVerse.number)); + m_dbMgr->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); - if (m_shownVerse.surah == 1 && m_shownVerse.number == 1) + if (m_shownVerse.surah() == 1 && m_shownVerse.number() == 1) ui->btnPrev->setDisabled(true); - else if (m_shownVerse.surah == 114 && m_shownVerse.number == 6) + else if (m_shownVerse.surah() == 114 && m_shownVerse.number() == 6) ui->btnNext->setDisabled(true); else { ui->btnPrev->setDisabled(false); diff --git a/src/core/tafsirdialog.h b/src/core/tafsirdialog.h index 0932dd8e..f077d232 100644 --- a/src/core/tafsirdialog.h +++ b/src/core/tafsirdialog.h @@ -8,6 +8,7 @@ #include "../globals.h" #include "../utils/dbmanager.h" +#include "../utils/verse.h" #include #include #include diff --git a/src/core/tafsirdialog.ui b/src/core/tafsirdialog.ui index ee91c579..24388c16 100644 --- a/src/core/tafsirdialog.ui +++ b/src/core/tafsirdialog.ui @@ -15,11 +15,22 @@ - - - s-v - - + + + + + s-v + + + + + + + + + + + diff --git a/src/globals.h b/src/globals.h index 3b024d00..a7e92945 100644 --- a/src/globals.h +++ b/src/globals.h @@ -14,41 +14,6 @@ #include #include -/** - * @struct Verse - * @brief Verse struct represents a single quran verse - * @details Quran verses consist of 3 attributes. page (1-604). surah (1-114). - * number represents the number of the verse in the surah (0-surah verse count). - * Basmallah before the 1st verse is represented as verse number 0. - */ -struct Verse -{ - int page{ -1 }; ///< verse page - int surah{ -1 }; ///< verse surah number - int number{ -1 }; ///< verse number in surah - bool operator==(const Verse& v2) const - { - return (this->number == v2.number && this->surah == v2.surah); - } - bool operator!=(const Verse& v2) const - { - return (this->number != v2.number || this->surah != v2.surah); - } - bool operator<(const Verse& v2) const - { - if (this->surah == v2.surah) - return this->number < v2.number; - - return this->surah < v2.surah; - } - bool operator>(const Verse& v2) const - { - if (this->surah == v2.surah) - return this->number > v2.number; - - return this->surah > v2.surah; - } -}; /** * @struct Reciter * @brief Reciter struct represents a quran reciter diff --git a/src/utils/dbmanager.cpp b/src/utils/dbmanager.cpp index af9d0856..9193d23c 100644 --- a/src/utils/dbmanager.cpp +++ b/src/utils/dbmanager.cpp @@ -4,7 +4,6 @@ */ #include "dbmanager.h" -#include DBManager::DBManager(QObject* parent) : QObject(parent) @@ -134,10 +133,10 @@ DBManager::getPageLines(const int page) return lines; } -QList +QList> DBManager::getVerseInfoList(int page) { - QList viList; + QList> viList; setOpenDatabase(Database::quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); @@ -150,7 +149,7 @@ DBManager::getVerseInfoList(int page) } while (dbQuery.next()) { - Verse v{ page, dbQuery.value(0).toInt(), dbQuery.value(1).toInt() }; + QList v{ page, dbQuery.value(0).toInt(), dbQuery.value(1).toInt() }; viList.append(v); } @@ -313,7 +312,7 @@ DBManager::getVerseId(const int sIdx, const int vIdx) return dbQuery.value(0).toInt(); } -Verse +QList DBManager::getVerseById(const int id) { setOpenDatabase(Database::quran, m_quranDbPath.filePath()); @@ -326,9 +325,9 @@ DBManager::getVerseById(const int id) dbQuery.next(); - return Verse{ dbQuery.value(0).toInt(), - dbQuery.value(1).toInt(), - dbQuery.value(2).toInt() }; + return { dbQuery.value(0).toInt(), + dbQuery.value(1).toInt(), + dbQuery.value(2).toInt() }; } int @@ -374,12 +373,12 @@ DBManager::surahNameList() return m_surahNames; } -QList +QList> DBManager::searchSurahs(QString searchText, const QList surahs, const bool whole) { - QList results; + QList> results; setOpenDatabase(Database::quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); @@ -404,9 +403,9 @@ DBManager::searchSurahs(QString searchText, } while (dbQuery.next()) { - results.append(Verse{ dbQuery.value(0).toInt(), - dbQuery.value(1).toInt(), - dbQuery.value(2).toInt() }); + results.append({ dbQuery.value(0).toInt(), + dbQuery.value(1).toInt(), + dbQuery.value(2).toInt() }); } return results; @@ -440,7 +439,7 @@ DBManager::searchSurahNames(QString text) /* ---------------- Verse-related methods ---------------- */ bool -DBManager::getKhatmahPos(const int khatmahId, Verse& v) +DBManager::getKhatmahPos(const int khatmahId, QList& vInfo) { setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); @@ -454,14 +453,14 @@ DBManager::getKhatmahPos(const int khatmahId, Verse& v) if (!dbQuery.next()) return false; - v.page = dbQuery.value(0).toInt(); - v.surah = dbQuery.value(1).toInt(); - v.number = dbQuery.value(2).toInt(); + vInfo[0] = dbQuery.value(0).toInt(); + vInfo[1] = dbQuery.value(1).toInt(); + vInfo[2] = dbQuery.value(2).toInt(); return true; } int -DBManager::addKhatmah(const Verse& v, const QString name, const int id) +DBManager::addKhatmah(QList vInfo, const QString name, const int id) { setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); @@ -473,18 +472,18 @@ DBManager::addKhatmah(const Verse& v, const QString name, const int id) q = "INSERT INTO khatmah(name, page, surah, number) VALUES ('%0', %1, %2, " "%3)"; dbQuery.prepare(q.arg(name, - QString::number(v.page), - QString::number(v.surah), - QString::number(v.number))); + QString::number(vInfo[0]), + QString::number(vInfo[1]), + QString::number(vInfo[2]))); } else { q = "REPLACE INTO khatmah VALUES " "(%0, " "'%1', %2, %3, %4)"; dbQuery.prepare(q.arg(QString::number(id), name, - QString::number(v.page), - QString::number(v.surah), - QString::number(v.number))); + QString::number(vInfo[0]), + QString::number(vInfo[1]), + QString::number(vInfo[2]))); } if (!dbQuery.exec()) { @@ -536,15 +535,15 @@ DBManager::removeKhatmah(const int id) } bool -DBManager::saveActiveKhatmah(const Verse& v) +DBManager::saveActiveKhatmah(QList vInfo) { setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); QString q = QString::asprintf( "UPDATE khatmah SET page=%i, surah=%i, number=%i WHERE id=%i", - v.page, - v.surah, - v.number, + vInfo[0], + vInfo[1], + vInfo[2], m_activeKhatmah); if (!dbQuery.exec(q)) { qCritical() << "Couldn't save position in mushaf"; @@ -606,7 +605,7 @@ DBManager::getKhatmahName(const int id) return dbQuery.value(0).toString(); } -Verse +QList DBManager::randomVerse() { setOpenDatabase(Database::quran, m_quranDbPath.filePath()); @@ -645,12 +644,12 @@ DBManager::getVersePage(const int& surahIdx, const int& verse) return dbQuery.value(0).toInt(); } -QList +QList> DBManager::searchVerses(QString searchText, const int range[2], const bool whole) { - QList results; + QList> results; setOpenDatabase(Database::quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); @@ -672,19 +671,19 @@ DBManager::searchVerses(QString searchText, } while (dbQuery.next()) { - Verse entry{ dbQuery.value(0).toInt(), - dbQuery.value(1).toInt(), - dbQuery.value(2).toInt() }; + QList entry{ dbQuery.value(0).toInt(), + dbQuery.value(1).toInt(), + dbQuery.value(2).toInt() }; results.append(entry); } return results; } -QList +QList> DBManager::bookmarkedVerses(int surahIdx) { - QList results; + QList> results; setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); QString q = "SELECT page,surah,number FROM favorites"; @@ -696,25 +695,25 @@ DBManager::bookmarkedVerses(int surahIdx) qCritical() << "Couldn't execute bookmarkedVerses SELECT query"; while (dbQuery.next()) { - results.append(Verse{ dbQuery.value(0).toInt(), - dbQuery.value(1).toInt(), - dbQuery.value(2).toInt() }); + results.append({ dbQuery.value(0).toInt(), + dbQuery.value(1).toInt(), + dbQuery.value(2).toInt() }); } return results; } bool -DBManager::isBookmarked(Verse v) +DBManager::isBookmarked(QList vInfo) { setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); dbQuery.prepare( "SELECT page FROM favorites WHERE page=:p AND surah=:s AND number=:n"); - dbQuery.bindValue(0, v.page); - dbQuery.bindValue(1, v.surah); - dbQuery.bindValue(2, v.number); + dbQuery.bindValue(0, vInfo[0]); + dbQuery.bindValue(1, vInfo[1]); + dbQuery.bindValue(2, vInfo[2]); if (!dbQuery.exec()) { qWarning() << "Couldn't check if verse is bookmarked"; @@ -727,7 +726,7 @@ DBManager::isBookmarked(Verse v) } bool -DBManager::addBookmark(Verse v) +DBManager::addBookmark(QList vInfo) { setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); @@ -737,9 +736,9 @@ DBManager::addBookmark(Verse v) dbQuery.prepare( "INSERT INTO favorites(page, surah, number) VALUES (:p, :s, :n)"); - dbQuery.bindValue(0, v.page); - dbQuery.bindValue(1, v.surah); - dbQuery.bindValue(2, v.number); + dbQuery.bindValue(0, vInfo[0]); + dbQuery.bindValue(1, vInfo[1]); + dbQuery.bindValue(2, vInfo[2]); if (!dbQuery.exec()) { qWarning() << "Couldn't add verse to bookmarks db"; @@ -753,15 +752,15 @@ DBManager::addBookmark(Verse v) } bool -DBManager::removeBookmark(Verse v) +DBManager::removeBookmark(QList vInfo) { setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); dbQuery.prepare( "DELETE FROM favorites WHERE page=:p AND surah=:s AND number=:n"); - dbQuery.bindValue(0, v.page); - dbQuery.bindValue(1, v.surah); - dbQuery.bindValue(2, v.number); + dbQuery.bindValue(0, vInfo[0]); + dbQuery.bindValue(1, vInfo[1]); + dbQuery.bindValue(2, vInfo[2]); if (!dbQuery.exec()) { qWarning() << "Couldn't remove verse from bookmarks"; diff --git a/src/utils/dbmanager.h b/src/utils/dbmanager.h index e17b9c39..abcc8e01 100644 --- a/src/utils/dbmanager.h +++ b/src/utils/dbmanager.h @@ -86,7 +86,7 @@ class DBManager : public QObject * @param page - Quran page number * @return QList of ::Verse instances */ - QList getVerseInfoList(const int page); + QList> getVerseInfoList(const int page); /** * @brief gets the surah name glyph for the QCF_BSML font, used to render * surah frame in Quran page @@ -119,7 +119,7 @@ class DBManager : public QObject * active khatmah * @param v - ::Verse reached in khatmah */ - bool saveActiveKhatmah(const Verse& v); + bool saveActiveKhatmah(QList vInfo); /** * @brief get all available khatmah ids * @return QList of khatmah id(s) @@ -136,7 +136,7 @@ class DBManager : public QObject * @return boolean indicating a successful operation (false in case of error * and in case id does not exist) */ - bool getKhatmahPos(const int khatmahId, Verse& v); + bool getKhatmahPos(const int khatmahId, QList& vInfo); /** * @brief add a new khatmah/replace khatmah with given id with position of * ::Verse v @@ -145,7 +145,7 @@ class DBManager : public QObject * @param id - id of khatmah to replace, -1 means do not replace (default: -1) * @return id of newly added khatmah or id parameter if defined */ - int addKhatmah(const Verse& v, const QString name, const int id = -1); + int addKhatmah(QList vInfo, const QString name, const int id = -1); /** * @brief rename the khatmah with the given id to newName * @param khatmahId - id of khatmah to rename @@ -197,7 +197,7 @@ class DBManager : public QObject * @param id - verse id * @return ::Verse instance */ - Verse getVerseById(const int id); + QList getVerseById(const int id); /** * @brief gets the page where the verse is found * @param surahIdx - sura number @@ -231,9 +231,9 @@ class DBManager : public QObject * @param whole - boolean value to search for whole words only * @return QList of ::Verse instances representing the search results */ - QList searchSurahs(QString searchText, - const QList surahs, - const bool whole = false); + QList> searchSurahs(QString searchText, + const QList surahs, + const bool whole = false); /** * @brief search a range of pages for the given search text * @param searchText - text to search for @@ -241,9 +241,9 @@ class DBManager : public QObject * @param whole - boolean value to indicate search for whole words only * @return QList of ::Verse instances representing the search results */ - QList searchVerses(QString searchText, - const int range[2] = new int[2]{ 1, 604 }, - const bool whole = false); + QList> searchVerses(QString searchText, + const int range[2] = new int[2]{ 1, 604 }, + const bool whole = false); /** * @brief gets the tafsir content for the given verse using the active * DBManager::Tafsir @@ -264,32 +264,32 @@ class DBManager : public QObject * @brief gets a random verse from the Quran * @return QPair of ::Verse instance and verse text */ - Verse randomVerse(); + QList randomVerse(); /** * @brief gets a QList of ::Verse instances representing the bookmarked verse * within the given sura (default gets all) * @param surahIdx - sura number (-1 returns all bookmarks) * @return QList of bookmarked verses */ - QList bookmarkedVerses(int surahIdx = -1); + QList> bookmarkedVerses(int surahIdx = -1); /** * @brief checks whether the given ::Verse is bookmarked - * @param v - ::Verse instance to check + * @param vInfo - ::Verse instance to check * @return boolean */ - bool isBookmarked(Verse v); + bool isBookmarked(QList vInfo); /** * @brief add the given ::Verse to bookmarks - * @param v - ::Verse instance to add + * @param vInfo - ::Verse instance to add * @return boolean */ - bool addBookmark(Verse v); + bool addBookmark(QList vInfo); /** * @brief remove the given ::Verse from bookmarks - * @param v - ::Verse instance to remove + * @param vInfo - ::Verse instance to remove * @return boolean indicating successful removal */ - bool removeBookmark(Verse v); + bool removeBookmark(QList vInfo); /** * @brief getter for m_currTafsir * @return the currently set DBManager::Tafasir diff --git a/src/utils/notificationmanager.cpp b/src/utils/systemtray.cpp similarity index 56% rename from src/utils/notificationmanager.cpp rename to src/utils/systemtray.cpp index 6b4c0056..9d3d81b5 100644 --- a/src/utils/notificationmanager.cpp +++ b/src/utils/systemtray.cpp @@ -1,35 +1,36 @@ /** * @file notificationmanager.cpp - * @brief Implementation file for NotificationManager + * @brief Implementation file for SystemTray */ -#include "notificationmanager.h" +#include "systemtray.h" -NotificationManager::NotificationManager(QObject* parent) +SystemTray::SystemTray(QObject* parent) : QObject{ parent } , m_sysTray{ new QSystemTrayIcon(this) } , m_trayMenu{ new QMenu() } { addActions(); + setTooltip(qApp->translate("MainWindow", "Quran Companion")); m_sysTray->setContextMenu(m_trayMenu); m_sysTray->setIcon(QIcon(":/resources/tray.png")); m_sysTray->show(); } void -NotificationManager::notify(QString title, QString msg) +SystemTray::notify(QString title, QString msg) { m_sysTray->showMessage(title, msg); } void -NotificationManager::setTooltip(QString text) +SystemTray::setTooltip(QString text) { m_sysTray->setToolTip(text); } void -NotificationManager::addActions() +SystemTray::addActions() { QAction* togglePlay = new QAction(tr("Play/Pause recitation"), m_trayMenu); QAction* show = new QAction(tr("Show window"), m_trayMenu); @@ -50,20 +51,16 @@ NotificationManager::addActions() m_trayMenu->addSeparator(); m_trayMenu->addAction(exit); - connect(togglePlay, - &QAction::triggered, - this, - &NotificationManager::togglePlayback); - connect(show, &QAction::triggered, this, &NotificationManager::showWindow); - connect(hide, &QAction::triggered, this, &NotificationManager::hideWindow); - connect(prefs, &QAction::triggered, this, &NotificationManager::openPrefs); - connect(exit, &QAction::triggered, this, &NotificationManager::exit); - connect(about, &QAction::triggered, this, &NotificationManager::openAbout); - connect( - update, &QAction::triggered, this, &NotificationManager::checkForUpdates); + connect(togglePlay, &QAction::triggered, this, &SystemTray::togglePlayback); + connect(show, &QAction::triggered, this, &SystemTray::showWindow); + connect(hide, &QAction::triggered, this, &SystemTray::hideWindow); + connect(prefs, &QAction::triggered, this, &SystemTray::openPrefs); + connect(exit, &QAction::triggered, this, &SystemTray::exit); + connect(about, &QAction::triggered, this, &SystemTray::openAbout); + connect(update, &QAction::triggered, this, &SystemTray::checkForUpdates); } -NotificationManager::~NotificationManager() +SystemTray::~SystemTray() { delete m_trayMenu; } diff --git a/src/utils/notificationmanager.h b/src/utils/systemtray.h similarity index 86% rename from src/utils/notificationmanager.h rename to src/utils/systemtray.h index b80a6d47..3d55b4c9 100644 --- a/src/utils/notificationmanager.h +++ b/src/utils/systemtray.h @@ -3,8 +3,8 @@ * @brief Header file for NotificationManager */ -#ifndef NOTIFICATIONMANAGER_H -#define NOTIFICATIONMANAGER_H +#ifndef SYSTEMTRAY_H +#define SYSTEMTRAY_H #include "../globals.h" #include "dbmanager.h" @@ -18,10 +18,10 @@ #include /** - * @brief NotificationManager class is responsible for system tray functionality + * @brief SystemTray class is responsible for system tray functionality * and verse of the day notification */ -class NotificationManager : public QObject +class SystemTray : public QObject { Q_OBJECT @@ -30,8 +30,8 @@ class NotificationManager : public QObject * @brief Class constructor * @param parent - pointer to parent widget */ - explicit NotificationManager(QObject* parent = nullptr); - ~NotificationManager(); + explicit SystemTray(QObject* parent = nullptr); + ~SystemTray(); /** * @brief send a desktop notification message @@ -99,4 +99,4 @@ class NotificationManager : public QObject QSystemTrayIcon* m_sysTray; }; -#endif // NOTIFICATIONMANAGER_H +#endif // SYSTEMTRAY_H diff --git a/src/utils/verse.cpp b/src/utils/verse.cpp new file mode 100644 index 00000000..971903a5 --- /dev/null +++ b/src/utils/verse.cpp @@ -0,0 +1,172 @@ +#include "verse.h" + +Verse* +Verse::current() +{ + static Verse m_current = Verse(); + return &m_current; +} + +QList +Verse::fromList(QList> lst) +{ + QList final(lst.size()); + for (int i = 0; i < lst.size(); i++) + final[i].update(lst[i]); + + return final; +} + +Verse::Verse() {} + +Verse::Verse(int page, int surah, int number) + : m_page(page) + , m_number(number) +{ + setSurah(surah); +} + +Verse::Verse(const QList vInfo) +{ + m_page = vInfo[0]; + setSurah(vInfo[1]); + m_number = vInfo[2]; +} + +Verse& +Verse::operator=(const QList& vInfo) +{ + m_page = vInfo[0]; + setSurah(vInfo[1]); + m_number = vInfo[2]; + return *this; +} + +Verse& +Verse::operator=(const Verse& cp) +{ + m_page = cp.m_page; + m_surah = cp.m_surah; + m_surahCount = cp.m_surahCount; + m_number = cp.m_number; + return *this; +} + +bool +Verse::operator==(const Verse& v2) const +{ + return (m_number == v2.m_number && m_surah == v2.m_surah); +} + +bool +Verse::operator!=(const Verse& v2) const +{ + return (m_number != v2.m_number || m_surah != v2.m_surah); +} + +bool +Verse::operator<(const Verse& v2) const +{ + if (m_surah == v2.surah()) + return m_number < v2.m_number; + + return m_surah < v2.m_surah; +} + +bool +Verse::operator>(const Verse& v2) const +{ + if (m_surah == v2.m_surah) + return m_number > v2.m_number; + + return m_surah > v2.m_surah; +} + +void +Verse::update(const Verse& v) +{ + m_page = v.m_page; + setSurah(v.m_surah); + m_number = v.m_number; +} + +void +Verse::update(const QList& vInfo) +{ + m_page = vInfo[0]; + setSurah(vInfo[1]); + m_number = vInfo[2]; +} + +Verse +Verse::next() const +{ + QList vInfo = + m_dbMgr->getVerseById(m_dbMgr->getVerseId(m_surah, m_number) + 1); + return Verse(vInfo); +} + +Verse +Verse::prev() const +{ + QList vInfo = + m_dbMgr->getVerseById(m_dbMgr->getVerseId(m_surah, m_number) - 1); + return Verse(vInfo); +} + +void +Verse::updateSurahCount() +{ + m_surahCount = m_dbMgr->getSurahVerseCount(m_surah); +} + +void +Verse::setPage(int newPage) +{ + m_page = newPage; +} + +void +Verse::setSurah(int newSurah) +{ + if (m_surah == newSurah) + return; + m_surah = newSurah; + updateSurahCount(); +} + +void +Verse::setNumber(int newNumber) +{ + m_number = newNumber; +} + +QList +Verse::toList() const +{ + return { m_page, m_surah, m_number }; +} + +int +Verse::surahCount() const +{ + return m_surahCount; +} + +int +Verse::page() const +{ + return m_page; +} + +int +Verse::surah() const +{ + return m_surah; +} + +int +Verse::number() const +{ + return m_number; +} diff --git a/src/utils/verse.h b/src/utils/verse.h new file mode 100644 index 00000000..09052b95 --- /dev/null +++ b/src/utils/verse.h @@ -0,0 +1,57 @@ +#ifndef VERSE_H +#define VERSE_H + +#include "../globals.h" +#include "dbmanager.h" + +/** + * @brief Verse class represents a single quran verse + * @details Quran verses consist of 3 attributes. page (1-604). surah (1-114). + * number represents the number of the verse in the surah (0-surah verse count). + * Basmallah before the 1st verse is represented as verse number 0. + */ +class Verse +{ +public: + static Verse* current(); + static QList fromList(QList> lst); + + Verse(); + Verse(const QList vInfo); + Verse(const Verse& cp) = default; + explicit Verse(int page, int surah, int number); + + QList toList() const; + void update(const Verse& v); + void update(const QList& vInfo); + Verse next() const; + Verse prev() const; + + Verse& operator=(const Verse& cp); + Verse& operator=(const QList& vInfo); + bool operator==(const Verse& v2) const; + bool operator!=(const Verse& v2) const; + bool operator<(const Verse& v2) const; + bool operator>(const Verse& v2) const; + + int page() const; + int surah() const; + int number() const; + int surahCount() const; + void setPage(int newPage); + void setSurah(int newSurah); + void setNumber(int newNumber); + +private: + static Verse m_current; + const QSettings* m_settings = Globals::settings; + DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + void updateSurahCount(); + + int m_page = -1; ///< verse page + int m_surah = -1; ///< verse surah number + int m_number = -1; ///< verse number in surah + int m_surahCount = 0; ///< surah verse count +}; + +#endif // VERSE_H diff --git a/src/utils/verseplayer.cpp b/src/utils/verseplayer.cpp index ecd8a71e..8de5850e 100644 --- a/src/utils/verseplayer.cpp +++ b/src/utils/verseplayer.cpp @@ -5,11 +5,10 @@ #include "verseplayer.h" -VersePlayer::VersePlayer(QObject* parent, Verse initVerse, int reciterIdx) +VersePlayer::VersePlayer(QObject* parent, int reciterIdx) : QMediaPlayer(parent) - , m_activeVerse{ initVerse } - , m_reciter{ reciterIdx } - , m_output{ new QAudioOutput(this) } + , m_reciter(reciterIdx) + , m_output(new QAudioOutput(this)) { setAudioOutput(m_output); @@ -44,12 +43,6 @@ VersePlayer::stop() QMediaPlayer::stop(); } -void -VersePlayer::setVerse(Verse& newVerse) -{ - m_activeVerse = newVerse; -} - void VersePlayer::changeUsedAudioDevice(QAudioDevice dev) { @@ -64,12 +57,12 @@ VersePlayer::setPlayerVolume(qreal volume) } QString -VersePlayer::constructVerseFilename(Verse v) +VersePlayer::constructVerseFilename(const Verse* v) { // construct verse mp3 filename e.g. 002005.mp3 QString filename; - filename.append(QString::number(v.surah).rightJustified(3, '0')); - filename.append(QString::number(v.number).rightJustified(3, '0')); + filename.append(QString::number(v->surah()).rightJustified(3, '0')); + filename.append(QString::number(v->number()).rightJustified(3, '0')); filename.append(".mp3"); return filename; @@ -85,8 +78,8 @@ VersePlayer::playCurrentVerse() bool VersePlayer::changeReciter(int reciterIdx) { - if (m_activeVerse.number == 0) - m_activeVerse.number = 1; + if (m_activeVerse->number() == 0) + m_activeVerse->setNumber(1); stop(); if (reciterIdx != m_reciter) { @@ -104,7 +97,7 @@ VersePlayer::setVerseFile(const QString& newVerseFilename) if (!m_reciterDir.exists(newVerseFilename)) { setSource(QUrl()); qDebug() << "file " + newVerseFilename + " is missing."; - emit missingVerseFile(m_reciter, m_activeVerse.surah); + emit missingVerseFile(m_reciter, m_activeVerse->surah()); return false; } @@ -117,7 +110,7 @@ VersePlayer::setVerseFile(const QString& newVerseFilename) bool VersePlayer::loadActiveVerse() { - if (m_activeVerse.number == 0) { + if (m_activeVerse->number() == 0) { setSource(QUrl::fromLocalFile(m_recitersList.at(m_reciter).basmallahPath)); return true; } @@ -125,8 +118,6 @@ VersePlayer::loadActiveVerse() return setVerseFile(constructVerseFilename(m_activeVerse)); } -/* -------------------- Getters ----------------------- */ - QString VersePlayer::reciterName() const { @@ -144,9 +135,3 @@ VersePlayer::verseFilename() const { return m_verseFile; } - -Verse -VersePlayer::activeVerse() const -{ - return m_activeVerse; -} diff --git a/src/utils/verseplayer.h b/src/utils/verseplayer.h index c3866d92..86ed4c99 100644 --- a/src/utils/verseplayer.h +++ b/src/utils/verseplayer.h @@ -8,6 +8,7 @@ #include "../globals.h" #include "dbmanager.h" +#include "verse.h" #include #include #include @@ -30,9 +31,7 @@ class VersePlayer : public QMediaPlayer * @param initVerse - inital ::Verse to load recitation for * @param reciterIdx - ::Globals::recitersList index for the reciter */ - explicit VersePlayer(QObject* parent = nullptr, - Verse initVerse = Verse{}, - int reciterIdx = 0); + explicit VersePlayer(QObject* parent, int reciterIdx); /** * @brief construct the verse filename for the ::Verse given using the @@ -40,12 +39,7 @@ class VersePlayer : public QMediaPlayer * @param v - ::Verse to get the filename of * @return QString of the filename */ - QString constructVerseFilename(Verse v); - /** - * @brief setter for the m_activeVerse attribute - * @param newVerse - ::Verse instance - */ - void setVerse(Verse& newVerse); + QString constructVerseFilename(const Verse* v); /** * @brief load the verse mp3 from the current reciter directory and set the * m_verseFile variable @@ -69,11 +63,6 @@ class VersePlayer : public QMediaPlayer * @return QString containing the verse filename */ QString verseFilename() const; - /** - * @brief getter for m_activeVerse - * @return ::Verse instance - */ - Verse activeVerse() const; /** * @brief getter for m_output, used for controlling audio output device * @return pointer to the used QAudioOutput object @@ -127,6 +116,7 @@ public slots: void missingVerseFile(int reciterIdx, int surah); private: + Verse* m_activeVerse = Verse::current(); QDir m_reciterDir = Globals::downloadsDir.absoluteFilePath("recitations"); const QList& m_recitersList = Globals::recitersList; DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); @@ -139,10 +129,6 @@ public slots: * @brief ::Globals::recitersList index for the reciter */ int m_reciter = 0; - /** - * @brief ::Verse instance representing the current verse being played - */ - Verse m_activeVerse; /** * @brief current verse mp3 filename */ diff --git a/src/widgets/notificationpopup.cpp b/src/widgets/notificationpopup.cpp index e1e1a3b9..a5c9846e 100644 --- a/src/widgets/notificationpopup.cpp +++ b/src/widgets/notificationpopup.cpp @@ -56,7 +56,7 @@ NotificationPopup::setupConnections() } void -NotificationPopup::dockLocationChanged(Qt::DockWidgetArea dockPos) +NotificationPopup::setDockArea(Qt::DockWidgetArea dockPos) { m_dockArea = dockPos; } diff --git a/src/widgets/notificationpopup.h b/src/widgets/notificationpopup.h index f6c6388c..0d6f44f7 100644 --- a/src/widgets/notificationpopup.h +++ b/src/widgets/notificationpopup.h @@ -71,7 +71,7 @@ public slots: * @brief slot to update m_dockArea variable on dock position change * @param dockPos - new dock position relative to the main window */ - void dockLocationChanged(Qt::DockWidgetArea dockPos); + void setDockArea(Qt::DockWidgetArea dockPos); /** * @brief slot to show a notification on download completion * @param reciterIdx - ::Globals::recitersList index for the reciter diff --git a/src/widgets/versedialog.cpp b/src/widgets/versedialog.cpp index 0c7eaee7..81a37066 100644 --- a/src/widgets/versedialog.cpp +++ b/src/widgets/versedialog.cpp @@ -12,14 +12,17 @@ VerseDialog::VerseDialog(QWidget* parent) ui->lbContent->setFont(sideFont); ui->lbInfo->setFont(sideFont); ui->lbVerse->setFont(QFont("kfgqpc_hafs_uthmanic _script", 20)); + + if (m_settings->value("VOTD").toBool()) + showVOTD(true); } QString VerseDialog::votdStringEntry() const { - QString entry = QString::number(m_votd.page).rightJustified(3, '0') + ":" + - QString::number(m_votd.surah).rightJustified(3, '0') + ":" + - QString::number(m_votd.number).rightJustified(3, '0'); + QString entry = QString::number(m_votd.page()).rightJustified(3, '0') + ":" + + QString::number(m_votd.surah()).rightJustified(3, '0') + ":" + + QString::number(m_votd.number()).rightJustified(3, '0'); return entry; } @@ -79,12 +82,13 @@ void VerseDialog::updateLabels() { ui->lbVerse->setText( - "ﵩ " + m_dbMgr->getVerseText(m_votd.surah, m_votd.number) + " ﵨ"); - ui->lbContent->setText(m_dbMgr->getTranslation(m_votd.surah, m_votd.number)); + "ﵩ " + m_dbMgr->getVerseText(m_votd.surah(), m_votd.number()) + " ﵨ"); + ui->lbContent->setText( + m_dbMgr->getTranslation(m_votd.surah(), m_votd.number())); ui->lbInfo->setText(qApp->translate("BookmarksDialog", "Surah: ") + - m_dbMgr->getSurahName(m_votd.surah) + " - " + + m_dbMgr->getSurahName(m_votd.surah()) + " - " + qApp->translate("BookmarksDialog", "Verse: ") + - QString::number(m_votd.number)); + QString::number(m_votd.number())); } void diff --git a/src/widgets/versedialog.h b/src/widgets/versedialog.h index c281df33..201f45ec 100644 --- a/src/widgets/versedialog.h +++ b/src/widgets/versedialog.h @@ -3,6 +3,7 @@ #include "../globals.h" #include "../utils/dbmanager.h" +#include "../utils/verse.h" #include #include From 433a440086d8fa8f3b42d07ccdd266907a2aa3a9 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Wed, 14 Feb 2024 10:04:51 +0200 Subject: [PATCH 02/51] Refactoring: Restore missing functionality --- src/core/mainwindow.cpp | 26 +++++- src/core/playercontrols.cpp | 1 + src/core/playercontrols.h | 1 + src/core/playercontrols.ui | 4 +- src/core/quranreader.cpp | 1 + src/core/tafsirdialog.cpp | 33 +++---- src/core/tafsirdialog.h | 2 +- src/core/tafsirdialog.ui | 171 +++++++++++++++++++----------------- src/utils/verse.cpp | 21 ++++- src/utils/verse.h | 4 +- 10 files changed, 156 insertions(+), 108 deletions(-) diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index afc43241..37956361 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -256,6 +256,14 @@ MainWindow::setupConnections() &QuranReader::currentSurahChanged, this, &MainWindow::currentSurahChanged); + connect(m_playerControls, + &PlayerControls::currentVerseChanged, + this, + &MainWindow::currentVerseChanged); + connect(m_playerControls, + &PlayerControls::currentSurahChanged, + this, + &MainWindow::currentSurahChanged); // ########## navigation dock ########## // connect(ui->lineEditSearchSurah, @@ -376,6 +384,18 @@ MainWindow::setupConnections() &QMediaPlayer::playbackStateChanged, this, &MainWindow::updateTrayTooltip); + connect(m_player, + &QMediaPlayer::mediaStatusChanged, + this, + &MainWindow::mediaStatusChanged); + + connect(m_reader, + &QuranReader::showVerseTafsir, + this, + &MainWindow::showVerseTafsir); + connect(m_reader, &QuranReader::showBetaqa, this, [this](int surah) { + m_betaqaViewer->showSurah(surah); + }); } void @@ -392,15 +412,15 @@ MainWindow::init() m_settingsDlg = new SettingsDialog(this, m_player); QHBoxLayout* controls = new QHBoxLayout(); + QFrame* controlsFrame = new QFrame(this); controls->setAlignment(Qt::AlignCenter); controls->setContentsMargins(0, 0, 0, 0); controls->setSpacing(0); controls->addStretch(1); controls->addWidget(m_playerControls); controls->addStretch(1); - QFrame* frm = new QFrame(this); - frm->setLayout(controls); - ui->scrollAreaWidgetContents->layout()->addWidget(frm); + controlsFrame->setLayout(controls); + ui->scrollAreaWidgetContents->layout()->addWidget(controlsFrame); ui->scrollAreaWidgetContents->layout()->addWidget(m_reader); ui->cmbPage->setValidator(new QIntValidator(1, 604, this)); diff --git a/src/core/playercontrols.cpp b/src/core/playercontrols.cpp index edac9d48..db8055dc 100644 --- a/src/core/playercontrols.cpp +++ b/src/core/playercontrols.cpp @@ -129,6 +129,7 @@ PlayerControls::btnStopClicked() { m_player->stop(); m_reader->setVerseToStartOfPage(); + emit currentVerseChanged(); emit currentSurahChanged(); } diff --git a/src/core/playercontrols.h b/src/core/playercontrols.h index c99f0744..e3d95c5e 100644 --- a/src/core/playercontrols.h +++ b/src/core/playercontrols.h @@ -32,6 +32,7 @@ public slots: void togglePlayback(); signals: + void currentVerseChanged(); void currentSurahChanged(); private slots: diff --git a/src/core/playercontrols.ui b/src/core/playercontrols.ui index 88c0681b..4cca8d93 100644 --- a/src/core/playercontrols.ui +++ b/src/core/playercontrols.ui @@ -51,13 +51,13 @@ - 25 + 27 5 - 25 + 27 5 diff --git a/src/core/quranreader.cpp b/src/core/quranreader.cpp index c8736c27..f17246db 100644 --- a/src/core/quranreader.cpp +++ b/src/core/quranreader.cpp @@ -11,6 +11,7 @@ QuranReader::QuranReader(QWidget* parent, , m_player(player) { ui->setupUi(this); + setLayoutDirection(Qt::LeftToRight); loadIcons(); loadReader(); updateHighlight(); diff --git a/src/core/tafsirdialog.cpp b/src/core/tafsirdialog.cpp index 801a0638..ee4ca022 100644 --- a/src/core/tafsirdialog.cpp +++ b/src/core/tafsirdialog.cpp @@ -20,7 +20,7 @@ TafsirDialog::TafsirDialog(QWidget* parent) ui->cmbTafsir->addItem(t.displayName); } setTafsirAsTitle(); - setLayoutDirection(Qt::LeftToRight); + ui->frmNav->setLayoutDirection(Qt::LeftToRight); if (m_qcfVer == 1) m_fontSZ = 18; else @@ -33,20 +33,17 @@ TafsirDialog::TafsirDialog(QWidget* parent) void TafsirDialog::btnNextClicked() { - if (m_shownVerse.number() == m_shownVerse.surahCount() && - m_shownVerse.surah() < 114) { - m_shownVerse = m_shownVerse.next(); - loadVerseTafsir(); - } + m_shownVerse = m_shownVerse.next(); + loadVerseTafsir(); } void TafsirDialog::btnPrevClicked() { - if (m_shownVerse.number() == 1 && m_shownVerse.surah() > 1) { - m_shownVerse = m_shownVerse.prev(); - loadVerseTafsir(); - } + if (m_shownVerse.number() == 1) + m_shownVerse.setNumber(0); + m_shownVerse = m_shownVerse.prev(); + loadVerseTafsir(); } void @@ -68,12 +65,16 @@ TafsirDialog::setTafsirAsTitle() void TafsirDialog::loadVerseTafsir() { - QString title = tr("Surah: ") + m_dbMgr->getSurahName(m_shownVerse.surah()) + - " - " + tr("Verse: ") + QString::number(m_shownVerse.number()); + if (!m_shownVerse.number()) + m_shownVerse.setNumber(1); + + QString title = tr("Surah: ") + m_dbMgr->getSurahName(m_shownVerse.surah()) + + " - " + tr("Verse: ") + + QString::number(m_shownVerse.number()); QString glyphs = - m_dbMgr->getVerseGlyphs(m_shownVerse.surah(), m_shownVerse.number()); + m_dbMgr->getVerseGlyphs(m_shownVerse.surah(), m_shownVerse.number()); QString fontFamily = - Globals::verseFontname(m_dbMgr->getVerseType(), m_shownVerse.page()); + Globals::verseFontname(m_dbMgr->getVerseType(), m_shownVerse.page()); ui->lbVerseInfo->setText(title); ui->lbVerseText->setWordWrap(true); @@ -86,10 +87,10 @@ TafsirDialog::loadVerseTafsir() if (m_dbMgr->currTafsir()->text) ui->tedTafsir->setText( - m_dbMgr->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); + m_dbMgr->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); else ui->tedTafsir->setHtml( - m_dbMgr->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); + m_dbMgr->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); if (m_shownVerse.surah() == 1 && m_shownVerse.number() == 1) ui->btnPrev->setDisabled(true); diff --git a/src/core/tafsirdialog.h b/src/core/tafsirdialog.h index f077d232..9a577418 100644 --- a/src/core/tafsirdialog.h +++ b/src/core/tafsirdialog.h @@ -95,7 +95,7 @@ private slots: /** * @brief ::Verse instance representing the shown verse. */ - Verse m_shownVerse{ 1, 1, 1 }; + Verse m_shownVerse; }; #endif // TAFSIRDIALOG_H diff --git a/src/core/tafsirdialog.ui b/src/core/tafsirdialog.ui index 24388c16..643aeef8 100644 --- a/src/core/tafsirdialog.ui +++ b/src/core/tafsirdialog.ui @@ -73,88 +73,95 @@ - - - 3 - - - 3 - - - - - - 0 - 25 - - - - - 16777215 - 25 - - - - - Noto Sans Display - 12 - false - - - - PointingHandCursor - - - next - - - - - - Left - - - - - - - - 0 - 25 - - - - - 16777215 - 25 - - - - - Noto Sans Display - 12 - 50 - false - false - false - true - - - - PointingHandCursor - - - previous - - - - - - Right - - - - + + + + 0 + + + 3 + + + 0 + + + 3 + + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + + Noto Sans Display + 12 + false + + + + PointingHandCursor + + + next + + + + + + Left + + + + + + + + 0 + 25 + + + + + 16777215 + 25 + + + + + Noto Sans Display + 12 + false + false + false + true + + + + PointingHandCursor + + + previous + + + + + + Right + + + + + diff --git a/src/utils/verse.cpp b/src/utils/verse.cpp index 971903a5..6bba7bef 100644 --- a/src/utils/verse.cpp +++ b/src/utils/verse.cpp @@ -99,16 +99,33 @@ Verse::update(const QList& vInfo) } Verse -Verse::next() const +Verse::next() { + if (!m_number) { + m_number = 1; + return *this; + } + QList vInfo = m_dbMgr->getVerseById(m_dbMgr->getVerseId(m_surah, m_number) + 1); + + if (vInfo[2] == 1 && vInfo[1] != 9 && vInfo[1] != 1) + vInfo[2] = 0; + return Verse(vInfo); } Verse -Verse::prev() const +Verse::prev() { + if (m_number == 1 && m_surah != 9 && m_surah != 1) { + m_number = 0; + return *this; + } + + if (!m_number) + m_number = 1; + QList vInfo = m_dbMgr->getVerseById(m_dbMgr->getVerseId(m_surah, m_number) - 1); return Verse(vInfo); diff --git a/src/utils/verse.h b/src/utils/verse.h index 09052b95..e054227c 100644 --- a/src/utils/verse.h +++ b/src/utils/verse.h @@ -24,8 +24,8 @@ class Verse QList toList() const; void update(const Verse& v); void update(const QList& vInfo); - Verse next() const; - Verse prev() const; + Verse next(); + Verse prev(); Verse& operator=(const Verse& cp); Verse& operator=(const QList& vInfo); From ce397d802d10333ea2596f34af5566145ca7f679 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Wed, 14 Feb 2024 21:18:55 +0200 Subject: [PATCH 03/51] Refactoring: use static DBManager instance instead of global pointer --- dist/translations/qc_template.ts | 275 ++++++++++++----------- dist/translations/update_translations.sh | 4 +- src/core/bookmarksdialog.h | 2 +- src/core/copydialog.h | 3 +- src/core/downloaderdialog.h | 2 +- src/core/khatmahdialog.h | 2 +- src/core/mainwindow.h | 2 +- src/core/playercontrols.cpp | 5 - src/core/playercontrols.h | 6 +- src/core/playercontrols.ui | 2 +- src/core/quranreader.h | 2 +- src/core/quranreader.ui | 2 +- src/core/searchdialog.h | 2 +- src/core/tafsirdialog.h | 2 +- src/globals.cpp | 1 - src/globals.h | 4 - src/main.cpp | 1 - src/utils/dbmanager.cpp | 7 + src/utils/dbmanager.h | 202 ++++++++--------- src/utils/downloadmanager.h | 2 +- src/utils/shortcuthandler.h | 2 + src/utils/systemtray.h | 2 +- src/utils/verse.h | 3 +- src/utils/verseplayer.h | 2 +- src/widgets/betaqaviewer.h | 2 +- src/widgets/notificationpopup.h | 2 +- src/widgets/quranpagebrowser.h | 2 +- src/widgets/versedialog.h | 2 +- 28 files changed, 269 insertions(+), 276 deletions(-) diff --git a/dist/translations/qc_template.ts b/dist/translations/qc_template.ts index 23124d48..8e403c28 100644 --- a/dist/translations/qc_template.ts +++ b/dist/translations/qc_template.ts @@ -558,216 +558,196 @@ - - + + Quran Companion - - Reciter - - - - - next - - - - - previous - - - - + View - + Edit - + File - + Help - - + + Navigation - + Juz - + Page - + Verse - + Search surah - + Preferences - + Download manager - + Exit - + Find - + Check for updates - + Bookmarks - + About Quran Companion - - + + About Qt - + Tafsir - + Verse of the day - + Khatmah - + Advanced copy - + Toggle reader view - + Khatmah - + Default - + There are currently no updates available. - - - + + + Update info - + Updates available, do you want to open the update tool? - + Updates info - + Updates are available, use the maintainance tool to install the latest updates. - + Now playing: - + Surah - + Recitation not found - + The recitation files for the current surah is missing, would you like to download it? - - + + Files Missing - + The selected font files are missing, would you like to download it? - + The selected tafsir is missing, would you like to download it? - - - Verse Of The Day - - AboutDialog @@ -777,82 +757,82 @@ - + Quran Companion - + Version - + About - + A free, open-source Quran reader & player - + Useful Links - + Translators - + Credits - + Recitations - + Tafsir/Translations - + Surah Cards - + Libraries - + Licensed under the - + Waqf General Public License - + Project Homepage - + Report a bug/Request a feature - + Contribute to translations @@ -901,13 +881,13 @@ - + Surah: - + Verse: @@ -940,12 +920,12 @@ - + Invalid range - + The entered verse range is invalid @@ -1032,12 +1012,12 @@ - + Download Completed - + Download Failed @@ -1065,69 +1045,31 @@ - + Set as active - + Remove - + Surah: - + Verse: - + Khatmah - - NotificationManager - - - Play/Pause recitation - - - - - Show window - - - - - Hide window - - - - - Preferences - - - - - Check for updates - - - - - About - - - - - Exit - - - NotificationPopup @@ -1172,6 +1114,14 @@ + + PlayerControls + + + Reciter + + + QuranPageBrowser @@ -1215,6 +1165,19 @@ + + QuranReader + + + next + + + + + previous + + + SearchDialog @@ -1288,17 +1251,17 @@ - + Search results - + Surah: - + Verse: @@ -1316,6 +1279,44 @@ + + SystemTray + + + Play/Pause recitation + + + + + Show window + + + + + Hide window + + + + + Preferences + + + + + Check for updates + + + + + About + + + + + Exit + + + TafsirDialog @@ -1324,32 +1325,32 @@ - + next - + Left - + previous - + Right - + Surah: - + Verse: diff --git a/dist/translations/update_translations.sh b/dist/translations/update_translations.sh index a86a8420..e871dee8 100755 --- a/dist/translations/update_translations.sh +++ b/dist/translations/update_translations.sh @@ -42,5 +42,5 @@ prompt -s "----- Generating tafasir TS file -----\n" "$SCRIPT_DIR/tafasir.sh" prompt -s "----- Updating template TS file -----\n" -lupdate -recursive -no-obsolete "$SCRIPT_DIR/../../src" -ts "$SCRIPT_DIR/qc_src.ts" -lconvert -i "$SCRIPT_DIR/shortcuts.ts" "$SCRIPT_DIR/reciters.ts" "$SCRIPT_DIR/tafasir.ts" "$SCRIPT_DIR/qc_src.ts" -no-obsolete -o qc_template.ts +~/Qt/6.6.1/gcc_64/bin/lupdate -recursive -no-obsolete "$SCRIPT_DIR/../../src" -ts "$SCRIPT_DIR/qc_src.ts" +~/Qt/6.6.1/gcc_64/bin/lconvert -i "$SCRIPT_DIR/shortcuts.ts" "$SCRIPT_DIR/reciters.ts" "$SCRIPT_DIR/tafasir.ts" "$SCRIPT_DIR/qc_src.ts" -no-obsolete -o qc_template.ts diff --git a/src/core/bookmarksdialog.h b/src/core/bookmarksdialog.h index bcf7ecbc..a74bddf0 100644 --- a/src/core/bookmarksdialog.h +++ b/src/core/bookmarksdialog.h @@ -117,7 +117,7 @@ private slots: private: const int m_qcfVer = Globals::qcfVersion; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/core/copydialog.h b/src/core/copydialog.h index 160e1091..59d42e16 100644 --- a/src/core/copydialog.h +++ b/src/core/copydialog.h @@ -1,7 +1,6 @@ #ifndef COPYDIALOG_H #define COPYDIALOG_H -#include "../globals.h" #include "../utils/dbmanager.h" #include "../utils/verse.h" #include @@ -29,7 +28,7 @@ class CopyDialog : public QDialog private: Ui::CopyDialog* ui; Verse* m_currVerse = Verse::current(); - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); void copyRange(); int m_surah = -1; diff --git a/src/core/downloaderdialog.h b/src/core/downloaderdialog.h index 6e42ef60..b1551e36 100644 --- a/src/core/downloaderdialog.h +++ b/src/core/downloaderdialog.h @@ -122,7 +122,7 @@ private slots: const QList& m_recitersList = Globals::recitersList; const QList& m_tafasirList = Globals::tafasirList; const QList& m_trList = Globals::translationsList; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/core/khatmahdialog.h b/src/core/khatmahdialog.h index bdf802c7..2acda9cd 100644 --- a/src/core/khatmahdialog.h +++ b/src/core/khatmahdialog.h @@ -76,7 +76,7 @@ private slots: const Verse* m_currVerse = Verse::current(); const bool m_darkmode = Globals::themeId == 2; QSettings* m_settings = Globals::settings; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); /** * @brief load all khatmah entries available */ diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 985b3b6b..7e34328b 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -253,7 +253,7 @@ private slots: const QList& m_recitersList = Globals::recitersList; const QList& m_tafasirList = Globals::tafasirList; const QString& m_updateToolPath = Globals::updateToolPath; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); fa::QtAwesome* m_fa = Globals::awesome; Ui::MainWindow* ui; /** diff --git a/src/core/playercontrols.cpp b/src/core/playercontrols.cpp index db8055dc..78f8530e 100644 --- a/src/core/playercontrols.cpp +++ b/src/core/playercontrols.cpp @@ -32,11 +32,6 @@ PlayerControls::loadIcons() ui->lbSpeaker->setFont(m_fa->font(fa_solid, 16)); } -void -PlayerControls::setupConnections() -{ -} - void PlayerControls::setupShortcuts(ShortcutHandler* handler) { diff --git a/src/core/playercontrols.h b/src/core/playercontrols.h index e3d95c5e..20da3bb2 100644 --- a/src/core/playercontrols.h +++ b/src/core/playercontrols.h @@ -83,17 +83,13 @@ private slots: Verse* m_currVerse = Verse::current(); QSettings* const m_settings = Globals::settings; const QList& m_recitersList = Globals::recitersList; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); fa::QtAwesome* m_fa = Globals::awesome; Ui::PlayerControls* ui; /** * @brief load icons for different UI elements */ void loadIcons(); - /** - * @brief connects signals and slots for different UI components - */ - void setupConnections(); /** * @brief connect ShortcutHandler signals to their corresponding slots */ diff --git a/src/core/playercontrols.ui b/src/core/playercontrols.ui index 4cca8d93..90c39d5a 100644 --- a/src/core/playercontrols.ui +++ b/src/core/playercontrols.ui @@ -23,7 +23,7 @@ - Form + diff --git a/src/core/quranreader.h b/src/core/quranreader.h index 534b539f..dd6317c4 100644 --- a/src/core/quranreader.h +++ b/src/core/quranreader.h @@ -149,7 +149,7 @@ private slots: const QList& m_tafasirList = Globals::tafasirList; const QString& m_updateToolPath = Globals::updateToolPath; const ReaderMode& m_readerMode = Globals::readerMode; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); fa::QtAwesome* m_fa = Globals::awesome; Ui::QuranReader* ui; void setupConnections(); diff --git a/src/core/quranreader.ui b/src/core/quranreader.ui index abdea7c7..2876e504 100644 --- a/src/core/quranreader.ui +++ b/src/core/quranreader.ui @@ -11,7 +11,7 @@ - Form + diff --git a/src/core/searchdialog.h b/src/core/searchdialog.h index d94107a5..738cebbc 100644 --- a/src/core/searchdialog.h +++ b/src/core/searchdialog.h @@ -97,7 +97,7 @@ private slots: private: const QLocale::Language m_lang = Globals::language; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/core/tafsirdialog.h b/src/core/tafsirdialog.h index 9a577418..132e9d78 100644 --- a/src/core/tafsirdialog.h +++ b/src/core/tafsirdialog.h @@ -74,7 +74,7 @@ private slots: private: const int m_qcfVer = Globals::qcfVersion; const QSettings* m_settings = Globals::settings; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/globals.cpp b/src/globals.cpp index ebce296c..2db93aa6 100644 --- a/src/globals.cpp +++ b/src/globals.cpp @@ -12,7 +12,6 @@ int themeId = 0; ReaderMode readerMode = ReaderMode::SinglePage; bool darkMode = false; QSettings* settings = nullptr; -QObject* databaseManager = nullptr; QLocale::Language language; // qcf fonts diff --git a/src/globals.h b/src/globals.h index a7e92945..9fa2072a 100644 --- a/src/globals.h +++ b/src/globals.h @@ -129,10 +129,6 @@ extern QMap ///< application shortcuts as keys and their descriptions ///< as values. -extern QObject* databaseManager; ///< global pointer to the application-wide - ///< DBManager instance for interacting with - ///< different database files. - extern fa::QtAwesome* awesome; ///< global pointer used for generating font awesome icons diff --git a/src/main.cpp b/src/main.cpp index 30d2a1cd..688dd795 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -141,7 +141,6 @@ main(int argc, char* argv[]) populateShortcutsMap(); populateContentLists(); - databaseManager = new DBManager(&a); MainWindow w(nullptr); splash.finish(&w); w.show(); diff --git a/src/utils/dbmanager.cpp b/src/utils/dbmanager.cpp index 9193d23c..e1c82c2e 100644 --- a/src/utils/dbmanager.cpp +++ b/src/utils/dbmanager.cpp @@ -5,6 +5,13 @@ #include "dbmanager.h" +DBManager* +DBManager::instance() +{ + static DBManager controller = DBManager(qApp); + return &controller; +} + DBManager::DBManager(QObject* parent) : QObject(parent) { diff --git a/src/utils/dbmanager.h b/src/utils/dbmanager.h index abcc8e01..6304ab22 100644 --- a/src/utils/dbmanager.h +++ b/src/utils/dbmanager.h @@ -26,118 +26,118 @@ class DBManager : public QObject Q_OBJECT public: - /** + static DBManager *instance(); + /** * @brief Database enum holds different values representing database files * used in different member functions. */ - enum Database - { - null, ///< default value - quran, ///< (quran.db) main Quran database file - glyphs, ///< (glyphs.db) QCF glyphs database - betaqat, - bookmarks, ///< (bookmarks.db) bookmarked verses and khatmah database - tafsir, ///< currently selected tafsir database file - translation ///< currently selected translation database file - }; + enum Database { + null, ///< default value + quran, ///< (quran.db) main Quran database file + glyphs, ///< (glyphs.db) QCF glyphs database + betaqat, + bookmarks, ///< (bookmarks.db) bookmarked verses and khatmah database + tafsir, ///< currently selected tafsir database file + translation ///< currently selected translation database file + }; - /** + /** * @brief Class constructor * @param parent - pointer to parent widget */ - explicit DBManager(QObject* parent = nullptr); - /** + explicit DBManager(QObject *parent = nullptr); + /** * @brief getter for translated Surah names QList * @return QList of Surah name strings */ - QList surahNameList(); - /** + QList surahNameList(); + /** * @brief sets the active tafsir * @param tafsirName - DBManager::Tafsir entry */ - void setCurrentTafsir(int tafsirIdx); - /** + void setCurrentTafsir(int tafsirIdx); + /** * @brief sets the active translation * @param translationName - DBManager::Translation entry */ - void setCurrentTranslation(int translationIdx); - /** + void setCurrentTranslation(int translationIdx); + /** * @brief sets the currently active sqlite database file and opens * corresponding connection based on the DBManager::Database type * @param db - DBManager::Database entry * @param filePath - path to the database file */ - void setOpenDatabase(Database db, QString filePath); - /** + void setOpenDatabase(Database db, QString filePath); + /** * @brief gets the surah number and juz number of the first verse in the page, * used to display page header information * @param page - Quran page number * @return QList of 2 integers [0: surah index, 1: juz number] */ - QPair getPageMetadata(const int page); - /** + QPair getPageMetadata(const int page); + /** * @brief get Quran page QCF glyphs separated as lines * @param page - Quran page number * @return QList of page lines */ - QStringList getPageLines(const int page); - /** + QStringList getPageLines(const int page); + /** * @brief gets a QList of ::Verse instances for the page verses * @param page - Quran page number * @return QList of ::Verse instances */ - QList> getVerseInfoList(const int page); - /** + QList> getVerseInfoList(const int page); + /** * @brief gets the surah name glyph for the QCF_BSML font, used to render * surah frame in Quran page * @param sura - sura number (1-114) * @return QString of glyphs */ - QString getSurahNameGlyph(const int sura); - /** + QString getSurahNameGlyph(const int sura); + /** * @brief gets the juz name in arabic, used in page header * @param juz - juz number * @return QString of the juz name */ - QString getJuzGlyph(const int juz); - /** + QString getJuzGlyph(const int juz); + /** * @brief gets the verse QCF glyphs for the corresponding QCF page font * @param sIdx - sura number (1-114) * @param vIdx - verse number * @return QString of verse glyphs */ - QString getVerseGlyphs(const int sIdx, const int vIdx); - /** + QString getVerseGlyphs(const int sIdx, const int vIdx); + /** * @brief gets the verse text * @param sIdx - sura number (1-114) * @param vIdx - verse number * @return QString of the verse text */ - QString getVerseText(const int sIdx, const int vIdx); - /** + QString getVerseText(const int sIdx, const int vIdx); + /** * @brief sets the given ::Verse as the last position reached in the current * active khatmah * @param v - ::Verse reached in khatmah */ - bool saveActiveKhatmah(QList vInfo); - /** + bool saveActiveKhatmah(QList vInfo); + /** * @brief get all available khatmah ids * @return QList of khatmah id(s) */ - QList getAllKhatmah(); - /** + QList getAllKhatmah(); + /** * @brief get the name of the khatmah with id given * @return QString containing the khatmah name */ - QString getKhatmahName(const int id); - /** + QString getKhatmahName(const int id); + /** * @brief gets the last position saved for the khatmah with the id given and * stores the position in the ::Verse v * @return boolean indicating a successful operation (false in case of error * and in case id does not exist) */ - bool getKhatmahPos(const int khatmahId, QList& vInfo); - /** + bool getKhatmahPos(const int khatmahId, QList &vInfo); + /** * @brief add a new khatmah/replace khatmah with given id with position of * ::Verse v * @param v - ::Verse to set as the khatmah position @@ -145,176 +145,176 @@ class DBManager : public QObject * @param id - id of khatmah to replace, -1 means do not replace (default: -1) * @return id of newly added khatmah or id parameter if defined */ - int addKhatmah(QList vInfo, const QString name, const int id = -1); - /** + int addKhatmah(QList vInfo, const QString name, const int id = -1); + /** * @brief rename the khatmah with the given id to newName * @param khatmahId - id of khatmah to rename * @param newName - new name to set * @return boolean indicating a successful operation (false in case the name * exists) */ - bool editKhatmahName(const int khatmahId, QString newName); - /** + bool editKhatmahName(const int khatmahId, QString newName); + /** * @brief remove the khatmah with the given id from database * @param id - id of khatmah to remove */ - void removeKhatmah(const int id); - /** + void removeKhatmah(const int id); + /** * @brief gets the number of the last verse in the surah passed * @param surahIdx - surah number (1-114) * @return number of verses in the sura */ - int getSurahVerseCount(const int surahIdx); - /** + int getSurahVerseCount(const int surahIdx); + /** * @brief gets the page where the surah begins * @param surahIdx - sura number * @return page of the first verse in the sura */ - int getSurahStartPage(int surahIdx); - /** + int getSurahStartPage(int surahIdx); + /** * @brief gets the surah name in English or Arabic (default is English) * @param sIdx - sura number * @param ar - boolean to return arabic sura name * @return QString containing the sura name */ - QString getSurahName(const int sIdx, bool ar = false); - /** + QString getSurahName(const int sIdx, bool ar = false); + /** * @brief get the surah card (betaqa) content * @param surah - surah number * @return QString of the surah card text */ - QString getBetaqa(const int surah); - /** + QString getBetaqa(const int surah); + /** * @brief gets the corresponding id for the verse in the database * @param sIdx - sura number * @param vIdx - verse number * @return id of the verse */ - int getVerseId(const int sIdx, const int vIdx); - /** + int getVerseId(const int sIdx, const int vIdx); + /** * @brief get the verse with the corresponding id and return it as a ::Verse * instance * @param id - verse id * @return ::Verse instance */ - QList getVerseById(const int id); - /** + QList getVerseById(const int id); + /** * @brief gets the page where the verse is found * @param surahIdx - sura number * @param verse - verse number * @return page number */ - int getVersePage(const int& surahIdx, const int& verse); - /** + int getVersePage(const int &surahIdx, const int &verse); + /** * @brief gets the page where the corresponding juz starts * @param juz - juz number * @return page number */ - int getJuzStartPage(const int juz); - /** + int getJuzStartPage(const int juz); + /** * @brief get the juz which the passed page is a part of * @param page - page number * @return juz number */ - int getJuzOfPage(const int page); - /** + int getJuzOfPage(const int page); + /** * @brief searches the database for surahs matching the given text pattern, * the pattern can be either in English or Arabic * @param text - name / part of the name of the sura * @return QList of sura numbers which contain the given text */ - QList searchSurahNames(QString text); - /** + QList searchSurahNames(QString text); + /** * @brief search specific surahs for the given search text * @param searchText - text to search for * @param surahs - QList of surah numbers to search in * @param whole - boolean value to search for whole words only * @return QList of ::Verse instances representing the search results */ - QList> searchSurahs(QString searchText, - const QList surahs, - const bool whole = false); - /** + QList> searchSurahs(QString searchText, + const QList surahs, + const bool whole = false); + /** * @brief search a range of pages for the given search text * @param searchText - text to search for * @param range - array of start & end page numbers * @param whole - boolean value to indicate search for whole words only * @return QList of ::Verse instances representing the search results */ - QList> searchVerses(QString searchText, - const int range[2] = new int[2]{ 1, 604 }, - const bool whole = false); - /** + QList> searchVerses(QString searchText, + const int range[2] = new int[2]{1, 604}, + const bool whole = false); + /** * @brief gets the tafsir content for the given verse using the active * DBManager::Tafsir * @param sIdx - surah number * @param vIdx - verse number * @return QString containing the tafsir of the verse */ - QString getTafsir(const int sIdx, const int vIdx); - /** + QString getTafsir(const int sIdx, const int vIdx); + /** * @brief gets the translation of the given verse using the active * DBManager::Translation * @param sIdx - surah number * @param vIdx - verse number * @return QString containing the verse translation */ - QString getTranslation(const int sIdx, const int vIdx); - /** + QString getTranslation(const int sIdx, const int vIdx); + /** * @brief gets a random verse from the Quran * @return QPair of ::Verse instance and verse text */ - QList randomVerse(); - /** + QList randomVerse(); + /** * @brief gets a QList of ::Verse instances representing the bookmarked verse * within the given sura (default gets all) * @param surahIdx - sura number (-1 returns all bookmarks) * @return QList of bookmarked verses */ - QList> bookmarkedVerses(int surahIdx = -1); - /** + QList> bookmarkedVerses(int surahIdx = -1); + /** * @brief checks whether the given ::Verse is bookmarked * @param vInfo - ::Verse instance to check * @return boolean */ - bool isBookmarked(QList vInfo); - /** + bool isBookmarked(QList vInfo); + /** * @brief add the given ::Verse to bookmarks * @param vInfo - ::Verse instance to add * @return boolean */ - bool addBookmark(QList vInfo); - /** + bool addBookmark(QList vInfo); + /** * @brief remove the given ::Verse from bookmarks * @param vInfo - ::Verse instance to remove * @return boolean indicating successful removal */ - bool removeBookmark(QList vInfo); - /** + bool removeBookmark(QList vInfo); + /** * @brief getter for m_currTafsir * @return the currently set DBManager::Tafasir */ - const Tafsir* currTafsir() const; - /** + const Tafsir *currTafsir() const; + /** * @brief getter for m_activeKhatmah * @return the currently active khatmah id */ - const int activeKhatmah() const; - /** + const int activeKhatmah() const; + /** * @brief setter for m_activeKhatmah * @param id - id of the active khatmah */ - void setActiveKhatmah(const int id); - /** + void setActiveKhatmah(const int id); + /** * @brief Set the VerseType shown * @param newVerseType */ - void setVerseType(VerseType newVerseType); - /** + void setVerseType(VerseType newVerseType); + /** * @brief getter for m_verseType * @return VerseType */ - VerseType getVerseType() const; + VerseType getVerseType() const; private: const QDir& m_assetsDir = Globals::assetsDir; diff --git a/src/utils/downloadmanager.h b/src/utils/downloadmanager.h index 79692e6f..34e9e34c 100644 --- a/src/utils/downloadmanager.h +++ b/src/utils/downloadmanager.h @@ -189,7 +189,7 @@ public slots: const QList& m_recitersList = Globals::recitersList; const QList& m_tafasirList = Globals::tafasirList; const QList& m_trList = Globals::translationsList; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); /** * @brief generate download url for specified verse using the reciter download * url diff --git a/src/utils/shortcuthandler.h b/src/utils/shortcuthandler.h index 3e48b3f7..e99595f0 100644 --- a/src/utils/shortcuthandler.h +++ b/src/utils/shortcuthandler.h @@ -29,6 +29,8 @@ class ShortcutHandler : public QObject */ explicit ShortcutHandler(QObject* parent = nullptr); + void setContext(QWidget* newContext); + public slots: /** * @brief slot to reload the key sequence for the shortcut with the given key diff --git a/src/utils/systemtray.h b/src/utils/systemtray.h index 3d55b4c9..f816e8f8 100644 --- a/src/utils/systemtray.h +++ b/src/utils/systemtray.h @@ -84,7 +84,7 @@ class SystemTray : public QObject void openAbout(); private: - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); /** * @brief adds system tray actions and set their connections */ diff --git a/src/utils/verse.h b/src/utils/verse.h index e054227c..e1b6488f 100644 --- a/src/utils/verse.h +++ b/src/utils/verse.h @@ -43,9 +43,8 @@ class Verse void setNumber(int newNumber); private: - static Verse m_current; const QSettings* m_settings = Globals::settings; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); void updateSurahCount(); int m_page = -1; ///< verse page diff --git a/src/utils/verseplayer.h b/src/utils/verseplayer.h index 86ed4c99..aaaa8a76 100644 --- a/src/utils/verseplayer.h +++ b/src/utils/verseplayer.h @@ -119,7 +119,7 @@ public slots: Verse* m_activeVerse = Verse::current(); QDir m_reciterDir = Globals::downloadsDir.absoluteFilePath("recitations"); const QList& m_recitersList = Globals::recitersList; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); /** * @brief boolean indicating whether the player is on or off, 'on' implies * that playback should continue in case of verse change diff --git a/src/widgets/betaqaviewer.h b/src/widgets/betaqaviewer.h index 47a00674..7337c85d 100644 --- a/src/widgets/betaqaviewer.h +++ b/src/widgets/betaqaviewer.h @@ -28,7 +28,7 @@ class BetaqaViewer : public QWidget private: Ui::BetaqaViewer* ui; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); int m_surah = -1; QGraphicsDropShadowEffect* m_shadowEffect = nullptr; QPropertyAnimation* m_sizeAnim = nullptr; diff --git a/src/widgets/notificationpopup.h b/src/widgets/notificationpopup.h index 0d6f44f7..9c0848b1 100644 --- a/src/widgets/notificationpopup.h +++ b/src/widgets/notificationpopup.h @@ -107,7 +107,7 @@ public slots: const QList& m_recitersList = Globals::recitersList; const QList& m_tafasirList = Globals::tafasirList; const QList& m_trList = Globals::translationsList; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/widgets/quranpagebrowser.h b/src/widgets/quranpagebrowser.h index f86dc656..1864da37 100644 --- a/src/widgets/quranpagebrowser.h +++ b/src/widgets/quranpagebrowser.h @@ -145,7 +145,7 @@ public slots: QSettings* const m_settings = Globals::settings; const int m_qcfVer = Globals::qcfVersion; const bool m_darkMode = Globals::darkMode; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); fa::QtAwesome* m_fa = Globals::awesome; /** * @brief utility for creating menu actions for interacting with the widget diff --git a/src/widgets/versedialog.h b/src/widgets/versedialog.h index 201f45ec..1e91112f 100644 --- a/src/widgets/versedialog.h +++ b/src/widgets/versedialog.h @@ -36,7 +36,7 @@ public slots: private: Ui::VerseDialog* ui; const QSettings* m_settings = Globals::settings; - DBManager* m_dbMgr = qobject_cast(Globals::databaseManager); + DBManager* m_dbMgr = DBManager::instance(); QFile m_timestampFile = Globals::configDir.absoluteFilePath("votd.log"); fa::QtAwesome* m_fa = Globals::awesome; /** From 3998eb4f99219a97a87e9234167610a5b931a316 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:50:31 +0200 Subject: [PATCH 04/51] Refactoring: Replace Globals namespace with meaningful static classes --- CMakeLists.txt | 44 ++- src/core/bookmarksdialog.cpp | 13 +- src/core/bookmarksdialog.h | 8 +- src/core/copydialog.cpp | 53 ++-- src/core/copydialog.h | 16 +- src/core/downloaderdialog.cpp | 65 ++-- src/core/downloaderdialog.h | 11 +- src/core/khatmahdialog.cpp | 13 +- src/core/khatmahdialog.h | 8 +- src/core/mainwindow.cpp | 220 +++++++------- src/core/mainwindow.h | 24 +- src/core/playercontrols.cpp | 32 +- src/core/playercontrols.h | 18 +- src/core/quranreader.cpp | 51 ++-- src/core/quranreader.h | 37 ++- src/core/searchdialog.cpp | 15 +- src/core/searchdialog.h | 7 +- src/core/settingsdialog.cpp | 23 +- src/core/settingsdialog.h | 30 +- src/core/tafsirdialog.cpp | 36 ++- src/core/tafsirdialog.h | 27 +- src/globals.cpp | 75 ----- src/globals.h | 145 --------- src/main.cpp | 442 ++-------------------------- src/types/reciter.cpp | 88 ++++++ src/types/reciter.h | 56 ++++ src/types/tafsir.cpp | 82 ++++++ src/types/tafsir.h | 31 ++ src/types/translation.cpp | 73 +++++ src/types/translation.h | 29 ++ src/{utils => types}/verse.cpp | 0 src/{utils => types}/verse.h | 9 +- src/utils/dbmanager.cpp | 36 +-- src/utils/dbmanager.h | 237 ++++++++------- src/utils/dirmanager.cpp | 43 +++ src/utils/dirmanager.h | 18 ++ src/utils/downloadmanager.cpp | 18 +- src/utils/downloadmanager.h | 19 +- src/utils/fontmanager.cpp | 75 +++++ src/utils/fontmanager.h | 17 ++ src/utils/settings.cpp | 90 ++++++ src/utils/settings.h | 41 +++ src/utils/shortcuthandler.cpp | 59 +++- src/utils/shortcuthandler.h | 11 +- src/utils/stylemanager.cpp | 80 +++++ src/utils/stylemanager.h | 16 + src/utils/systemtray.h | 7 +- src/utils/verseplayer.cpp | 8 +- src/utils/verseplayer.h | 10 +- src/widgets/aboutdialog.h | 4 +- src/widgets/betaqaviewer.cpp | 2 +- src/widgets/betaqaviewer.h | 8 +- src/widgets/downloadprogressbar.cpp | 2 +- src/widgets/downloadprogressbar.h | 3 +- src/widgets/notificationpopup.cpp | 28 +- src/widgets/notificationpopup.h | 11 +- src/widgets/quranpagebrowser.cpp | 32 +- src/widgets/quranpagebrowser.h | 10 +- src/widgets/versedialog.h | 11 +- 59 files changed, 1462 insertions(+), 1215 deletions(-) delete mode 100644 src/globals.cpp delete mode 100644 src/globals.h create mode 100644 src/types/reciter.cpp create mode 100644 src/types/reciter.h create mode 100644 src/types/tafsir.cpp create mode 100644 src/types/tafsir.h create mode 100644 src/types/translation.cpp create mode 100644 src/types/translation.h rename src/{utils => types}/verse.cpp (100%) rename src/{utils => types}/verse.h (86%) create mode 100644 src/utils/dirmanager.cpp create mode 100644 src/utils/dirmanager.h create mode 100644 src/utils/fontmanager.cpp create mode 100644 src/utils/fontmanager.h create mode 100644 src/utils/settings.cpp create mode 100644 src/utils/settings.h create mode 100644 src/utils/stylemanager.cpp create mode 100644 src/utils/stylemanager.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7aac7449..a11bc382 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,8 +30,14 @@ add_subdirectory(third_party/QtAwesome) set(PROJECT_SOURCES src/main.cpp - src/globals.h - src/globals.cpp + src/types/verse.h + src/types/verse.cpp + src/types/reciter.h + src/types/reciter.cpp + src/types/tafsir.h + src/types/tafsir.cpp + src/types/translation.h + src/types/translation.cpp src/core/mainwindow.cpp src/core/mainwindow.h src/core/mainwindow.ui @@ -62,6 +68,26 @@ set(PROJECT_SOURCES src/core/copydialog.h src/core/copydialog.cpp src/core/copydialog.ui + src/utils/settings.h + src/utils/settings.cpp + src/utils/shortcuthandler.h + src/utils/shortcuthandler.cpp + src/utils/dbmanager.h + src/utils/dbmanager.cpp + src/utils/verseplayer.h + src/utils/verseplayer.cpp + src/utils/downloadmanager.h + src/utils/downloadmanager.cpp + src/utils/systemtray.h + src/utils/systemtray.cpp + src/utils/logger.h + src/utils/logger.cpp + src/utils/dirmanager.h + src/utils/dirmanager.cpp + src/utils/stylemanager.h + src/utils/stylemanager.cpp + src/utils/fontmanager.h + src/utils/fontmanager.cpp src/widgets/quranpagebrowser.h src/widgets/quranpagebrowser.cpp src/widgets/clickablelabel.cpp @@ -85,20 +111,6 @@ set(PROJECT_SOURCES src/widgets/versedialog.h src/widgets/versedialog.cpp src/widgets/versedialog.ui - src/utils/verse.h - src/utils/verse.cpp - src/utils/shortcuthandler.h - src/utils/shortcuthandler.cpp - src/utils/dbmanager.h - src/utils/dbmanager.cpp - src/utils/verseplayer.h - src/utils/verseplayer.cpp - src/utils/downloadmanager.h - src/utils/downloadmanager.cpp - src/utils/systemtray.h - src/utils/systemtray.cpp - src/utils/logger.h - src/utils/logger.cpp resources.qrc qurancompanion.rc) diff --git a/src/core/bookmarksdialog.cpp b/src/core/bookmarksdialog.cpp index bea4682e..6713c43f 100644 --- a/src/core/bookmarksdialog.cpp +++ b/src/core/bookmarksdialog.cpp @@ -4,6 +4,8 @@ */ #include "bookmarksdialog.h" +#include "../utils/fontmanager.h" +#include "../utils/stylemanager.h" #include "ui_bookmarksdialog.h" #include @@ -13,11 +15,12 @@ BookmarksDialog::BookmarksDialog(QWidget* parent) { ui->setupUi(this); ui->navBar->setLayoutDirection(Qt::LeftToRight); - ui->btnNext->setIcon(Globals::awesome->icon(fa::fa_solid, fa::fa_arrow_left)); + ui->btnNext->setIcon( + StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_left)); ui->btnPrev->setIcon( - Globals::awesome->icon(fa::fa_solid, fa::fa_arrow_right)); + StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_right)); ui->scrollArea->setLayoutDirection(Qt::LeftToRight); - setWindowIcon(Globals::awesome->icon(fa::fa_solid, fa::fa_bookmark)); + setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_bookmark)); ui->listViewBookmarkedSurahs->setModel(&m_surahsModel); loadDefault(); @@ -104,7 +107,7 @@ BookmarksDialog::loadBookmarks(int surah) for (int i = m_startIdx; i < end; i++) { const Verse& verse = m_shownVerses.at(i); QString fontName = - Globals::verseFontname(m_dbMgr->getVerseType(), verse.page()); + FontManager::verseFontname(m_dbMgr->getVerseType(), verse.page()); QFrame* frame = new QFrame(ui->scrlBookmarks); frame->setProperty("bookmark", true); @@ -175,7 +178,7 @@ BookmarksDialog::loadSurahs() std::set surahs; foreach (const Verse& v, m_allBookmarked) { - surahs.insert(v.surah()); + surahs.insert(v.surah()); } for (int s : surahs) { diff --git a/src/core/bookmarksdialog.h b/src/core/bookmarksdialog.h index a74bddf0..ea0adf8d 100644 --- a/src/core/bookmarksdialog.h +++ b/src/core/bookmarksdialog.h @@ -6,9 +6,9 @@ #ifndef BOOKMARKSDIALOG_H #define BOOKMARKSDIALOG_H -#include "../globals.h" +#include "../types/verse.h" #include "../utils/dbmanager.h" -#include "../utils/verse.h" +#include "../utils/settings.h" #include #include #include @@ -116,8 +116,8 @@ private slots: void surahSelected(const QModelIndex& index); private: - const int m_qcfVer = Globals::qcfVersion; - DBManager* m_dbMgr = DBManager::instance(); + const int m_qcfVer = Settings::qcfVersion; + QSharedPointer m_dbMgr = DBManager::current(); /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/core/copydialog.cpp b/src/core/copydialog.cpp index ff359fad..53883ce7 100644 --- a/src/core/copydialog.cpp +++ b/src/core/copydialog.cpp @@ -13,24 +13,18 @@ CopyDialog::CopyDialog(QWidget* parent) } void -CopyDialog::show() +CopyDialog::copyVerseText(const Verse v) { - ui->cmbCopyFrom->clear(); - ui->cmbCopyTo->clear(); - ui->lbCopySurahName->setText(m_dbMgr->getSurahName(m_currVerse->surah())); - - m_surah = m_currVerse->surah(); - for (int i = 1; i <= m_currVerse->surahCount(); i++) { - ui->cmbCopyFrom->addItem(QString::number(i)); - ui->cmbCopyTo->addItem(QString::number(i)); - } - int idx = m_currVerse->number() ? m_currVerse->number() - 1 : 0; - ui->cmbCopyFrom->setCurrentIndex(idx); - ui->cmbCopyTo->setCurrentIndex(idx); - m_verseValidator->setBottom(1); - m_verseValidator->setTop(m_currVerse->surahCount()); - - QDialog::show(); + QClipboard* clip = QApplication::clipboard(); + QString text = m_dbMgr->getVerseText(v.surah(), v.number()); + QString vNum = QString::number(v.number()); + text.remove(text.size() - 1, 1); + text = text.trimmed(); + text = "{" + text + "}"; + text += ' '; + text += "[" + m_dbMgr->surahNameList().at(v.surah() - 1) + ":" + vNum + "]"; + clip->setText(text); + emit verseCopied(); } void @@ -48,14 +42,35 @@ CopyDialog::copyRange() QString final = "{ "; QClipboard* clip = QApplication::clipboard(); for (int i = from; i <= to; i++) { - QString text = m_dbMgr->getVerseText(m_surah, i); + QString text = m_dbMgr->getVerseText(m_currVerse->surah(), i); text.remove(text.size() - 1, 1); text += "(" + QString::number(i) + ") "; final.append(text); } - final += "} [" + m_dbMgr->surahNameList().at(m_surah - 1) + "]"; + final += "} [" + m_dbMgr->surahNameList().at(m_currVerse->surah() - 1) + "]"; clip->setText(final); + emit rangeCopied(); +} + +void +CopyDialog::show() +{ + ui->cmbCopyFrom->clear(); + ui->cmbCopyTo->clear(); + ui->lbCopySurahName->setText(m_dbMgr->getSurahName(m_currVerse->surah())); + + for (int i = 1; i <= m_currVerse->surahCount(); i++) { + ui->cmbCopyFrom->addItem(QString::number(i)); + ui->cmbCopyTo->addItem(QString::number(i)); + } + int idx = m_currVerse->number() ? m_currVerse->number() - 1 : 0; + ui->cmbCopyFrom->setCurrentIndex(idx); + ui->cmbCopyTo->setCurrentIndex(idx); + m_verseValidator->setBottom(1); + m_verseValidator->setTop(m_currVerse->surahCount()); + + QDialog::show(); } void diff --git a/src/core/copydialog.h b/src/core/copydialog.h index 59d42e16..9aa661a6 100644 --- a/src/core/copydialog.h +++ b/src/core/copydialog.h @@ -2,7 +2,7 @@ #define COPYDIALOG_H #include "../utils/dbmanager.h" -#include "../utils/verse.h" +#include "../types/verse.h" #include #include #include @@ -22,16 +22,26 @@ class CopyDialog : public QDialog void show(); +public slots: + /** + * @brief copy to clipboard the text of the verse with the given index + * @param IdxInPage - verse index relative to the start of the page + */ + void copyVerseText(const Verse v); + +signals: + void verseCopied(); + void rangeCopied(); + protected: void closeEvent(QCloseEvent* event); private: Ui::CopyDialog* ui; Verse* m_currVerse = Verse::current(); - DBManager* m_dbMgr = DBManager::instance(); + QSharedPointer m_dbMgr = DBManager::current(); void copyRange(); - int m_surah = -1; QIntValidator* m_verseValidator = new QIntValidator(this); }; diff --git a/src/core/downloaderdialog.cpp b/src/core/downloaderdialog.cpp index 0dd434a8..62c6f96c 100644 --- a/src/core/downloaderdialog.cpp +++ b/src/core/downloaderdialog.cpp @@ -4,6 +4,7 @@ */ #include "downloaderdialog.h" +#include "../utils/stylemanager.h" #include "ui_downloaderdialog.h" DownloaderDialog::DownloaderDialog(QWidget* parent, DownloadManager* downloader) @@ -14,7 +15,7 @@ DownloaderDialog::DownloaderDialog(QWidget* parent, DownloadManager* downloader) { ui->setupUi(this); - setWindowIcon(Globals::awesome->icon(fa::fa_solid, fa::fa_download)); + setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_download)); // treeview setup QStringList headers; @@ -82,9 +83,9 @@ void DownloaderDialog::populateTreeModel() { // add reciters - for (const Reciter& reciter : m_recitersList) { - QStandardItem* item = new QStandardItem(reciter.displayName); - item->setToolTip(reciter.displayName); + for (const QSharedPointer& reciter : m_reciters) { + QStandardItem* item = new QStandardItem(reciter->displayName()); + item->setToolTip(reciter->displayName()); m_treeModel.invisibleRootItem()->appendRow(item); for (int j = 1; j <= 114; j++) { @@ -104,11 +105,11 @@ DownloaderDialog::populateTreeModel() tafsir->setData("tafsir", Qt::UserRole); m_treeModel.invisibleRootItem()->appendRow(tafsir); // -- tafasir - for (int i = 0; i < m_tafasirList.size(); i++) { - const Tafsir& t = m_tafasirList.at(i); - if (!t.extra) + for (int i = 0; i < m_tafasir.size(); i++) { + const QSharedPointer& t = m_tafasir.at(i); + if (!t->isExtra()) continue; - QStandardItem* item = new QStandardItem(t.displayName); + QStandardItem* item = new QStandardItem(t->displayName()); item->setData("tadb", Qt::UserRole); item->setData(i, Qt::UserRole + 1); tafsir->appendRow(item); @@ -120,11 +121,11 @@ DownloaderDialog::populateTreeModel() tafsir->setData("translation", Qt::UserRole); m_treeModel.invisibleRootItem()->appendRow(translation); // -- translations - for (int i = 0; i < m_trList.size(); i++) { - const Translation& tr = m_trList.at(i); - if (!tr.extra) + for (int i = 0; i < m_tr.size(); i++) { + const QSharedPointer& tr = m_tr.at(i); + if (!tr->isExtra()) continue; - QStandardItem* item = new QStandardItem(tr.displayName); + QStandardItem* item = new QStandardItem(tr->displayName()); item->setData("trdb", Qt::UserRole); item->setData(i, Qt::UserRole + 1); translation->appendRow(item); @@ -162,7 +163,7 @@ DownloaderDialog::removeFromDownloading(int reciter, int surah) void DownloaderDialog::addToQueue() { - static int recitersnum = m_recitersList.size(); + static int recitersnum = m_reciters.size(); QModelIndexList selected = ui->treeView->selectionModel()->selectedRows(); foreach (const QModelIndex& i, selected) { @@ -181,19 +182,19 @@ DownloaderDialog::addToQueue() // tafasir else if (i.data(Qt::UserRole).toString() == "tadb") { QPair info(0, i.data(Qt::UserRole + 1).toInt()); - m_downloaderPtr->addToQueue(File, info); - addTaskProgress(File, info); + m_downloaderPtr->addToQueue(DownloadManager::File, info); + addTaskProgress(DownloadManager::File, info); } // translation else if (i.data(Qt::UserRole).toString() == "trdb") { QPair info(1, i.data(Qt::UserRole + 1).toInt()); - m_downloaderPtr->addToQueue(File, info); - addTaskProgress(File, info); + m_downloaderPtr->addToQueue(DownloadManager::File, info); + addTaskProgress(DownloadManager::File, info); } // extras else if (i.data(Qt::UserRole).toString() == "qcf") { - m_downloaderPtr->addToQueue(QCF); - addTaskProgress(QCF); + m_downloaderPtr->addToQueue(DownloadManager::QCF); + addTaskProgress(DownloadManager::QCF); } } @@ -206,17 +207,17 @@ DownloaderDialog::addTaskProgress(DownloadType type, QPair info) { int total = 0; QString objName; - if (type == Recitation) { - QString reciter = m_recitersList.at(info.first).displayName; + if (type == DownloadManager::Recitation) { + QString reciter = m_reciters.at(info.first)->displayName(); QString surahName = m_surahDisplayNames.at(info.second - 1); objName = reciter + tr(" // Surah: ") + surahName; total = m_dbMgr->getSurahVerseCount(info.second); - } else if (type == QCF) { + } else if (type == DownloadManager::QCF) { objName = qApp->translate("SettingsDialog", "QCF V2"); total = 604; - } else if (type == File) { - objName = info.first ? m_trList.at(info.second).displayName - : m_tafasirList.at(info.second).displayName; + } else if (type == DownloadManager::File) { + objName = info.first ? m_tr.at(info.second)->displayName() + : m_tafasir.at(info.second)->displayName(); } QFrame* prgFrm = new QFrame(ui->scrollAreaWidgetContents); @@ -255,7 +256,7 @@ DownloaderDialog::enqueueSurah(int reciter, int surah) return; addToDownloading(reciter, surah); - addTaskProgress(Recitation, QPair(reciter, surah)); + addTaskProgress(DownloadManager::Recitation, QPair(reciter, surah)); m_downloaderPtr->addToQueue(reciter, surah); } @@ -289,13 +290,13 @@ DownloaderDialog::selectDownload(DownloadType type, QPair info) QItemSelectionModel* selector = ui->treeView->selectionModel(); QModelIndex parent; QModelIndex task; - if (type == Recitation) { + if (type == DownloadManager::Recitation) { parent = m_treeModel.index(info.first, 0); task = m_treeModel.index(info.second - 1, 0, parent); - } else if (type == QCF) { + } else if (type == DownloadManager::QCF) { parent = m_treeModel.index(m_treeModel.rowCount() - 1, 0); task = m_treeModel.index(0, 0, parent); - } else if (type == File) { + } else if (type == DownloadManager::File) { parent = m_treeModel.index(m_treeModel.rowCount() - 2 - !info.first, 0); // remove default db indices from current index as defaults are not // downloadable @@ -355,7 +356,7 @@ DownloaderDialog::downloadCompleted(DownloadType type, m_currentBar, &DownloadProgressBar::updateProgress); - if (type == Recitation) + if (type == DownloadManager::Recitation) removeFromDownloading(metainfo[0], metainfo[1]); if (m_currentBar->maximum() == 1) m_currentBar->setFormat("1 / 1"); @@ -376,7 +377,7 @@ DownloaderDialog::topTaskDownloadError(DownloadType type, m_currentBar, &DownloadProgressBar::updateProgress); - if (type == Recitation) + if (type == DownloadManager::Recitation) removeFromDownloading(metainfo[0], metainfo[1]); m_finishedFrames.append(m_frameLst.front()); m_frameLst.pop_front(); @@ -386,7 +387,7 @@ DownloaderDialog::topTaskDownloadError(DownloadType type, void DownloaderDialog::openDownloadsDir() { - QUrl url = QUrl::fromLocalFile(Globals::downloadsDir.absolutePath()); + QUrl url = QUrl::fromLocalFile(DirManager::downloadsDir->absolutePath()); QDesktopServices::openUrl(url); } diff --git a/src/core/downloaderdialog.h b/src/core/downloaderdialog.h index b1551e36..29ecec99 100644 --- a/src/core/downloaderdialog.h +++ b/src/core/downloaderdialog.h @@ -6,7 +6,6 @@ #ifndef DOWNLOADERDIALOG_H #define DOWNLOADERDIALOG_H -#include "../globals.h" #include "../utils/dbmanager.h" #include "../utils/downloadmanager.h" #include "../widgets/downloadprogressbar.h" @@ -118,11 +117,11 @@ private slots: void closeEvent(QCloseEvent* event); private: - const int m_languageCode = Globals::language; - const QList& m_recitersList = Globals::recitersList; - const QList& m_tafasirList = Globals::tafasirList; - const QList& m_trList = Globals::translationsList; - DBManager* m_dbMgr = DBManager::instance(); + QSharedPointer m_dbMgr = DBManager::current(); + const int m_languageCode = Settings::language; + const QList>& m_reciters = Reciter::reciters; + const QList>& m_tafasir = Tafsir::tafasir; + const QList>& m_tr = Translation::translations; /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/core/khatmahdialog.cpp b/src/core/khatmahdialog.cpp index 0952aa85..75147a43 100644 --- a/src/core/khatmahdialog.cpp +++ b/src/core/khatmahdialog.cpp @@ -1,14 +1,15 @@ #include "khatmahdialog.h" +#include "../utils/settings.h" +#include "../utils/stylemanager.h" #include "ui_khatmahdialog.h" -#include -#include +#include KhatmahDialog::KhatmahDialog(QWidget* parent) : QDialog(parent) , ui(new Ui::KhatmahDialog) { ui->setupUi(this); - setWindowIcon(Globals::awesome->icon(fa::fa_solid, fa::fa_list)); + setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_list)); ui->lbCurrKhatmah->setText(m_dbMgr->getKhatmahName(m_dbMgr->activeKhatmah())); loadAll(); @@ -21,12 +22,12 @@ KhatmahDialog::KhatmahDialog(QWidget* parent) InputField* KhatmahDialog::loadKhatmah(const int id) { - QList vInfo; + QList vInfo(3); m_dbMgr->getKhatmahPos(id, vInfo); QFrame* frame = new QFrame(ui->scrlDialogContent); // property used for styling frame->setProperty("khatmah", true); - if (!m_darkmode) { + if (!Settings::darkMode) { QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect(frame); shadow->setColor(palette().color(QPalette::Shadow)); shadow->setOffset(1); @@ -143,7 +144,7 @@ KhatmahDialog::removeKhatmah() void KhatmahDialog::setActiveKhatmah() { - QList vInfo; + QList vInfo(3); QFrame* newActive = qobject_cast(sender()->parent()); QVariant id = newActive->objectName(); diff --git a/src/core/khatmahdialog.h b/src/core/khatmahdialog.h index 2acda9cd..2f9be19a 100644 --- a/src/core/khatmahdialog.h +++ b/src/core/khatmahdialog.h @@ -1,9 +1,8 @@ #ifndef KHATMAHDIALOG_H #define KHATMAHDIALOG_H -#include "../globals.h" +#include "../types/verse.h" #include "../utils/dbmanager.h" -#include "../utils/verse.h" #include "../widgets/inputfield.h" #include #include @@ -74,9 +73,8 @@ private slots: private: const Verse* m_currVerse = Verse::current(); - const bool m_darkmode = Globals::themeId == 2; - QSettings* m_settings = Globals::settings; - DBManager* m_dbMgr = DBManager::instance(); + QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_settings = Settings::settings; /** * @brief load all khatmah entries available */ diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 37956361..aa6ef4f0 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -4,20 +4,22 @@ */ #include "mainwindow.h" +#include "../utils/stylemanager.h" #include "../widgets/aboutdialog.h" #include "khatmahdialog.h" #include "ui_mainwindow.h" +#include using namespace fa; MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_process(new QProcess(this)) - , m_shortcutHandler(new ShortcutHandler(this)) + , m_shortcutHandler(ShortcutHandler::current()) { ui->setupUi(this); - loadCurrent(); loadIcons(); + loadVerse(); init(); if (m_settings->value("WindowState").isNull()) @@ -38,21 +40,30 @@ MainWindow::MainWindow(QWidget* parent) void MainWindow::loadIcons() { - ui->actionKhatmah->setIcon(m_fa->icon(fa_solid, fa_list)); - ui->actionDownloadManager->setIcon(m_fa->icon(fa_solid, fa_download)); - ui->actionExit->setIcon(m_fa->icon(fa_solid, fa_xmark)); - ui->actionFind->setIcon(m_fa->icon(fa_solid, fa_magnifying_glass)); - ui->actionTafsir->setIcon(m_fa->icon(fa_solid, fa_book_open)); - ui->actionVOTD->setIcon(m_fa->icon(fa_solid, fa_calendar_day)); - ui->actionBookmarks->setIcon(m_fa->icon(fa_solid, fa_bookmark)); - ui->actionPereferences->setIcon(m_fa->icon(fa_solid, fa_gear)); - ui->actionAdvancedCopy->setIcon(m_fa->icon(fa_solid, fa_clipboard)); - ui->actionReaderViewToggle->setIcon(m_fa->icon(fa_solid, fa_columns)); - ui->actionUpdates->setIcon(m_fa->icon(fa_solid, fa_arrow_rotate_right)); -} - -void -MainWindow::loadCurrent() + ui->actionKhatmah->setIcon(StyleManager::awesome->icon(fa_solid, fa_list)); + ui->actionDownloadManager->setIcon( + StyleManager::awesome->icon(fa_solid, fa_download)); + ui->actionExit->setIcon(StyleManager::awesome->icon(fa_solid, fa_xmark)); + ui->actionFind->setIcon( + StyleManager::awesome->icon(fa_solid, fa_magnifying_glass)); + ui->actionTafsir->setIcon( + StyleManager::awesome->icon(fa_solid, fa_book_open)); + ui->actionVOTD->setIcon( + StyleManager::awesome->icon(fa_solid, fa_calendar_day)); + ui->actionBookmarks->setIcon( + StyleManager::awesome->icon(fa_solid, fa_bookmark)); + ui->actionPereferences->setIcon( + StyleManager::awesome->icon(fa_solid, fa_gear)); + ui->actionAdvancedCopy->setIcon( + StyleManager::awesome->icon(fa_solid, fa_clipboard)); + ui->actionReaderViewToggle->setIcon( + StyleManager::awesome->icon(fa_solid, fa_columns)); + ui->actionUpdates->setIcon( + StyleManager::awesome->icon(fa_solid, fa_arrow_rotate_right)); +} + +void +MainWindow::loadVerse() { int id = m_settings->value("Reader/Khatmah").toInt(); QList vInfo = m_currVerse->toList(); @@ -69,10 +80,12 @@ void MainWindow::checkForUpdates() { #if defined Q_OS_WIN - QFileInfo tool(m_updateToolPath); + static QString updateTool = QApplication::applicationDirPath() + + QDir::separator() + "QCMaintenanceTool.exe"; + QFileInfo tool(updateTool); if (tool.exists()) { m_process->setWorkingDirectory(QApplication::applicationDirPath()); - m_process->start(m_updateToolPath, QStringList("ch")); + m_process->start(updateTool, QStringList("ch")); return; } #endif @@ -83,6 +96,9 @@ MainWindow::checkForUpdates() void MainWindow::updateProcessCallback() { + static QString updateTool = QApplication::applicationDirPath() + + QDir::separator() + "QCMaintenanceTool.exe"; + QString output = m_process->readAll(); QString displayText; @@ -101,7 +117,7 @@ MainWindow::updateProcessCallback() QMessageBox::StandardButton btn = QMessageBox::question(this, tr("Updates info"), displayText); if (btn == QMessageBox::Yes) - m_process->startDetached(m_updateToolPath); + m_process->startDetached(updateTool); } else { @@ -116,74 +132,77 @@ MainWindow::updateProcessCallback() void MainWindow::setupShortcuts() { - connect(m_shortcutHandler, + m_shortcutHandler->createShortcuts(this); + + connect(m_shortcutHandler.data(), &ShortcutHandler::toggleMenubar, this, &MainWindow::toggleMenubar); - connect(m_shortcutHandler, + connect(m_shortcutHandler.data(), &ShortcutHandler::toggleNavDock, this, &MainWindow::toggleNavDock); - connect(m_shortcutHandler, + connect(m_shortcutHandler.data(), &ShortcutHandler::bookmarkCurrent, this, &MainWindow::addCurrentToBookmarks); - connect(m_shortcutHandler, + connect(m_shortcutHandler.data(), &ShortcutHandler::openDownloads, this, &MainWindow::actionDMTriggered); - connect(m_shortcutHandler, + connect(m_shortcutHandler.data(), &ShortcutHandler::openBookmarks, this, &MainWindow::actionBookmarksTriggered); - connect(m_shortcutHandler, + connect(m_shortcutHandler.data(), &ShortcutHandler::openKhatmah, this, &MainWindow::actionKhatmahTriggered); - connect(m_shortcutHandler, + connect(m_shortcutHandler.data(), &ShortcutHandler::openSearch, this, &MainWindow::actionSearchTriggered); - connect(m_shortcutHandler, + connect(m_shortcutHandler.data(), &ShortcutHandler::openSettings, this, &MainWindow::actionPrefTriggered); - connect(m_shortcutHandler, + connect(m_shortcutHandler.data(), &ShortcutHandler::openTafsir, this, &MainWindow::actionTafsirTriggered); - connect(m_shortcutHandler, + connect(m_shortcutHandler.data(), &ShortcutHandler::openAdvancedCopy, this, &MainWindow::actionAdvancedCopyTriggered); - connect(m_shortcutHandler, + connect(m_shortcutHandler.data(), &ShortcutHandler::nextVerse, this, &MainWindow::nextVerse); - connect(m_shortcutHandler, + connect(m_shortcutHandler.data(), &ShortcutHandler::prevVerse, this, &MainWindow::prevVerse); - connect(m_shortcutHandler, + connect(m_shortcutHandler.data(), &ShortcutHandler::nextSurah, this, &MainWindow::nextSurah); - connect(m_shortcutHandler, + connect(m_shortcutHandler.data(), &ShortcutHandler::prevSurah, this, &MainWindow::prevSurah); - connect( - m_shortcutHandler, &ShortcutHandler::nextJuz, this, &MainWindow::nextJuz); - connect( - m_shortcutHandler, &ShortcutHandler::prevJuz, this, &MainWindow::prevJuz); + connect(m_shortcutHandler.data(), + &ShortcutHandler::nextJuz, + this, + &MainWindow::nextJuz); + connect(m_shortcutHandler.data(), + &ShortcutHandler::prevJuz, + this, + &MainWindow::prevJuz); } void MainWindow::setupConnections() { - - /* ------------------ UI connectors ------------------ */ - // ########## Menubar ########## // connect(ui->actionExit, &QAction::triggered, this, &QApplication::exit); connect(ui->actionPereferences, @@ -226,14 +245,6 @@ MainWindow::setupConnections() &QAction::triggered, this, &MainWindow::actionAboutTriggered); - connect(ui->actionReaderViewToggle, - &QAction::triggered, - m_reader, - &QuranReader::toggleReaderView); - connect(m_verseDlg, - &VerseDialog::navigateToVerse, - m_reader, - &QuranReader::navigateToVerse); // ########## page controls ########## // connect(ui->cmbPage, @@ -313,16 +324,18 @@ MainWindow::setupConnections() &DownloadManager::latestVersionFound, m_popup, &NotificationPopup::checkUpdate); - connect(m_reader, - &QuranReader::notifyBookmarkAdded, + connect(m_cpyDlg, + &CopyDialog::verseCopied, + m_popup, + &NotificationPopup::copiedToClipboard); + connect(m_dbMgr.data(), + &DBManager::bookmarkAdded, m_popup, &NotificationPopup::bookmarkAdded); - connect(m_reader, - &QuranReader::notifyBookmarkRemoved, + connect(m_dbMgr.data(), + &DBManager::bookmarkRemoved, m_popup, &NotificationPopup::bookmarkRemoved); - connect( - m_reader, &QuranReader::copyVerseText, this, &MainWindow::copyVerseText); // ########## Settings Dialog ########## // // Restart signal @@ -373,7 +386,7 @@ MainWindow::setupConnections() // shortcut change connect(m_settingsDlg, &SettingsDialog::shortcutChanged, - m_shortcutHandler, + m_shortcutHandler.data(), &ShortcutHandler::shortcutChanged); connect(m_player, @@ -389,27 +402,40 @@ MainWindow::setupConnections() this, &MainWindow::mediaStatusChanged); + connect(ui->actionReaderViewToggle, + &QAction::triggered, + m_reader, + &QuranReader::toggleReaderView); + connect(m_verseDlg, + &VerseDialog::navigateToVerse, + m_reader, + &QuranReader::navigateToVerse); + connect(m_reader, + &QuranReader::copyVerseText, + m_cpyDlg, + &CopyDialog::copyVerseText); connect(m_reader, &QuranReader::showVerseTafsir, this, &MainWindow::showVerseTafsir); - connect(m_reader, &QuranReader::showBetaqa, this, [this](int surah) { - m_betaqaViewer->showSurah(surah); - }); + connect(m_reader, + &QuranReader::showBetaqa, + m_betaqaViewer, + &BetaqaViewer::showSurah); } void MainWindow::init() { m_player = new VersePlayer(this, m_settings->value("Reciter", 0).toInt()); - m_reader = new QuranReader(this, m_player, m_shortcutHandler); - m_playerControls = - new PlayerControls(this, m_player, m_reader, m_shortcutHandler); + m_reader = new QuranReader(this, m_player); + m_playerControls = new PlayerControls(this, m_player, m_reader); + m_settingsDlg = new SettingsDialog(this, m_player); m_popup = new NotificationPopup(this); m_betaqaViewer = new BetaqaViewer(this); m_verseDlg = new VerseDialog(this); m_downManPtr = new DownloadManager(this); - m_settingsDlg = new SettingsDialog(this, m_player); + m_cpyDlg = new CopyDialog(this); QHBoxLayout* controls = new QHBoxLayout(); QFrame* controlsFrame = new QFrame(this); @@ -474,7 +500,7 @@ MainWindow::setupMenubarToggle() .arg(QString::number(ui->menubar->height()), QString::number(ui->menubar->height() * 2))); ui->menubar->setCornerWidget(toggleNav); - toggleNav->setIcon(m_fa->icon(fa_solid, fa_compass)); + toggleNav->setIcon(StyleManager::awesome->icon(fa_solid, fa_compass)); toggleNav->setIconSize(QSize(20, 20)); connect(toggleNav, &QPushButton::toggled, this, [this](bool checked) { @@ -658,55 +684,54 @@ void MainWindow::addCurrentToBookmarks() { QList vInfo = m_currVerse->toList(); - if (!m_dbMgr->isBookmarked(vInfo)) { + if (!m_dbMgr->isBookmarked(vInfo)) m_dbMgr->addBookmark(vInfo); - m_popup->bookmarkAdded(); - } } void -MainWindow::missingRecitationFileWarn(int reciterIdx, int surah) +MainWindow::missingQCF() { - if (!m_settings->value("MissingFileWarning").toBool()) - return; - - QMessageBox::StandardButton btn = - QMessageBox::question(this, - tr("Recitation not found"), - tr("The recitation files for the current surah is " - "missing, would you like to download it?")); + QMessageBox::StandardButton btn = QMessageBox::question( + this, + tr("Files Missing"), + tr("The selected font files are missing, would you like to download it?")); if (btn == QMessageBox::Yes) { actionDMTriggered(); - m_downloaderDlg->selectDownload(Recitation, { reciterIdx, surah }); + m_downloaderDlg->selectDownload(DownloadManager::QCF); } } void -MainWindow::missingQCF() +MainWindow::missingTafsir(int idx) { QMessageBox::StandardButton btn = QMessageBox::question( this, tr("Files Missing"), - tr("The selected font files are missing, would you like to download it?")); + tr("The selected tafsir is missing, would you like to download it?")); if (btn == QMessageBox::Yes) { actionDMTriggered(); - m_downloaderDlg->selectDownload(QCF); + m_downloaderDlg->selectDownload(DownloadManager::File, { 0, idx }); } } void -MainWindow::missingTafsir(int idx) +MainWindow::missingRecitationFileWarn(int reciterIdx, int surah) { - QMessageBox::StandardButton btn = QMessageBox::question( - this, - tr("Files Missing"), - tr("The selected tafsir is missing, would you like to download it?")); + if (!m_settings->value("MissingFileWarning").toBool()) + return; + + QMessageBox::StandardButton btn = + QMessageBox::question(this, + tr("Recitation not found"), + tr("The recitation files for the current surah is " + "missing, would you like to download it?")); if (btn == QMessageBox::Yes) { actionDMTriggered(); - m_downloaderDlg->selectDownload(File, { 0, idx }); + m_downloaderDlg->selectDownload(DownloadManager::Recitation, + { reciterIdx, surah }); } } @@ -757,8 +782,6 @@ MainWindow::actionKhatmahTriggered() void MainWindow::actionAdvancedCopyTriggered() { - if (m_cpyDlg == nullptr) - m_cpyDlg = new CopyDialog(this); m_cpyDlg->show(); } @@ -885,10 +908,10 @@ MainWindow::showVerseTafsir(Verse v) reload = false; } - if (!Globals::tafsirExists(m_dbMgr->currTafsir())) { + if (!Tafsir::tafsirExists(m_dbMgr->currTafsir())) { int i; - for (i = 0; i < m_tafasirList.size(); i++) - if (m_dbMgr->currTafsir() == &m_tafasirList[i]) + for (i = 0; i < m_tafasir.size(); i++) + if (m_dbMgr->currTafsir() == m_tafasir[i]) break; reload = true; return missingTafsir(i); @@ -903,21 +926,6 @@ MainWindow::showVerseTafsir(Verse v) m_tafsirDlg->show(); } -void -MainWindow::copyVerseText(const Verse v) -{ - QClipboard* clip = QApplication::clipboard(); - QString text = m_dbMgr->getVerseText(v.surah(), v.number()); - QString vNum = QString::number(v.number()); - text.remove(text.size() - 1, 1); - text = text.trimmed(); - text = "{" + text + "}"; - text += ' '; - text += "[" + m_dbMgr->surahNameList().at(v.surah() - 1) + ":" + vNum + "]"; - clip->setText(text); - m_popup->copiedToClipboard(); -} - void MainWindow::resizeEvent(QResizeEvent* event) { diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 7e34328b..2925ae93 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -6,11 +6,10 @@ #ifndef MAINWINDOW_H #define MAINWINDOW_H -#include "../globals.h" +#include "../types/verse.h" #include "../utils/dbmanager.h" #include "../utils/shortcuthandler.h" #include "../utils/systemtray.h" -#include "../utils/verse.h" #include "../utils/verseplayer.h" #include "../widgets/betaqaviewer.h" #include "../widgets/notificationpopup.h" @@ -221,11 +220,6 @@ private slots: * @param v - ::Verse to show the tafsir of */ void showVerseTafsir(Verse v); - /** - * @brief copy to clipboard the text of the verse with the given index - * @param IdxInPage - verse index relative to the start of the page - */ - void copyVerseText(const Verse v); /** * @brief search for the surahs with the given argument when the text in the * side dock search box is changed @@ -248,14 +242,12 @@ private slots: void toggleNavDock(); private: - Verse* m_currVerse = Verse::current(); - QSettings* const m_settings = Globals::settings; - const QList& m_recitersList = Globals::recitersList; - const QList& m_tafasirList = Globals::tafasirList; - const QString& m_updateToolPath = Globals::updateToolPath; - DBManager* m_dbMgr = DBManager::instance(); - fa::QtAwesome* m_fa = Globals::awesome; Ui::MainWindow* ui; + Verse* m_currVerse = Verse::current(); + QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_settings = Settings::settings; + const QList>& m_reciters = Reciter::reciters; + const QList>& m_tafasir = Tafsir::tafasir; /** * @brief initalizes different parts used by the app */ @@ -267,7 +259,7 @@ private slots: /** * @brief set the current ::Verse from settings */ - void loadCurrent(); + void loadVerse(); /** * @brief connect ShortcutHandler signals to their corresponding slots */ @@ -333,7 +325,7 @@ private slots: /** * @brief ShortcutHandler instance for handling shortcuts */ - ShortcutHandler* m_shortcutHandler = nullptr; + QSharedPointer m_shortcutHandler = nullptr; /** * @brief pointer to SystemTray instance */ diff --git a/src/core/playercontrols.cpp b/src/core/playercontrols.cpp index 78f8530e..71b5c99c 100644 --- a/src/core/playercontrols.cpp +++ b/src/core/playercontrols.cpp @@ -1,11 +1,13 @@ #include "playercontrols.h" +#include "../utils/shortcuthandler.h" +#include "../utils/stylemanager.h" #include "ui_playercontrols.h" +#include using namespace fa; PlayerControls::PlayerControls(QWidget* parent, VersePlayer* player, - QuranReader* reader, - ShortcutHandler* handler) + QuranReader* reader) : QWidget(parent) , ui(new Ui::PlayerControls) , m_player(player) @@ -13,10 +15,10 @@ PlayerControls::PlayerControls(QWidget* parent, { ui->setupUi(this); loadIcons(); - setupShortcuts(handler); + setupConnections(); - foreach (const Reciter& r, m_recitersList) - ui->cmbReciter->addItem(r.displayName); + foreach (const QSharedPointer& r, m_reciters) + ui->cmbReciter->addItem(r->displayName()); ui->cmbReciter->setCurrentIndex(m_settings->value("Reciter", 0).toInt()); } @@ -24,31 +26,31 @@ PlayerControls::PlayerControls(QWidget* parent, void PlayerControls::loadIcons() { - ui->btnPlay->setIcon(m_fa->icon(fa_solid, fa_play)); - ui->btnPause->setIcon(m_fa->icon(fa_solid, fa_pause)); - ui->btnStop->setIcon(m_fa->icon(fa_solid, fa_stop)); + ui->btnPlay->setIcon(StyleManager::awesome->icon(fa_solid, fa_play)); + ui->btnPause->setIcon(StyleManager::awesome->icon(fa_solid, fa_pause)); + ui->btnStop->setIcon(StyleManager::awesome->icon(fa_solid, fa_stop)); ui->lbSpeaker->setText(QString(fa_volume_high)); - ui->lbSpeaker->setFont(m_fa->font(fa_solid, 16)); + ui->lbSpeaker->setFont(StyleManager::awesome->font(fa_solid, 16)); } void -PlayerControls::setupShortcuts(ShortcutHandler* handler) +PlayerControls::setupConnections() { - - connect(handler, + QSharedPointer handler = ShortcutHandler::current(); + connect(handler.data(), &ShortcutHandler::togglePlayerControls, this, &PlayerControls::toggleVisibility); - connect(handler, + connect(handler.data(), &ShortcutHandler::togglePlayback, this, &PlayerControls::togglePlayback); - connect(handler, + connect(handler.data(), &ShortcutHandler::incrementVolume, this, &PlayerControls::incrementVolume); - connect(handler, + connect(handler.data(), &ShortcutHandler::decrementVolume, this, &PlayerControls::decrementVolume); diff --git a/src/core/playercontrols.h b/src/core/playercontrols.h index 20da3bb2..9f9271ce 100644 --- a/src/core/playercontrols.h +++ b/src/core/playercontrols.h @@ -1,9 +1,7 @@ #ifndef PLAYERCONTROLS_H #define PLAYERCONTROLS_H -#include "../globals.h" -#include "../utils/shortcuthandler.h" -#include "../utils/verse.h" +#include "../types/verse.h" #include "../utils/verseplayer.h" #include "quranreader.h" #include @@ -19,8 +17,7 @@ class PlayerControls : public QWidget public: explicit PlayerControls(QWidget* parent, VersePlayer* player, - QuranReader* reader, - ShortcutHandler* handler); + QuranReader* reader); ~PlayerControls(); int currentReciter() const; @@ -80,12 +77,11 @@ private slots: void decrementVolume(); private: - Verse* m_currVerse = Verse::current(); - QSettings* const m_settings = Globals::settings; - const QList& m_recitersList = Globals::recitersList; - DBManager* m_dbMgr = DBManager::instance(); - fa::QtAwesome* m_fa = Globals::awesome; Ui::PlayerControls* ui; + Verse* m_currVerse = Verse::current(); + QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer const m_settings = Settings::settings; + const QList>& m_reciters = Reciter::reciters; /** * @brief load icons for different UI elements */ @@ -93,7 +89,7 @@ private slots: /** * @brief connect ShortcutHandler signals to their corresponding slots */ - void setupShortcuts(ShortcutHandler* handler); + void setupConnections(); /** * @brief float value of the current playback volume (0 - 1.0) */ diff --git a/src/core/quranreader.cpp b/src/core/quranreader.cpp index f17246db..27bbd849 100644 --- a/src/core/quranreader.cpp +++ b/src/core/quranreader.cpp @@ -1,11 +1,13 @@ #include "quranreader.h" +#include "../utils/fontmanager.h" +#include "../utils/shortcuthandler.h" +#include "../utils/stylemanager.h" #include "../widgets/clickablelabel.h" #include "ui_quranreader.h" +#include using namespace fa; -QuranReader::QuranReader(QWidget* parent, - VersePlayer* player, - ShortcutHandler* handler) +QuranReader::QuranReader(QWidget* parent, VersePlayer* player) : QWidget(parent) , ui(new Ui::QuranReader) , m_player(player) @@ -24,14 +26,13 @@ QuranReader::QuranReader(QWidget* parent, addSideContent(); setupConnections(); - setupShortcuts(handler); } void QuranReader::loadIcons() { - ui->btnNext->setIcon(m_fa->icon(fa_solid, fa_arrow_left)); - ui->btnPrev->setIcon(m_fa->icon(fa_solid, fa_arrow_right)); + ui->btnNext->setIcon(StyleManager::awesome->icon(fa_solid, fa_arrow_left)); + ui->btnPrev->setIcon(StyleManager::awesome->icon(fa_solid, fa_arrow_right)); } void @@ -108,28 +109,28 @@ QuranReader::setupConnections() &QuranReader::currentVerseChanged, m_player, &VersePlayer::loadActiveVerse); -} -void -QuranReader::setupShortcuts(ShortcutHandler* handler) -{ - connect(handler, + QSharedPointer handler = ShortcutHandler::current(); + connect(handler.data(), + &ShortcutHandler::nextPage, + this, + &QuranReader::btnNextClicked); + connect(handler.data(), + &ShortcutHandler::prevPage, + this, + &QuranReader::btnPrevClicked); + connect(handler.data(), &ShortcutHandler::toggleReaderView, this, &QuranReader::toggleReaderView); - connect( - handler, &ShortcutHandler::nextPage, this, &QuranReader::btnNextClicked); - connect( - handler, &ShortcutHandler::prevPage, this, &QuranReader::btnPrevClicked); - for (int i = 0; i <= 1; i++) { if (m_quranBrowsers[i]) { - connect(handler, + connect(handler.data(), &ShortcutHandler::zoomIn, m_quranBrowsers[i], &QuranPageBrowser::actionZoomIn); - connect(handler, + connect(handler.data(), &ShortcutHandler::zoomOut, m_quranBrowsers[i], &QuranPageBrowser::actionZoomOut); @@ -175,7 +176,7 @@ QuranReader::updateVerseType() { VerseType type = qvariant_cast(m_settings->value("Reader/VerseType")); - m_versesFont.setFamily(Globals::verseFontname(type, m_currVerse->page())); + m_versesFont.setFamily(FontManager::verseFontname(type, m_currVerse->page())); m_versesFont.setPointSize(m_settings->value("Reader/VerseFontSize").toInt()); m_dbMgr->setVerseType(type); } @@ -203,7 +204,7 @@ QuranReader::redrawQuranPage(bool manualSz) { if (m_activeQuranBrowser == m_quranBrowsers[0]) { m_quranBrowsers[0]->constructPage(m_currVerse->page(), manualSz); - if (m_readerMode == DoublePage && m_quranBrowsers[1]) + if (m_readerMode == Settings::DoublePage && m_quranBrowsers[1]) m_quranBrowsers[1]->constructPage(m_currVerse->page() + 1, manualSz); } else { m_quranBrowsers[0]->constructPage(m_currVerse->page() - 1, manualSz); @@ -229,8 +230,8 @@ QuranReader::addSideContent() QLabel* contentLb; VerseFrame* verseContFrame; QString prevLbContent, currLbContent; - if (m_dbMgr->getVerseType() == qcf) - m_versesFont.setFamily(Globals::pageFontname(m_currVerse->page())); + if (m_dbMgr->getVerseType() == Settings::qcf) + m_versesFont.setFamily(FontManager::pageFontname(m_currVerse->page())); for (int i = m_activeVList->size() - 1; i >= 0; i--) { const Verse* vInfo = &(m_activeVList->at(i)); @@ -326,7 +327,7 @@ QuranReader::updatePageVerseInfoList() if (m_activeQuranBrowser == m_quranBrowsers[0]) { m_vLists[0] = Verse::fromList(m_dbMgr->getVerseInfoList(m_currVerse->page())); - if (m_readerMode == DoublePage) + if (m_readerMode == Settings::DoublePage) m_vLists[1] = Verse::fromList(m_dbMgr->getVerseInfoList(m_currVerse->page() + 1)); @@ -442,12 +443,10 @@ QuranReader::verseAnchorClicked(const QUrl& hrefUrl) break; case QuranPageBrowser::addBookmark: m_dbMgr->addBookmark(v.toList()); - emit notifyBookmarkAdded(); break; case QuranPageBrowser::removeBookmark: if (m_dbMgr->removeBookmark(v.toList())) - emit notifyBookmarkRemoved(); - break; + break; default: break; } diff --git a/src/core/quranreader.h b/src/core/quranreader.h index dd6317c4..571d8de6 100644 --- a/src/core/quranreader.h +++ b/src/core/quranreader.h @@ -1,15 +1,14 @@ #ifndef QURANREADER_H #define QURANREADER_H -#include "../globals.h" -#include "../utils/shortcuthandler.h" -#include "../utils/verse.h" +#include "../types/verse.h" #include "../utils/verseplayer.h" #include "../widgets/quranpagebrowser.h" #include "../widgets/verseframe.h" #include #include #include +typedef Settings::ReaderMode ReaderMode; namespace Ui { class QuranReader; @@ -20,13 +19,10 @@ class QuranReader : public QWidget Q_OBJECT public: - explicit QuranReader(QWidget* parent, - VersePlayer* player, - ShortcutHandler* handler); + explicit QuranReader(QWidget* parent, VersePlayer* player); ~QuranReader(); public slots: - void updatePageFontSize(); /** * @brief highlight the currently active ::Verse m_currVerse in the * active QuranPageBrowser and the side panel depending on the ::ReaderMode @@ -100,15 +96,19 @@ public slots: * @brief sets m_currVerse to the first verse in m_vInfoList */ void setVerseToStartOfPage(); + /** + * @brief updatePageFontSize + * + * MODIFIED + */ + void updatePageFontSize(); signals: + void currentVerseChanged(); + void currentSurahChanged(); void showBetaqa(int surah); void showVerseTafsir(const Verse& v); void copyVerseText(const Verse& v); - void currentVerseChanged(); - void currentSurahChanged(); - void notifyBookmarkAdded(); - void notifyBookmarkRemoved(); private slots: /** @@ -143,17 +143,14 @@ private slots: void gotoDoublePage(int page); private: - Verse* m_currVerse = Verse::current(); - QSettings* const m_settings = Globals::settings; - const QList& m_recitersList = Globals::recitersList; - const QList& m_tafasirList = Globals::tafasirList; - const QString& m_updateToolPath = Globals::updateToolPath; - const ReaderMode& m_readerMode = Globals::readerMode; - DBManager* m_dbMgr = DBManager::instance(); - fa::QtAwesome* m_fa = Globals::awesome; Ui::QuranReader* ui; + Verse* m_currVerse = Verse::current(); + QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_settings = Settings::settings; + const QList>& m_recitersList = Reciter::reciters; + const QList>& m_tafasirList = Tafsir::tafasir; + const ReaderMode& m_readerMode = Settings::readerMode; void setupConnections(); - void setupShortcuts(ShortcutHandler* handler); /** * @brief load icons for different UI elements */ diff --git a/src/core/searchdialog.cpp b/src/core/searchdialog.cpp index 0251cf27..215a26f6 100644 --- a/src/core/searchdialog.cpp +++ b/src/core/searchdialog.cpp @@ -4,6 +4,8 @@ */ #include "searchdialog.h" +#include "../utils/fontmanager.h" +#include "../utils/stylemanager.h" #include "../widgets/clickablelabel.h" #include "ui_searchdialog.h" @@ -12,17 +14,19 @@ SearchDialog::SearchDialog(QWidget* parent) , ui(new Ui::SearchDialog) , m_surahNames{ m_dbMgr->surahNameList() } { - setWindowIcon(Globals::awesome->icon(fa::fa_solid, fa::fa_magnifying_glass)); + setWindowIcon( + StyleManager::awesome->icon(fa::fa_solid, fa::fa_magnifying_glass)); ui->setupUi(this); ui->frmNavBtns->setLayoutDirection(Qt::LeftToRight); - ui->btnNext->setIcon(Globals::awesome->icon(fa::fa_solid, fa::fa_arrow_left)); + ui->btnNext->setIcon( + StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_left)); ui->btnPrev->setIcon( - Globals::awesome->icon(fa::fa_solid, fa::fa_arrow_right)); + StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_right)); ui->btnNext->setDisabled(true); ui->btnPrev->setDisabled(true); ui->btnTransfer->setIcon( - Globals::awesome->icon(fa::fa_solid, fa::fa_arrow_right_arrow_left)); + StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_right_arrow_left)); ui->listViewAllSurahs->setModel(&m_modelAllSurahs); ui->listViewSelected->setModel(&m_modelSelectedSurahs); if (m_lang == QLocale::Arabic) @@ -109,7 +113,8 @@ SearchDialog::showResults() for (int i = m_startResult; i < endIdx; i++) { Verse v = m_currResults.at(i); - QString fontName = Globals::verseFontname(m_dbMgr->getVerseType(), v.page()); + QString fontName = + FontManager::verseFontname(m_dbMgr->getVerseType(), v.page()); VerseFrame* vFrame = new VerseFrame(ui->srclResults); QLabel* lbInfo = new QLabel(vFrame); diff --git a/src/core/searchdialog.h b/src/core/searchdialog.h index 738cebbc..22ca1e78 100644 --- a/src/core/searchdialog.h +++ b/src/core/searchdialog.h @@ -6,9 +6,8 @@ #ifndef SEARCHDIALOG_H #define SEARCHDIALOG_H -#include "../globals.h" +#include "../types/verse.h" #include "../utils/dbmanager.h" -#include "../utils/verse.h" #include "../widgets/verseframe.h" #include #include @@ -96,8 +95,8 @@ private slots: void btnTransferClicked(); private: - const QLocale::Language m_lang = Globals::language; - DBManager* m_dbMgr = DBManager::instance(); + const QLocale::Language m_lang = Settings::language; + QSharedPointer m_dbMgr = DBManager::current(); /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/core/settingsdialog.cpp b/src/core/settingsdialog.cpp index 2a521653..7094c1ca 100644 --- a/src/core/settingsdialog.cpp +++ b/src/core/settingsdialog.cpp @@ -4,6 +4,7 @@ */ #include "settingsdialog.h" +#include "../utils/stylemanager.h" #include "../widgets/shortcutdelegate.h" #include "ui_settingsdialog.h" @@ -15,7 +16,7 @@ SettingsDialog::SettingsDialog(QWidget* parent, VersePlayer* vPlayerPtr) ui->setupUi(this); ui->cmbQuranFontSz->setValidator(new QIntValidator(10, 72)); ui->cmbSideFontSz->setValidator(new QIntValidator(10, 72)); - setWindowIcon(Globals::awesome->icon(fa::fa_solid, fa::fa_gear)); + setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_gear)); ui->tableViewShortcuts->setModel(&m_shortcutsModel); ui->tableViewShortcuts->horizontalHeader()->setStretchLastSection(true); ui->tableViewShortcuts->setItemDelegate(new ShortcutDelegate); @@ -69,15 +70,15 @@ SettingsDialog::updateContentCombobox() { ui->cmbTafsir->clear(); for (int i = 0; i < m_tafasirList.size(); i++) { - const Tafsir& t = m_tafasirList[i]; - if (Globals::tafsirExists(&t)) - ui->cmbTafsir->addItem(t.displayName, i); + const QSharedPointer& t = m_tafasirList[i]; + if (Tafsir::tafsirExists(t)) + ui->cmbTafsir->addItem(t->displayName(), i); } ui->cmbTranslation->clear(); for (int i = 0; i < m_trList.size(); i++) { - const Translation& tr = m_trList[i]; - if (Globals::translationExists(&tr)) - ui->cmbTranslation->addItem(tr.displayName, i); + const QSharedPointer& tr = m_trList[i]; + if (Translation::translationExists(tr)) + ui->cmbTranslation->addItem(tr->displayName(), i); } m_tafsir = ui->cmbTafsir->findData(m_settings->value("Reader/Tafsir")); @@ -294,7 +295,7 @@ SettingsDialog::updateSideFontSize(QString size) } void -SettingsDialog::updateVerseText(int vt) +SettingsDialog::updateVerseType(int vt) { m_settings->setValue("Reader/VerseType", vt); emit verseTypeChanged(); @@ -302,7 +303,7 @@ SettingsDialog::updateVerseText(int vt) } void -SettingsDialog::updateVerseTextFontsize(QString size) +SettingsDialog::updateVerseFontsize(QString size) { m_settings->setValue("Reader/VerseFontSize", size); emit verseTypeChanged(); @@ -347,10 +348,10 @@ SettingsDialog::applyAllChanges() updateQuranFont(ui->cmbQCF->currentIndex() + 1); if (ui->cmbVerseText->currentIndex() != m_verseType) - updateVerseText(ui->cmbVerseText->currentIndex()); + updateVerseType(ui->cmbVerseText->currentIndex()); if (ui->cmbVersesFontSz->currentText() != QString::number(m_verseFontSize)) - updateVerseTextFontsize(ui->cmbVersesFontSz->currentText()); + updateVerseFontsize(ui->cmbVersesFontSz->currentText()); if (ui->chkAdaptive->isChecked() != m_adaptive) updateAdaptiveFont(ui->chkAdaptive->isChecked()); diff --git a/src/core/settingsdialog.h b/src/core/settingsdialog.h index 5935bab1..ee381147 100644 --- a/src/core/settingsdialog.h +++ b/src/core/settingsdialog.h @@ -6,6 +6,8 @@ #ifndef SETTINGSDIALOG_H #define SETTINGSDIALOG_H +#include "../utils/settings.h" +#include "../utils/shortcuthandler.h" #include "../utils/verseplayer.h" #include #include @@ -21,6 +23,7 @@ #include #include #include +typedef Settings::ReaderMode ReaderMode; namespace Ui { class SettingsDialog; @@ -120,8 +123,8 @@ public slots: * @param size - QString representing the new font size */ void updateSideFontSize(QString size); - void updateVerseText(int vt); - void updateVerseTextFontsize(QString size); + void updateVerseType(int vt); + void updateVerseFontsize(QString size); /** * @brief update the key sequence that trigger the shortcut with the given key * @param key - QString of the shortcut name in the settings file @@ -218,16 +221,17 @@ public slots: void closeEvent(QCloseEvent* event); private: - const int m_qcfVer = Globals::qcfVersion; - const int m_themeIdx = Globals::themeId; - const ReaderMode m_readerMode = Globals::readerMode; - const QLocale::Language m_languageCode = Globals::language; - QSettings* const m_settings = Globals::settings; - const QDir& m_downloadsDir = Globals::downloadsDir; - const QList& m_tafasirList = Globals::tafasirList; - const QList& m_trList = Globals::translationsList; + const int m_qcfVer = Settings::qcfVersion; + const int m_themeIdx = Settings::themeId; + const ReaderMode m_readerMode = Settings::readerMode; + const QLocale::Language m_languageCode = Settings::language; + QSharedPointer const m_settings = Settings::settings; + const QDir& m_downloadsDir = *DirManager::downloadsDir; + const QList>& m_tafasirList = Tafsir::tafasir; + const QList>& m_trList = + Translation::translations; const QMap& m_shortcutDescription = - Globals::shortcutDescription; + ShortcutHandler::shortcutsDescription; /** * @brief connects signals and slots for different UI * components and shortcuts. @@ -274,11 +278,15 @@ public slots: /** * @brief DBManager::Tafsir enum value mapped to the tafsir index in the * combobox. + * + * MODIFIED */ int m_tafsir; /** * @brief DBManager::Translation enum value mapped to the translation index in * the combobox. + * + * MODIFIED */ int m_translation; int m_verseType; diff --git a/src/core/tafsirdialog.cpp b/src/core/tafsirdialog.cpp index ee4ca022..17308d04 100644 --- a/src/core/tafsirdialog.cpp +++ b/src/core/tafsirdialog.cpp @@ -4,23 +4,24 @@ */ #include "tafsirdialog.h" +#include "../types/tafsir.h" +#include "../utils/fontmanager.h" +#include "../utils/stylemanager.h" #include "ui_tafsirdialog.h" TafsirDialog::TafsirDialog(QWidget* parent) : QDialog(parent) , ui(new Ui::TafsirDialog) { - setWindowIcon(Globals::awesome->icon(fa::fa_solid, fa::fa_book_open)); + setWindowTitle(qApp->translate("SettingsDialog", "Tafsir")); + setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_book_open)); ui->setupUi(this); - ui->btnNext->setIcon(Globals::awesome->icon(fa::fa_solid, fa::fa_arrow_left)); + ui->frmNav->setLayoutDirection(Qt::LeftToRight); + ui->btnNext->setIcon( + StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_left)); ui->btnPrev->setIcon( - Globals::awesome->icon(fa::fa_solid, fa::fa_arrow_right)); + StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_right)); - foreach (const Tafsir& t, Globals::tafasirList) { - ui->cmbTafsir->addItem(t.displayName); - } - setTafsirAsTitle(); - ui->frmNav->setLayoutDirection(Qt::LeftToRight); if (m_qcfVer == 1) m_fontSZ = 18; else @@ -56,10 +57,17 @@ TafsirDialog::setupConnections() } void -TafsirDialog::setTafsirAsTitle() +TafsirDialog::updateContentComboBox() { - QString title = m_dbMgr->currTafsir()->displayName; - setWindowTitle(title); + ui->cmbTafsir->clear(); + for (int i = 0; i < m_tafasirList.size(); i++) { + const QSharedPointer t = m_tafasirList.at(i); + if (Tafsir::tafsirExists(t)) + ui->cmbTafsir->addItem(t->displayName(), i); + } + + m_tafsir = ui->cmbTafsir->findData(m_settings->value("Reader/Tafsir")); + ui->cmbTafsir->setCurrentIndex(m_tafsir); } void @@ -74,7 +82,7 @@ TafsirDialog::loadVerseTafsir() QString glyphs = m_dbMgr->getVerseGlyphs(m_shownVerse.surah(), m_shownVerse.number()); QString fontFamily = - Globals::verseFontname(m_dbMgr->getVerseType(), m_shownVerse.page()); + FontManager::verseFontname(m_dbMgr->getVerseType(), m_shownVerse.page()); ui->lbVerseInfo->setText(title); ui->lbVerseText->setWordWrap(true); @@ -85,7 +93,7 @@ TafsirDialog::loadVerseTafsir() qvariant_cast(m_settings->value("Reader/SideContentFont")); ui->tedTafsir->setFont(sideFont); - if (m_dbMgr->currTafsir()->text) + if (m_dbMgr->currTafsir()->isText()) ui->tedTafsir->setText( m_dbMgr->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); else @@ -117,7 +125,7 @@ TafsirDialog::closeEvent(QCloseEvent* event) void TafsirDialog::showEvent(QShowEvent* event) { - setTafsirAsTitle(); + updateContentComboBox(); QDialog::showEvent(event); } diff --git a/src/core/tafsirdialog.h b/src/core/tafsirdialog.h index 132e9d78..e357a7ee 100644 --- a/src/core/tafsirdialog.h +++ b/src/core/tafsirdialog.h @@ -6,9 +6,8 @@ #ifndef TAFSIRDIALOG_H #define TAFSIRDIALOG_H -#include "../globals.h" +#include "../types/verse.h" #include "../utils/dbmanager.h" -#include "../utils/verse.h" #include #include #include @@ -22,7 +21,7 @@ class TafsirDialog; * @details Tafsir is shown for a single verse at a time. Navigation between * verses is independant of the main Quran reader navigation for easier * navigation. Tafsir is displayed using the side content font set in the - * Globals::settings. + * Settings::settings. */ class TafsirDialog : public QDialog { @@ -72,22 +71,26 @@ private slots: void btnPrevClicked(); private: - const int m_qcfVer = Globals::qcfVersion; - const QSettings* m_settings = Globals::settings; - DBManager* m_dbMgr = DBManager::instance(); + Ui::TafsirDialog* ui; + QSharedPointer m_dbMgr = DBManager::current(); + const int m_qcfVer = Settings::qcfVersion; + const QSharedPointer m_settings = Settings::settings; + const QList>& m_tafasirList = Tafsir::tafasir; + const QList>& m_trList = + Translation::translations; /** * @brief connects signals and slots for different UI * components and shortcuts. */ void setupConnections(); + void updateContentComboBox(); /** - * @brief sets the Tafsir dialog title to match the displayed tafsir name. - */ - void setTafsirAsTitle(); - /** - * @brief Pointer to access ui elements generated from .ui files. + * @brief Tafsir enum value mapped to the tafsir index in the + * combobox. + * + * MODIFIED */ - Ui::TafsirDialog* ui; + int m_tafsir; /** * @brief fixed font size for the verse text displayed above the tafsir. */ diff --git a/src/globals.cpp b/src/globals.cpp deleted file mode 100644 index 2db93aa6..00000000 --- a/src/globals.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @file globals.cpp - * @brief Instantiation of application-wide variables. - */ - -#include "globals.h" -#include - -namespace Globals { -// app settings -int themeId = 0; -ReaderMode readerMode = ReaderMode::SinglePage; -bool darkMode = false; -QSettings* settings = nullptr; -QLocale::Language language; - -// qcf fonts -int qcfVersion = 1; -QString qcfFontPrefix = "QCF_P"; - -// app directories -QDir themeResources; -QDir assetsDir; -QDir bismillahDir; -QDir downloadsDir = - QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); -QDir configDir = - QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); - -// application specific -QList recitersList; -QList tafasirList; -QList translationsList; -QString updateToolPath; -QMap shortcutDescription; -fa::QtAwesome* awesome; - -QString -pageFontname(int page) -{ - return qcfFontPrefix + QString::number(page).rightJustified(3, '0'); -} - -QString -verseFontname(VerseType type, int page) -{ - QString fontname; - switch (type) { - case qcf: - fontname = pageFontname(page); - break; - case uthmanic: - fontname = "kfgqpc_hafs_uthmanic _script"; - break; - case annotated: - fontname = "Emine"; - break; - } - return fontname; -} - -bool -tafsirExists(const Tafsir* tafsir) -{ - const QDir& baseDir = tafsir->extra ? downloadsDir : assetsDir; - return baseDir.exists("tafasir/" + tafsir->filename); -} - -bool -translationExists(const Translation* tr) -{ - const QDir& baseDir = tr->extra ? downloadsDir : assetsDir; - return baseDir.exists("translations/" + tr->filename); -} -}; diff --git a/src/globals.h b/src/globals.h deleted file mode 100644 index 9fa2072a..00000000 --- a/src/globals.h +++ /dev/null @@ -1,145 +0,0 @@ -/** - * @file globals.h - * @brief Header file defining application-wide variables. - */ - -#ifndef GLOBALS_H -#define GLOBALS_H - -#include -#include -#include -#include -#include -#include -#include - -/** - * @struct Reciter - * @brief Reciter struct represents a quran reciter - */ -struct Reciter -{ - QString baseDirName{}; ///< The name of the directory conatining recitations - ///< in the application recitations directory. - QString displayName{}; ///< The reciter name as its displayed in the UI. - QString - basmallahPath{}; ///< Absolute path to the reciters bismillah audio file. - QString baseUrl{}; ///< Url to download recitation files from. - bool useId{ - false - }; ///< Boolean value representing whether the verse recitations should be - ///< downloading using the verse number relative to the beginning of the - ///< Quran or a combination of surah and verse numbers. -}; -/** - * @brief Tafsir struct contains data about a single tafsir - * @details tafasir are stored in the resource file "files.xml" - */ -struct Tafsir -{ - QString displayName; - QString filename; - bool text; - bool extra; -}; -/** - * @brief Translation struct holds different values representing available Quran - * translations - */ -struct Translation -{ - QString displayName; - QString filename; - bool extra; -}; - -enum DownloadType -{ - QCF, - Recitation, - File -}; - -enum VerseType -{ - qcf, - uthmanic, - annotated -}; - -/** - * @brief ReaderMode enum represents the available modes for the Quran reader in - * MainWindow - */ -enum ReaderMode -{ - SinglePage, ///< Single Quran page, side panel is used for displaying verses - ///< with translation - DoublePage ///< Two Quran pages, both panels are used to display Quran pages, - ///< no translation -}; - -namespace Globals { -extern int - themeId; ///< global variable represnting the application theme index. - -extern QSettings* - settings; ///< global pointer to the application QSettings instance. - -extern bool - darkMode; ///< global boolean to indicate if application is in dark mode. - -extern QLocale::Language - language; ///< global QLocale::Language instance for application languge. - -extern QString - updateToolPath; ///< global absolute path for the application update tool. - -extern int qcfVersion; ///< global variable for the QCF version in use. - -extern ReaderMode readerMode; - -extern QString qcfFontPrefix; ///< global variable for the QCF font prefix to - ///< generate font name from. - -extern QDir themeResources; ///< global QDir for the current theme resources - ///< (icons & styles). -extern QDir - configDir; ///< global QDir representing application config directiory. - -extern QDir assetsDir; ///< global QDir representing the application assets - ///< directory (fonts, translations, tafsir). - -extern QDir - bismillahDir; ///< global QDir representing the reciters basmallah files. - -extern QDir downloadsDir; ///< global QDir representing the top-level path - ///< for downloaded files. - -extern QList recitersList; ///< global QList containing reciters - ///< supported by the application. - -extern QList tafasirList; - -extern QList translationsList; - -extern QMap - shortcutDescription; ///< global QMap containing all available - ///< application shortcuts as keys and their descriptions - ///< as values. - -extern fa::QtAwesome* - awesome; ///< global pointer used for generating font awesome icons - -extern QString -pageFontname(int page); -extern QString -verseFontname(VerseType type, int page); -extern bool -tafsirExists(const Tafsir* tafsir); -extern bool -translationExists(const Translation* tr); -}; - -#endif // GLOBALS_H diff --git a/src/main.cpp b/src/main.cpp index 688dd795..80a30743 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,106 +4,17 @@ */ #include "core/mainwindow.h" -#include "globals.h" +#include "types/reciter.h" +#include "types/tafsir.h" +#include "types/translation.h" +#include "utils/dirmanager.h" +#include "utils/fontmanager.h" #include "utils/logger.h" +#include "utils/settings.h" +#include "utils/shortcuthandler.h" +#include "utils/stylemanager.h" #include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -using namespace Globals; - -/*! - * @brief sets the theme for the application. - * @details Application theme consists of a QPalette for the application and a - * custom stylesheet for different UI components. works through passing the - * theme index (0 = light, 1 = sepia , 2 = dark). - * - * @param themeIdx - index for theme as it appears in the application settings. - */ -void -setTheme(int themeIdx); -/** - * @brief loads the QCF fonts for Quran pages before starting the - * GUI. - * @details The application depends mainly on QCF fonts to display Quranic - * verses and pages, the QCF fonts are a set of fonts designed specifically for - * Madani Mushaf printing. As each page font includes unicode glyphs for each - * word in that page. The fonts are loaded at application startup by loading 604 - * truetype files (ttf). The available QCF versions are 1 and 2. - * - * @param qcfVersion - font version to use. - */ -void -addFonts(int qcfVersion); -/** - * @brief loads UI translation. - * @details Application UI translation is done through compiled Qt translation - * files (.qm). addTranslation() loads the correct qm files for the language - * given. The loaded translation files consist of a Qt base translation and a - * custom application translation file. - * - * @param localeCode - instance from QLocale::Language that defines a - * specific language - */ -void -addTranslation(QLocale::Language localeCode); -/** - * @brief checks the default settings groups and sets them with default - * values if not found. - * @details application settings are stored as ini-formatted files in order to - * provide the same functionality on different platforms. The configuration file - * is stored in Globals::configDir - * directory. - * - * @param settings - pointer to QSettings instance to access app settings - */ -void -checkSettings(QSettings* settings); -/** - * @brief set the default values for non-existing keys in a certain settings - * group (0 - general, 1 - Reader). - * @param settings - pointer to QSettings instance to access app settings - * @param group - integer refering to group to check - */ -void -checkSettingsGroup(QSettings* settings, int group); -/** - * @brief fills the global reciters list and creates their corresponding - * directories. - * @details Fills the QList that contains ::Reciter instances - * that represent the reciters supported by the application. The reciters list - * is used in many parts of the application by creating a constant reference to - * the global QList. It also creates the reciters directories in the application - * recitations directory as needed. - */ -void -populateRecitersList(); -/** - * @brief populates the global tafasir and translation lists with data from - * files.xml - */ -void -populateContentLists(); -/** - * @brief populates the Globals::shortcutDescription QMap with key-value pairs - * of (name - description) and set the default value if shortcut is not found in - * settings, any new shortcuts should be added to shortcuts.xml along with the - * default keybinding for it. - */ -void -populateShortcutsMap(); -/** - * @brief set application-wide variables that represents used paths. - */ -void -setGlobalPaths(); /** * @brief application entry point @@ -122,24 +33,20 @@ main(int argc, char* argv[]) QSplashScreen splash(QPixmap(":/resources/splash.png")); splash.show(); - setGlobalPaths(); - Logger::startLogger(configDir.absolutePath()); + DirManager::setup(); + Logger::startLogger(DirManager::configDir->absolutePath()); Logger::attach(); - settings = new QSettings( - configDir.filePath("qurancompanion.conf"), QSettings::IniFormat, &a); - checkSettings(settings); + Settings::setup(); + ShortcutHandler::populateDescriptionMap(); + StyleManager::loadTheme(); + FontManager::loadUiFonts(); + FontManager::loadQcf(); + Settings::loadQmFiles(); - themeId = settings->value("Theme").toInt(); - qcfVersion = settings->value("Reader/QCF").toInt(); - language = qvariant_cast(settings->value("Language")); - readerMode = qvariant_cast(settings->value("Reader/Mode")); - setTheme(themeId); - addFonts(qcfVersion); - addTranslation(language); - populateRecitersList(); - populateShortcutsMap(); - populateContentLists(); + Tafsir::populateTafasir(); + Translation::populateTranslations(); + Reciter::populateReciters(); MainWindow w(nullptr); splash.finish(&w); @@ -149,314 +56,3 @@ main(int argc, char* argv[]) Logger::stopLogger(); return exitcode; } - -void -setGlobalPaths() -{ - // data - assetsDir = QApplication::applicationDirPath() + QDir::separator() + "assets"; - bismillahDir = - QApplication::applicationDirPath() + QDir::separator() + "bismillah"; - - // config & downloads - if (!configDir.exists("QuranCompanion")) - configDir.mkpath("QuranCompanion"); - configDir.cd("QuranCompanion"); - - if (!downloadsDir.exists("QuranCompanion")) - downloadsDir.mkpath("QuranCompanion"); - downloadsDir.cd("QuranCompanion"); - - if (!downloadsDir.exists("recitations")) - downloadsDir.mkpath("recitations"); - if (!downloadsDir.exists("QCFV2")) - downloadsDir.mkpath("QCFV2"); - if (!downloadsDir.exists("tafasir")) - downloadsDir.mkpath("tafasir"); - if (!downloadsDir.exists("translations")) - downloadsDir.mkpath("translations"); - -#ifdef Q_OS_WIN - updateToolPath = QApplication::applicationDirPath() + QDir::separator() + - "QCMaintenanceTool.exe"; -#endif -} - -void -checkSettings(QSettings* settings) -{ - for (int i = 0; i < 2; i++) - checkSettingsGroup(settings, i); -} - -void -checkSettingsGroup(QSettings* settings, int group) -{ - switch (group) { - case 0: - settings->setValue("Language", - settings->value("Language", (int)QLocale::English)); - settings->setValue("Theme", settings->value("Theme", 0)); - settings->setValue("VOTD", settings->value("VOTD", true)); - settings->setValue("MissingFileWarning", - settings->value("MissingFileWarning", true)); - break; - case 1: - settings->beginGroup("Reader"); - settings->setValue("Mode", settings->value("Mode", 0)); - settings->setValue("FGHighlight", settings->value("FGHighlight", 1)); - settings->setValue("Khatmah", settings->value("Khatmah", 0)); - settings->setValue("AdaptiveFont", settings->value("AdaptiveFont", true)); - settings->setValue("QCF1Size", settings->value("QCF1Size", 22)); - settings->setValue("QCF2Size", settings->value("QCF2Size", 20)); - settings->setValue("QCF", settings->value("QCF", 1)); - settings->setValue("VerseType", settings->value("VerseType", 0)); - settings->setValue("VerseFontSize", settings->value("VerseFontSize", 20)); - settings->setValue("Tafsir", settings->value("Tafsir", 6)); - settings->setValue("Translation", settings->value("Translation", 5)); - settings->setValue( - "SideContentFont", - settings->value("SideContentFont", QFont("Expo Arabic", 14))); - settings->endGroup(); - break; - } -} - -void -addFonts(int qcfVersion) -{ - QDir fontsDir; - fontsDir = QApplication::applicationDirPath() + QDir::separator() + "assets" + - QDir::separator() + "fonts"; - - // ui fonts - foreach (const QFileInfo& font, fontsDir.entryInfoList(QDir::Files)) - QFontDatabase::addApplicationFont(font.absoluteFilePath()); - - // font for surah frames - QFontDatabase::addApplicationFont(fontsDir.filePath("QCFV1/QCF_BSML.ttf")); - switch (qcfVersion) { - case 1: - fontsDir.cd("QCFV1"); - break; - case 2: - fontsDir.setPath(downloadsDir.absolutePath() + "/QCFV2"); - qcfFontPrefix = "QCF2"; - break; - } - - // add required fonts - for (int i = 1; i < 605; i++) { - QString fontName = pageFontname(i) + ".ttf"; - - if (qcfVersion == 2 && !fontsDir.exists(fontName)) { - settings->setValue("Reader/QCF", 1); - settings->sync(); - qFatal() << fontsDir.filePath(fontName) - << " font file not found, fallback to QCF v1"; - } else - QFontDatabase::addApplicationFont(fontsDir.filePath(fontName)); - } - - // set default UI fonts to use - QStringList uiFonts; - uiFonts << "Noto Sans Display" - << "Expo Arabic"; - qApp->setFont(QFont(uiFonts, qApp->font().pointSize())); -} - -void -setTheme(int themeIdx) -{ - qApp->setStyle(QStyleFactory::create("Fusion")); - - QPalette themeColors; - QFile styles, palette; - switch (themeIdx) { - case 0: - themeResources.setPath(":/resources/light/"); - styles.setFileName(themeResources.filePath("light.qss")); - palette.setFileName(themeResources.filePath("light.xml")); - darkMode = false; - break; - - case 1: - themeResources.setPath(":/resources/light/"); - styles.setFileName(themeResources.filePath("sepia.qss")); - palette.setFileName(themeResources.filePath("sepia.xml")); - darkMode = false; - break; - - case 2: - themeResources.setPath(":/resources/dark/"); - styles.setFileName(themeResources.filePath("dark.qss")); - palette.setFileName(themeResources.filePath("dark.xml")); - darkMode = true; - break; - } - - if (!palette.open(QIODevice::ReadOnly | QIODevice::Text)) { - qCritical() << "Couldn't Read Color palette xml"; - } - - QXmlStreamReader paletteReader(&palette); - QPalette::ColorGroup group = QPalette::All; - while (!paletteReader.atEnd() && !paletteReader.hasError()) { - QXmlStreamReader::TokenType token = paletteReader.readNext(); - if (token == QXmlStreamReader::StartElement) { - if (paletteReader.name().toString() == "enabled") - group = QPalette::All; - else if (paletteReader.name().toString() == "disabled") - group = QPalette::Disabled; - // color element - else { - QPalette::ColorRole role; - int red, green, blue; - role = static_cast( - paletteReader.attributes().value("role").toInt()); - red = paletteReader.attributes().value("red").toInt(); - green = paletteReader.attributes().value("green").toInt(); - blue = paletteReader.attributes().value("blue").toInt(); - - themeColors.setColor(group, role, QColor(red, green, blue)); - } - } - } - - palette.close(); - qApp->setPalette(themeColors); - - // load stylesheet - if (styles.open(QIODevice::ReadOnly)) { - qApp->setStyleSheet(styles.readAll()); - styles.close(); - } - - awesome = new fa::QtAwesome(qApp); - awesome->initFontAwesome(); -} - -void -addTranslation(QLocale::Language localeCode) -{ - if (localeCode == QLocale::English) - return; - - QString code = QLocale::languageToCode(localeCode); - QTranslator *translation = new QTranslator(qApp), - *qtBase = new QTranslator(qApp); - - if (translation->load(":/i18n/qc_" + code + ".qm")) { - qInfo() << translation->language() << "translation loaded"; - qInfo() << "base translation:" << qtBase->load(":/base/" + code + ".qm"); - qApp->installTranslator(translation); - qApp->installTranslator(qtBase); - - } else { - qWarning() << code + " translation not loaded!"; - delete translation; - delete qtBase; - } -} - -void -populateRecitersList() -{ - QFile reciters(":/resources/reciters.xml"); - if (!reciters.open(QIODevice::ReadOnly)) - qFatal("Couldn't Open Reciters XML, Exiting"); - - QXmlStreamReader reader(&reciters); - while (!reader.atEnd() && !reader.hasError()) { - QXmlStreamReader::TokenType token = reader.readNext(); - if (token == QXmlStreamReader::StartElement) { - if (reader.name().toString() == "reciter") { - Reciter reciter; - reciter.baseDirName = reader.attributes().value("dirname").toString(); - reciter.displayName = qApp->translate( - "MainWindow", reader.attributes().value("display").toLatin1()); - reciter.baseUrl = reader.attributes().value("url").toString(); - reciter.basmallahPath = bismillahDir.absoluteFilePath( - reader.attributes().value("basmallah").toString()); - reciter.useId = reader.attributes().value("useid").toInt(); - - recitersList.append(reciter); - } - } - } - - reciters.close(); - recitersList.squeeze(); - - // create reciters directories - downloadsDir.cd("recitations"); - foreach (const Reciter& r, recitersList) { - if (!downloadsDir.exists(r.baseDirName)) - downloadsDir.mkdir(r.baseDirName); - } - downloadsDir.cdUp(); -} - -void -populateShortcutsMap() -{ - QFile shortcuts(":/resources/shortcuts.xml"); - if (!shortcuts.open(QIODevice::ReadOnly)) - qCritical("Couldn't Open Shortcuts XML"); - - settings->beginGroup("Shortcuts"); - QXmlStreamReader reader(&shortcuts); - while (!reader.atEnd() && !reader.hasError()) { - QXmlStreamReader::TokenType token = reader.readNext(); - if (token == QXmlStreamReader::StartElement) { - if (reader.name().toString() == "shortcut") { - QString key = reader.attributes().value("key").toString(); - QString defBind = reader.attributes().value("default").toString(); - QString desc = - qApp->translate("SettingsDialog", - reader.attributes().value("description").toLatin1()); - - shortcutDescription.insert(key, desc); - if (!settings->contains(key)) - settings->setValue(key, defBind); - } - } - } - - settings->endGroup(); - shortcuts.close(); -} - -void -populateContentLists() -{ - QFile content(":/resources/files.xml"); - if (!content.open(QIODevice::ReadOnly)) - qCritical("Couldn't Open Files XML"); - - QXmlStreamReader reader(&content); - while (!reader.atEnd() && !reader.hasError()) { - QXmlStreamReader::TokenType token = reader.readNext(); - if (token == QXmlStreamReader::StartElement) { - if (reader.name().toString() == "tafsir") { - QString name = qApp->translate( - "SettingsDialog", reader.attributes().value("name").toLatin1()); - QString file = reader.attributes().value("file").toString(); - bool text = reader.attributes().value("text").toInt(); - bool extra = reader.attributes().value("extra").toInt(); - tafasirList.append(Tafsir{ name, file, text, extra }); - } - // translations - else if (reader.name().toString() == "translation") { - QString name = reader.attributes().value("name").toString(); - QString file = reader.attributes().value("file").toString(); - bool extra = reader.attributes().value("extra").toInt(); - translationsList.append(Translation{ name, file, extra }); - } - } - } - - content.close(); - tafasirList.squeeze(); - translationsList.squeeze(); -} diff --git a/src/types/reciter.cpp b/src/types/reciter.cpp new file mode 100644 index 00000000..799f6adc --- /dev/null +++ b/src/types/reciter.cpp @@ -0,0 +1,88 @@ +#include "reciter.h" +#include "../utils/dirmanager.h" +#include +#include +#include + +QList> Reciter::reciters; + +void +Reciter::populateReciters() +{ + QFile recitersFile(":/resources/reciters.xml"); + if (!recitersFile.open(QIODevice::ReadOnly)) + qFatal("Couldn't Open Reciters XML, Exiting"); + + QXmlStreamReader reader(&recitersFile); + while (!reader.atEnd() && !reader.hasError()) { + QXmlStreamReader::TokenType token = reader.readNext(); + if (token == QXmlStreamReader::StartElement) { + if (reader.name().toString() == "reciter") { + QString baseDirName = reader.attributes().value("dirname").toString(); + QString displayName = qApp->translate( + "MainWindow", reader.attributes().value("display").toLatin1()); + QString baseUrl = reader.attributes().value("url").toString(); + QString basmallahPath = DirManager::basmallahDir->absoluteFilePath( + reader.attributes().value("basmallah").toString()); + bool useId = reader.attributes().value("useid").toInt(); + + reciters.append(QSharedPointer::create( + baseDirName, displayName, basmallahPath, baseUrl, useId)); + } + } + } + + recitersFile.close(); + reciters.squeeze(); + + // create reciters directories + DirManager::downloadsDir->cd("recitations"); + foreach (const QSharedPointer& r, reciters) { + if (!DirManager::downloadsDir->exists(r->baseDirName())) + DirManager::downloadsDir->mkdir(r->baseDirName()); + } + DirManager::downloadsDir->cdUp(); +} + +Reciter::Reciter(QString dir, + QString display, + QString basmallah, + QString url, + bool useId) + : m_baseDirName(dir) + , m_displayName(display) + , m_basmallahPath(basmallah) + , m_baseUrl(url) + , m_useId(useId) +{ +} + +QString +Reciter::baseUrl() const +{ + return m_baseUrl; +} + +QString +Reciter::basmallahPath() const +{ + return m_basmallahPath; +} + +QString +Reciter::displayName() const +{ + return m_displayName; +} + +QString +Reciter::baseDirName() const +{ + return m_baseDirName; +} + +bool +Reciter::useId() const +{ + return m_useId; +} diff --git a/src/types/reciter.h b/src/types/reciter.h new file mode 100644 index 00000000..132e3f72 --- /dev/null +++ b/src/types/reciter.h @@ -0,0 +1,56 @@ +#ifndef RECITER_H +#define RECITER_H + +#include +#include +#include + +/** + * @class Reciter + * @brief Reciter class represents a quran reciter + */ +class Reciter +{ +public: + static QList> reciters; + static void populateReciters(); + + explicit Reciter(QString dir, + QString display, + QString basmallah, + QString url, + bool useId = false); + + QString basmallahPath() const; + QString displayName() const; + QString baseDirName() const; + QString baseUrl() const; + bool useId() const; + +private: + /** + * @brief The name of the directory conatining recitations in the application + * recitations directory. + */ + QString m_baseDirName; + /** + * @brief The reciter name as its displayed in the UI. + */ + QString m_displayName; + /** + * @brief Absolute path to the reciters bismillah audio file. + */ + QString m_basmallahPath; + /** + * @brief Url to download recitation files from. + */ + QString m_baseUrl; + /** + * @brief Boolean value representing whether the verse recitations should be + * downloading using the verse number relative to the beginning of the Quran + * or a combination of surah and verse numbers. + */ + bool m_useId; +}; + +#endif // RECITER_H diff --git a/src/types/tafsir.cpp b/src/types/tafsir.cpp new file mode 100644 index 00000000..a2b09465 --- /dev/null +++ b/src/types/tafsir.cpp @@ -0,0 +1,82 @@ +#include "tafsir.h" +#include "../utils/dirmanager.h" +#include +#include +#include +#include + +QList> Tafsir::tafasir; + +void +Tafsir::populateTafasir() +{ + QFile content(":/resources/files.xml"); + if (!content.open(QIODevice::ReadOnly)) + qCritical("Couldn't Open Files XML"); + + QXmlStreamReader reader(&content); + while (!reader.atEnd() && !reader.hasError()) { + QXmlStreamReader::TokenType token = reader.readNext(); + if (token == QXmlStreamReader::StartElement) { + if (reader.name().toString() == "tafsir") { + QString name = qApp->translate( + "SettingsDialog", reader.attributes().value("name").toLatin1()); + QString file = reader.attributes().value("file").toString(); + bool isText = reader.attributes().value("text").toInt(); + bool isExtra = reader.attributes().value("extra").toInt(); + tafasir.append( + QSharedPointer::create(name, file, isText, isExtra)); + } + } + } + + content.close(); + tafasir.squeeze(); +} + +Tafsir::Tafsir(QString display, QString filename, bool isText, bool isExtra) + : m_displayName(display) + , m_filename(filename) + , m_isText(isText) + , m_isExtra(isExtra) +{ +} + +bool +Tafsir::tafsirExists(int idx) +{ + const QSharedPointer& t = tafasir.at(idx); + return tafsirExists(t); +} + +bool +Tafsir::tafsirExists(const QSharedPointer& t) +{ + const QDir& baseDir = + t->isExtra() ? *DirManager::downloadsDir : *DirManager::assetsDir; + return baseDir.exists("tafasir/" + t->filename()); +} + +const QString& +Tafsir::displayName() const +{ + return m_displayName; +} + +const QString& +Tafsir::filename() const +{ + return m_filename; +} + +const bool +Tafsir::isText() const +{ + return m_isText; +} + +const bool +Tafsir::isExtra() const +{ + return m_isExtra; +} diff --git a/src/types/tafsir.h b/src/types/tafsir.h new file mode 100644 index 00000000..e2a57d00 --- /dev/null +++ b/src/types/tafsir.h @@ -0,0 +1,31 @@ +#ifndef TAFSIR_H +#define TAFSIR_H + +#include +#include +#include + +class Tafsir +{ +public: + static void populateTafasir(); + static QList> tafasir; + + static bool tafsirExists(int idx); + static bool tafsirExists(const QSharedPointer& t); + + explicit Tafsir(QString display, QString filename, bool isText, bool isExtra); + + const QString& displayName() const; + const QString& filename() const; + const bool isText() const; + const bool isExtra() const; + +private: + QString m_displayName; + QString m_filename; + bool m_isText; + bool m_isExtra; +}; + +#endif // TAFSIR_H diff --git a/src/types/translation.cpp b/src/types/translation.cpp new file mode 100644 index 00000000..f4ee7259 --- /dev/null +++ b/src/types/translation.cpp @@ -0,0 +1,73 @@ +#include "translation.h" +#include "../utils/dirmanager.h" +#include +#include +#include +#include + +QList> Translation::translations; + +void +Translation::populateTranslations() +{ + QFile content(":/resources/files.xml"); + if (!content.open(QIODevice::ReadOnly)) + qCritical("Couldn't Open Files XML"); + + QXmlStreamReader reader(&content); + while (!reader.atEnd() && !reader.hasError()) { + QXmlStreamReader::TokenType token = reader.readNext(); + if (token == QXmlStreamReader::StartElement) { + if (reader.name().toString() == "translation") { + QString name = reader.attributes().value("name").toString(); + QString file = reader.attributes().value("file").toString(); + bool extra = reader.attributes().value("extra").toInt(); + translations.append( + QSharedPointer::create(name, file, extra)); + } + } + } + + content.close(); + translations.squeeze(); +} + +Translation::Translation(QString display, QString filename, bool isExtra) + : m_displayName(display) + , m_filename(filename) + , m_isExtra(isExtra) +{ +} + +bool +Translation::translationExists(int idx) +{ + const QSharedPointer& tr = translations.at(idx); + return translationExists(tr); +} + +bool +Translation::translationExists(const QSharedPointer& tr) +{ + const QDir& baseDir = + tr->isExtra() ? *DirManager::downloadsDir : *DirManager::assetsDir; + return baseDir.exists("translations/" + tr->filename()); +} + +const QString& +Translation::displayName() const +{ + return m_displayName; +} + +const QString& +Translation::filename() const +{ + return m_filename; +} + +const bool& +Translation::isExtra() const +{ + return m_isExtra; +} diff --git a/src/types/translation.h b/src/types/translation.h new file mode 100644 index 00000000..3f0de999 --- /dev/null +++ b/src/types/translation.h @@ -0,0 +1,29 @@ +#ifndef TRANSLATION_H +#define TRANSLATION_H + +#include +#include +#include + +class Translation +{ +public: + static QList> translations; + static void populateTranslations(); + + static bool translationExists(int idx); + static bool translationExists(const QSharedPointer& tr); + + explicit Translation(QString display, QString filename, bool isExtra); + + const QString& displayName() const; + const QString& filename() const; + const bool& isExtra() const; + +private: + QString m_displayName; + QString m_filename; + bool m_isExtra; +}; + +#endif // TRANSLATION_H diff --git a/src/utils/verse.cpp b/src/types/verse.cpp similarity index 100% rename from src/utils/verse.cpp rename to src/types/verse.cpp diff --git a/src/utils/verse.h b/src/types/verse.h similarity index 86% rename from src/utils/verse.h rename to src/types/verse.h index e1b6488f..6177f2d9 100644 --- a/src/utils/verse.h +++ b/src/types/verse.h @@ -1,8 +1,9 @@ #ifndef VERSE_H #define VERSE_H -#include "../globals.h" -#include "dbmanager.h" +#include "../utils/settings.h" +#include "../utils/dbmanager.h" +#include /** * @brief Verse class represents a single quran verse @@ -43,8 +44,8 @@ class Verse void setNumber(int newNumber); private: - const QSettings* m_settings = Globals::settings; - DBManager* m_dbMgr = DBManager::instance(); + const QSharedPointer m_settings = Settings::settings; + QSharedPointer m_dbMgr = DBManager::current(); void updateSurahCount(); int m_page = -1; ///< verse page diff --git a/src/utils/dbmanager.cpp b/src/utils/dbmanager.cpp index e1c82c2e..f80f1787 100644 --- a/src/utils/dbmanager.cpp +++ b/src/utils/dbmanager.cpp @@ -5,19 +5,20 @@ #include "dbmanager.h" -DBManager* -DBManager::instance() +QSharedPointer +DBManager::current() { - static DBManager controller = DBManager(qApp); - return &controller; + static QSharedPointer controller = + QSharedPointer::create(); + return controller; } DBManager::DBManager(QObject* parent) : QObject(parent) { - m_quranDbPath.setFile(m_assetsDir.filePath("quran.db")); - m_glyphsDbPath.setFile(m_assetsDir.filePath("glyphs.db")); - m_betaqatDbPath.setFile(m_assetsDir.filePath("betaqat.db")); + m_quranDbPath.setFile(m_assetsDir->filePath("quran.db")); + m_glyphsDbPath.setFile(m_assetsDir->filePath("glyphs.db")); + m_betaqatDbPath.setFile(m_assetsDir->filePath("betaqat.db")); // set database driver, set the path & open a connection with the db QSqlDatabase::addDatabase("QSQLITE", "QuranCon"); @@ -82,9 +83,10 @@ DBManager::setCurrentTafsir(int tafsirIdx) if (tafsirIdx < 0 || tafsirIdx >= m_tafasirList.size()) return; - m_currTafsir = &m_tafasirList[tafsirIdx]; - const QDir& baseDir = m_currTafsir->extra ? m_downloadsDir : m_assetsDir; - QString path = "tafasir/" + m_currTafsir->filename; + m_currTafsir = m_tafasirList[tafsirIdx]; + const QDir& baseDir = + m_currTafsir->isExtra() ? *m_downloadsDir : *m_assetsDir; + QString path = "tafasir/" + m_currTafsir->filename(); if (baseDir.exists(path)) m_tafsirDbPath.setFile(baseDir.filePath(path)); } @@ -95,9 +97,9 @@ DBManager::setCurrentTranslation(int translationIdx) if (translationIdx < 0 || translationIdx >= m_translationsList.size()) return; - m_currTrans = &m_translationsList[translationIdx]; - const QDir& baseDir = m_currTrans->extra ? m_downloadsDir : m_assetsDir; - QString path = "translations/" + m_currTrans->filename; + m_currTrans = m_translationsList[translationIdx]; + const QDir& baseDir = m_currTrans->isExtra() ? *m_downloadsDir : *m_assetsDir; + QString path = "translations/" + m_currTrans->filename(); if (baseDir.exists(path)) m_transDbPath.setFile(baseDir.filePath(path)); } @@ -752,9 +754,8 @@ DBManager::addBookmark(QList vInfo) return false; } - if (!m_openDBCon.commit()) - return false; - + m_openDBCon.commit(); + emit bookmarkAdded(); return true; } @@ -774,6 +775,7 @@ DBManager::removeBookmark(QList vInfo) return false; } + emit bookmarkRemoved(); return true; } @@ -841,7 +843,7 @@ DBManager::activeKhatmah() const return m_activeKhatmah; } -const Tafsir* +QSharedPointer DBManager::currTafsir() const { return m_currTafsir; diff --git a/src/utils/dbmanager.h b/src/utils/dbmanager.h index 6304ab22..8be889ef 100644 --- a/src/utils/dbmanager.h +++ b/src/utils/dbmanager.h @@ -6,16 +6,21 @@ #ifndef DBMANAGER_H #define DBMANAGER_H -#include "../globals.h" +#include "../types/tafsir.h" +#include "../types/translation.h" +#include "../utils/dirmanager.h" +#include "../utils/settings.h" #include #include #include #include #include #include +#include #include #include #include +typedef Settings::VerseType VerseType; /** * @brief DBManager is as an interface for preforming queries to @@ -26,118 +31,119 @@ class DBManager : public QObject Q_OBJECT public: - static DBManager *instance(); - /** + static QSharedPointer current(); + /** * @brief Database enum holds different values representing database files * used in different member functions. */ - enum Database { - null, ///< default value - quran, ///< (quran.db) main Quran database file - glyphs, ///< (glyphs.db) QCF glyphs database - betaqat, - bookmarks, ///< (bookmarks.db) bookmarked verses and khatmah database - tafsir, ///< currently selected tafsir database file - translation ///< currently selected translation database file - }; + enum Database + { + null, ///< default value + quran, ///< (quran.db) main Quran database file + glyphs, ///< (glyphs.db) QCF glyphs database + betaqat, + bookmarks, ///< (bookmarks.db) bookmarked verses and khatmah database + tafsir, ///< currently selected tafsir database file + translation ///< currently selected translation database file + }; - /** + /** * @brief Class constructor * @param parent - pointer to parent widget */ - explicit DBManager(QObject *parent = nullptr); - /** + explicit DBManager(QObject* parent = nullptr); + /** * @brief getter for translated Surah names QList * @return QList of Surah name strings */ - QList surahNameList(); - /** + QList surahNameList(); + /** * @brief sets the active tafsir * @param tafsirName - DBManager::Tafsir entry */ - void setCurrentTafsir(int tafsirIdx); - /** + void setCurrentTafsir(int tafsirIdx); + /** * @brief sets the active translation * @param translationName - DBManager::Translation entry */ - void setCurrentTranslation(int translationIdx); - /** + void setCurrentTranslation(int translationIdx); + /** * @brief sets the currently active sqlite database file and opens * corresponding connection based on the DBManager::Database type * @param db - DBManager::Database entry * @param filePath - path to the database file */ - void setOpenDatabase(Database db, QString filePath); - /** + void setOpenDatabase(Database db, QString filePath); + /** * @brief gets the surah number and juz number of the first verse in the page, * used to display page header information * @param page - Quran page number * @return QList of 2 integers [0: surah index, 1: juz number] */ - QPair getPageMetadata(const int page); - /** + QPair getPageMetadata(const int page); + /** * @brief get Quran page QCF glyphs separated as lines * @param page - Quran page number * @return QList of page lines */ - QStringList getPageLines(const int page); - /** + QStringList getPageLines(const int page); + /** * @brief gets a QList of ::Verse instances for the page verses * @param page - Quran page number * @return QList of ::Verse instances */ - QList> getVerseInfoList(const int page); - /** + QList> getVerseInfoList(const int page); + /** * @brief gets the surah name glyph for the QCF_BSML font, used to render * surah frame in Quran page * @param sura - sura number (1-114) * @return QString of glyphs */ - QString getSurahNameGlyph(const int sura); - /** + QString getSurahNameGlyph(const int sura); + /** * @brief gets the juz name in arabic, used in page header * @param juz - juz number * @return QString of the juz name */ - QString getJuzGlyph(const int juz); - /** + QString getJuzGlyph(const int juz); + /** * @brief gets the verse QCF glyphs for the corresponding QCF page font * @param sIdx - sura number (1-114) * @param vIdx - verse number * @return QString of verse glyphs */ - QString getVerseGlyphs(const int sIdx, const int vIdx); - /** + QString getVerseGlyphs(const int sIdx, const int vIdx); + /** * @brief gets the verse text * @param sIdx - sura number (1-114) * @param vIdx - verse number * @return QString of the verse text */ - QString getVerseText(const int sIdx, const int vIdx); - /** + QString getVerseText(const int sIdx, const int vIdx); + /** * @brief sets the given ::Verse as the last position reached in the current * active khatmah * @param v - ::Verse reached in khatmah */ - bool saveActiveKhatmah(QList vInfo); - /** + bool saveActiveKhatmah(QList vInfo); + /** * @brief get all available khatmah ids * @return QList of khatmah id(s) */ - QList getAllKhatmah(); - /** + QList getAllKhatmah(); + /** * @brief get the name of the khatmah with id given * @return QString containing the khatmah name */ - QString getKhatmahName(const int id); - /** + QString getKhatmahName(const int id); + /** * @brief gets the last position saved for the khatmah with the id given and * stores the position in the ::Verse v * @return boolean indicating a successful operation (false in case of error * and in case id does not exist) */ - bool getKhatmahPos(const int khatmahId, QList &vInfo); - /** + bool getKhatmahPos(const int khatmahId, QList& vInfo); + /** * @brief add a new khatmah/replace khatmah with given id with position of * ::Verse v * @param v - ::Verse to set as the khatmah position @@ -145,187 +151,192 @@ class DBManager : public QObject * @param id - id of khatmah to replace, -1 means do not replace (default: -1) * @return id of newly added khatmah or id parameter if defined */ - int addKhatmah(QList vInfo, const QString name, const int id = -1); - /** + int addKhatmah(QList vInfo, const QString name, const int id = -1); + /** * @brief rename the khatmah with the given id to newName * @param khatmahId - id of khatmah to rename * @param newName - new name to set * @return boolean indicating a successful operation (false in case the name * exists) */ - bool editKhatmahName(const int khatmahId, QString newName); - /** + bool editKhatmahName(const int khatmahId, QString newName); + /** * @brief remove the khatmah with the given id from database * @param id - id of khatmah to remove */ - void removeKhatmah(const int id); - /** + void removeKhatmah(const int id); + /** * @brief gets the number of the last verse in the surah passed * @param surahIdx - surah number (1-114) * @return number of verses in the sura */ - int getSurahVerseCount(const int surahIdx); - /** + int getSurahVerseCount(const int surahIdx); + /** * @brief gets the page where the surah begins * @param surahIdx - sura number * @return page of the first verse in the sura */ - int getSurahStartPage(int surahIdx); - /** + int getSurahStartPage(int surahIdx); + /** * @brief gets the surah name in English or Arabic (default is English) * @param sIdx - sura number * @param ar - boolean to return arabic sura name * @return QString containing the sura name */ - QString getSurahName(const int sIdx, bool ar = false); - /** + QString getSurahName(const int sIdx, bool ar = false); + /** * @brief get the surah card (betaqa) content * @param surah - surah number * @return QString of the surah card text */ - QString getBetaqa(const int surah); - /** + QString getBetaqa(const int surah); + /** * @brief gets the corresponding id for the verse in the database * @param sIdx - sura number * @param vIdx - verse number * @return id of the verse */ - int getVerseId(const int sIdx, const int vIdx); - /** + int getVerseId(const int sIdx, const int vIdx); + /** * @brief get the verse with the corresponding id and return it as a ::Verse * instance * @param id - verse id * @return ::Verse instance */ - QList getVerseById(const int id); - /** + QList getVerseById(const int id); + /** * @brief gets the page where the verse is found * @param surahIdx - sura number * @param verse - verse number * @return page number */ - int getVersePage(const int &surahIdx, const int &verse); - /** + int getVersePage(const int& surahIdx, const int& verse); + /** * @brief gets the page where the corresponding juz starts * @param juz - juz number * @return page number */ - int getJuzStartPage(const int juz); - /** + int getJuzStartPage(const int juz); + /** * @brief get the juz which the passed page is a part of * @param page - page number * @return juz number */ - int getJuzOfPage(const int page); - /** + int getJuzOfPage(const int page); + /** * @brief searches the database for surahs matching the given text pattern, * the pattern can be either in English or Arabic * @param text - name / part of the name of the sura * @return QList of sura numbers which contain the given text */ - QList searchSurahNames(QString text); - /** + QList searchSurahNames(QString text); + /** * @brief search specific surahs for the given search text * @param searchText - text to search for * @param surahs - QList of surah numbers to search in * @param whole - boolean value to search for whole words only * @return QList of ::Verse instances representing the search results */ - QList> searchSurahs(QString searchText, - const QList surahs, - const bool whole = false); - /** + QList> searchSurahs(QString searchText, + const QList surahs, + const bool whole = false); + /** * @brief search a range of pages for the given search text * @param searchText - text to search for * @param range - array of start & end page numbers * @param whole - boolean value to indicate search for whole words only * @return QList of ::Verse instances representing the search results */ - QList> searchVerses(QString searchText, - const int range[2] = new int[2]{1, 604}, - const bool whole = false); - /** + QList> searchVerses(QString searchText, + const int range[2] = new int[2]{ 1, 604 }, + const bool whole = false); + /** * @brief gets the tafsir content for the given verse using the active * DBManager::Tafsir * @param sIdx - surah number * @param vIdx - verse number * @return QString containing the tafsir of the verse */ - QString getTafsir(const int sIdx, const int vIdx); - /** + QString getTafsir(const int sIdx, const int vIdx); + /** * @brief gets the translation of the given verse using the active * DBManager::Translation * @param sIdx - surah number * @param vIdx - verse number * @return QString containing the verse translation */ - QString getTranslation(const int sIdx, const int vIdx); - /** + QString getTranslation(const int sIdx, const int vIdx); + /** * @brief gets a random verse from the Quran * @return QPair of ::Verse instance and verse text */ - QList randomVerse(); - /** + QList randomVerse(); + /** * @brief gets a QList of ::Verse instances representing the bookmarked verse * within the given sura (default gets all) * @param surahIdx - sura number (-1 returns all bookmarks) * @return QList of bookmarked verses */ - QList> bookmarkedVerses(int surahIdx = -1); - /** + QList> bookmarkedVerses(int surahIdx = -1); + /** * @brief checks whether the given ::Verse is bookmarked * @param vInfo - ::Verse instance to check * @return boolean */ - bool isBookmarked(QList vInfo); - /** + bool isBookmarked(QList vInfo); + /** * @brief add the given ::Verse to bookmarks * @param vInfo - ::Verse instance to add * @return boolean */ - bool addBookmark(QList vInfo); - /** + bool addBookmark(QList vInfo); + /** * @brief remove the given ::Verse from bookmarks * @param vInfo - ::Verse instance to remove * @return boolean indicating successful removal */ - bool removeBookmark(QList vInfo); - /** + bool removeBookmark(QList vInfo); + /** * @brief getter for m_currTafsir * @return the currently set DBManager::Tafasir */ - const Tafsir *currTafsir() const; - /** + QSharedPointer currTafsir() const; + /** * @brief getter for m_activeKhatmah * @return the currently active khatmah id */ - const int activeKhatmah() const; - /** + const int activeKhatmah() const; + /** * @brief setter for m_activeKhatmah * @param id - id of the active khatmah */ - void setActiveKhatmah(const int id); - /** + void setActiveKhatmah(const int id); + /** * @brief Set the VerseType shown * @param newVerseType */ - void setVerseType(VerseType newVerseType); - /** + void setVerseType(VerseType newVerseType); + /** * @brief getter for m_verseType * @return VerseType */ - VerseType getVerseType() const; + VerseType getVerseType() const; + +signals: + void bookmarkAdded(); + void bookmarkRemoved(); private: - const QDir& m_assetsDir = Globals::assetsDir; - const QDir& m_downloadsDir = Globals::downloadsDir; - const int m_qcfVer = Globals::qcfVersion; - const QSettings* m_settings = Globals::settings; - const QLocale::Language m_languageCode = Globals::language; - const QList& m_tafasirList = Globals::tafasirList; - const QList& m_translationsList = Globals::translationsList; + const int m_qcfVer = Settings::qcfVersion; + const QLocale::Language m_languageCode = Settings::language; + const QSharedPointer m_assetsDir = DirManager::assetsDir; + const QSharedPointer m_downloadsDir = DirManager::downloadsDir; + const QSharedPointer m_settings = Settings::settings; + const QList>& m_tafasirList = Tafsir::tafasir; + const QList>& m_translationsList = + Translation::translations; const QString m_bookmarksFilepath = - Globals::configDir.absoluteFilePath("bookmarks.db"); + DirManager::configDir->absoluteFilePath("bookmarks.db"); /** * @brief integer id of the current active khatmah */ @@ -340,15 +351,15 @@ class DBManager : public QObject */ QSqlDatabase m_openDBCon; - VerseType m_verseType = VerseType::qcf; + VerseType m_verseType = Settings::qcf; /** * @brief the current active DBManager::Tafasir */ - const Tafsir* m_currTafsir = nullptr; + QSharedPointer m_currTafsir = nullptr; /** * @brief the current active DBManager::Translation */ - const Translation* m_currTrans = nullptr; + QSharedPointer m_currTrans = nullptr; /** * @brief path to the currently active tafsir database file */ diff --git a/src/utils/dirmanager.cpp b/src/utils/dirmanager.cpp new file mode 100644 index 00000000..38b713f3 --- /dev/null +++ b/src/utils/dirmanager.cpp @@ -0,0 +1,43 @@ +#include "dirmanager.h" +#include +#include + +QSharedPointer DirManager::fontsDir; +QSharedPointer DirManager::assetsDir; +QSharedPointer DirManager::basmallahDir; +QSharedPointer DirManager::downloadsDir = QSharedPointer::create( + QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); +QSharedPointer DirManager::configDir = QSharedPointer::create( + QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); + +void +DirManager::setup() +{ + + assetsDir = QSharedPointer::create(QApplication::applicationDirPath() + + QDir::separator() + "assets"); + fontsDir = QSharedPointer::create(assetsDir->absoluteFilePath("fonts")); + basmallahDir = QSharedPointer::create( + QApplication::applicationDirPath() + QDir::separator() + "bismillah"); + + // config & downloads + if (!configDir->exists("QuranCompanion")) + configDir->mkpath("QuranCompanion"); + configDir->cd("QuranCompanion"); + + if (!downloadsDir->exists("QuranCompanion")) + downloadsDir->mkpath("QuranCompanion"); + downloadsDir->cd("QuranCompanion"); + + if (!downloadsDir->exists("recitations")) + downloadsDir->mkpath("recitations"); + + if (!downloadsDir->exists("QCFV2")) + downloadsDir->mkpath("QCFV2"); + + if (!downloadsDir->exists("tafasir")) + downloadsDir->mkpath("tafasir"); + + if (!downloadsDir->exists("translations")) + downloadsDir->mkpath("translations"); +} diff --git a/src/utils/dirmanager.h b/src/utils/dirmanager.h new file mode 100644 index 00000000..83209c9c --- /dev/null +++ b/src/utils/dirmanager.h @@ -0,0 +1,18 @@ +#ifndef DIRMANAGER_H +#define DIRMANAGER_H + +#include +#include + +class DirManager +{ +public: + static void setup(); + static QSharedPointer fontsDir; + static QSharedPointer configDir; + static QSharedPointer assetsDir; + static QSharedPointer downloadsDir; + static QSharedPointer basmallahDir; +}; + +#endif // DIRMANAGER_H diff --git a/src/utils/downloadmanager.cpp b/src/utils/downloadmanager.cpp index a960cc23..ef34f844 100644 --- a/src/utils/downloadmanager.cpp +++ b/src/utils/downloadmanager.cpp @@ -83,7 +83,7 @@ DownloadManager::enqeueQCF() .arg(QString::number(i).rightJustified(3, '0')); t.metainfo = { -1, -1, i }; t.metainfo.squeeze(); - t.downloadPath.setFile(m_downloadsDir.absoluteFilePath(path)); + t.downloadPath.setFile(m_downloadsDir->absoluteFilePath(path)); t.link = QUrl::fromEncoded((base + path).toLatin1()); m_taskQueue.enqueue(t); } @@ -96,14 +96,14 @@ DownloadManager::enqeueTask(QPair info) "https://github.com/0xzer0x/quran-companion/raw/main/extras/"; QString path; if (info.first) - path = "translations/" + m_trList.at(info.second).filename; + path = "translations/" + m_trList.at(info.second)->filename(); else - path = "tafasir/" + m_tafasirList.at(info.second).filename; + path = "tafasir/" + m_tafasirList.at(info.second)->filename(); DownloadTask t; t.metainfo = { info.first, info.second, 0 }; t.metainfo.squeeze(); t.link = QUrl::fromEncoded((base + path).toLatin1()); - t.downloadPath.setFile(m_downloadsDir.absoluteFilePath(path)); + t.downloadPath.setFile(m_downloadsDir->absoluteFilePath(path)); m_taskQueue.enqueue(t); } @@ -115,8 +115,8 @@ DownloadManager::enqeueTask(int reciterIdx, int surah, int verse) t.metainfo = { reciterIdx, surah, verse }; t.metainfo.squeeze(); t.link = downloadUrl(reciterIdx, surah, verse); - t.downloadPath.setFile(m_downloadsDir.absoluteFilePath( - path.arg(m_recitersList.at(reciterIdx).baseDirName, + t.downloadPath.setFile(m_downloadsDir->absoluteFilePath( + path.arg(m_recitersList.at(reciterIdx)->baseDirName(), QString::number(surah).rightJustified(3, '0') + QString::number(verse).rightJustified(3, '0')))); @@ -262,9 +262,9 @@ DownloadManager::downloadUrl(const int reciterIdx, const int surah, const int verse) const { - const Reciter& r = m_recitersList.at(reciterIdx); - QString url = r.baseUrl; - if (r.useId) + const Reciter& r = *m_recitersList.at(reciterIdx); + QString url = r.baseUrl(); + if (r.useId()) url.append(QString::number(m_dbMgr->getVerseId(surah, verse)) + ".mp3"); else url.append(QString::number(surah).rightJustified(3, '0') + diff --git a/src/utils/downloadmanager.h b/src/utils/downloadmanager.h index 34e9e34c..c82c04df 100644 --- a/src/utils/downloadmanager.h +++ b/src/utils/downloadmanager.h @@ -6,7 +6,7 @@ #ifndef DOWNLOADMANAGER_H #define DOWNLOADMANAGER_H -#include "../globals.h" +#include "../types/reciter.h" #include "dbmanager.h" #include #include @@ -27,6 +27,12 @@ class DownloadManager : public QObject { Q_OBJECT public: + enum DownloadType + { + QCF, + Recitation, + File + }; /** * @brief DownloadTask struct represents a single verse file download task * @details downloads are separated into 3 different types @@ -185,11 +191,12 @@ public slots: void filesFound(DownloadType type, const QList& metainfo); private: - const QDir& m_downloadsDir = Globals::downloadsDir; - const QList& m_recitersList = Globals::recitersList; - const QList& m_tafasirList = Globals::tafasirList; - const QList& m_trList = Globals::translationsList; - DBManager* m_dbMgr = DBManager::instance(); + QSharedPointer m_downloadsDir = DirManager::downloadsDir; + QSharedPointer m_dbMgr = DBManager::current(); + const QList>& m_recitersList = Reciter::reciters; + const QList>& m_tafasirList = Tafsir::tafasir; + const QList>& m_trList = + Translation::translations; /** * @brief generate download url for specified verse using the reciter download * url diff --git a/src/utils/fontmanager.cpp b/src/utils/fontmanager.cpp new file mode 100644 index 00000000..c7f98ffb --- /dev/null +++ b/src/utils/fontmanager.cpp @@ -0,0 +1,75 @@ +#include "fontmanager.h" +#include "dirmanager.h" +#include +#include + +QString FontManager::qcfFontPrefix = "QCF_P"; + +void +FontManager::loadQcf() +{ + QFontDatabase::addApplicationFont( + DirManager::fontsDir->filePath("QCFV1/QCF_BSML.ttf")); + switch (Settings::qcfVersion) { + case 1: + DirManager::fontsDir->cd("QCFV1"); + break; + case 2: + DirManager::fontsDir->setPath(DirManager::downloadsDir->absolutePath() + + "/QCFV2"); + qcfFontPrefix = "QCF2"; + break; + } + + // add required fonts + for (int i = 1; i < 605; i++) { + QString fontName = pageFontname(i) + ".ttf"; + + if (Settings::qcfVersion == 2 && !DirManager::fontsDir->exists(fontName)) { + Settings::settings->setValue("Reader/QCF", 1); + Settings::settings->sync(); + qFatal() << DirManager::fontsDir->filePath(fontName) + << " font file not found, fallback to QCF v1"; + } else + QFontDatabase::addApplicationFont( + DirManager::fontsDir->filePath(fontName)); + } +} + +void +FontManager::loadUiFonts() +{ + // ui fonts + foreach (const QFileInfo& font, + DirManager::fontsDir->entryInfoList(QDir::Files)) + QFontDatabase::addApplicationFont(font.absoluteFilePath()); + // set default UI fonts to use + QStringList uiFonts; + uiFonts << "Noto Sans Display" + << "Expo Arabic"; + qApp->setFont(QFont(uiFonts, qApp->font().pointSize())); +} + +QString +FontManager::pageFontname(int page) +{ + return qcfFontPrefix + QString::number(page).rightJustified(3, '0'); +} + +QString +FontManager::verseFontname(Settings::VerseType type, int page) +{ + QString fontname; + switch (type) { + case Settings::qcf: + fontname = pageFontname(page); + break; + case Settings::uthmanic: + fontname = "kfgqpc_hafs_uthmanic _script"; + break; + case Settings::annotated: + fontname = "Emine"; + break; + } + return fontname; +} diff --git a/src/utils/fontmanager.h b/src/utils/fontmanager.h new file mode 100644 index 00000000..1078394a --- /dev/null +++ b/src/utils/fontmanager.h @@ -0,0 +1,17 @@ +#ifndef FONTMANAGER_H +#define FONTMANAGER_H + +#include "settings.h" +#include + +class FontManager +{ +public: + static QString qcfFontPrefix; + static QString pageFontname(int page); + static QString verseFontname(Settings::VerseType type, int page); + static void loadQcf(); + static void loadUiFonts(); +}; + +#endif // FONTMANAGER_H diff --git a/src/utils/settings.cpp b/src/utils/settings.cpp new file mode 100644 index 00000000..ce8a177e --- /dev/null +++ b/src/utils/settings.cpp @@ -0,0 +1,90 @@ +#include "settings.h" +#include "../utils/dirmanager.h" +#include +#include +#include +#include + +int Settings::themeId = 0; +bool Settings::darkMode = false; +int Settings::qcfVersion = 1; +Settings::ReaderMode Settings::readerMode; +QLocale::Language Settings::language; +QSharedPointer Settings::settings; + +void +Settings::setup() +{ + settings = QSharedPointer::create( + DirManager::configDir->filePath("qurancompanion.conf"), + QSettings::IniFormat); + + themeId = settings->value("Theme").toInt(); + qcfVersion = settings->value("Reader/QCF").toInt(); + language = qvariant_cast(settings->value("Language")); + readerMode = qvariant_cast(settings->value("Reader/Mode")); + + checkGroups(); +} + +void +Settings::checkGroups() +{ + for (int i = 0; i < 2; i++) + checkConfGroup(i); +} + +void +Settings::checkConfGroup(int gId) +{ + switch (gId) { + case 0: + settings->setValue("Language", + settings->value("Language", (int)QLocale::English)); + settings->setValue("Theme", settings->value("Theme", 0)); + settings->setValue("VOTD", settings->value("VOTD", true)); + settings->setValue("MissingFileWarning", + settings->value("MissingFileWarning", true)); + break; + case 1: + settings->beginGroup("Reader"); + settings->setValue("Mode", settings->value("Mode", 0)); + settings->setValue("FGHighlight", settings->value("FGHighlight", 1)); + settings->setValue("Khatmah", settings->value("Khatmah", 0)); + settings->setValue("AdaptiveFont", settings->value("AdaptiveFont", true)); + settings->setValue("QCF1Size", settings->value("QCF1Size", 22)); + settings->setValue("QCF2Size", settings->value("QCF2Size", 20)); + settings->setValue("QCF", settings->value("QCF", 1)); + settings->setValue("VerseType", settings->value("VerseType", 0)); + settings->setValue("VerseFontSize", settings->value("VerseFontSize", 20)); + settings->setValue("Tafsir", settings->value("Tafsir", 6)); + settings->setValue("Translation", settings->value("Translation", 5)); + settings->setValue( + "SideContentFont", + settings->value("SideContentFont", QFont("Expo Arabic", 14))); + settings->endGroup(); + break; + } +} + +void +Settings::loadQmFiles() +{ + if (language == QLocale::English) + return; + + QString code = QLocale::languageToCode(language); + QTranslator *translation = new QTranslator(qApp), + *qtBase = new QTranslator(qApp); + + if (translation->load(":/i18n/qc_" + code + ".qm")) { + qInfo() << translation->language() << "translation loaded"; + qInfo() << "base translation:" << qtBase->load(":/base/" + code + ".qm"); + qApp->installTranslator(translation); + qApp->installTranslator(qtBase); + } else { + qWarning() << code + " translation not loaded!"; + delete translation; + delete qtBase; + } +} diff --git a/src/utils/settings.h b/src/utils/settings.h new file mode 100644 index 00000000..644ee116 --- /dev/null +++ b/src/utils/settings.h @@ -0,0 +1,41 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include +#include +#include + +class Settings +{ +public: + enum VerseType + { + qcf, + uthmanic, + annotated + }; + /** + * @brief ReaderMode enum represents the available modes for the Quran reader + * in MainWindow + */ + enum ReaderMode + { + SinglePage, ///< Single Quran page, side panel is used for displaying verses + ///< with translation + DoublePage ///< Two Quran pages, both panels are used to display Quran + ///< pages, no translation + }; + + static int themeId; + static bool darkMode; + static int qcfVersion; + static QLocale::Language language; + static QSharedPointer settings; + static ReaderMode readerMode; + static void setup(); + static void checkGroups(); + static void checkConfGroup(int gId); + static void loadQmFiles(); +}; + +#endif // SETTINGS_H diff --git a/src/utils/shortcuthandler.cpp b/src/utils/shortcuthandler.cpp index cea724ea..53defb65 100644 --- a/src/utils/shortcuthandler.cpp +++ b/src/utils/shortcuthandler.cpp @@ -4,14 +4,63 @@ */ #include "shortcuthandler.h" +#include "../utils/settings.h" +#include +#include +#include + +QMap ShortcutHandler::shortcutsDescription; + +void +ShortcutHandler::populateDescriptionMap() +{ + QFile shortcuts(":/resources/shortcuts.xml"); + if (!shortcuts.open(QIODevice::ReadOnly)) + qCritical("Couldn't Open Shortcuts XML"); + + Settings::settings->beginGroup("Shortcuts"); + QXmlStreamReader reader(&shortcuts); + while (!reader.atEnd() && !reader.hasError()) { + QXmlStreamReader::TokenType token = reader.readNext(); + if (token == QXmlStreamReader::StartElement) { + if (reader.name().toString() == "shortcut") { + QString key = reader.attributes().value("key").toString(); + QString defBind = reader.attributes().value("default").toString(); + QString desc = + qApp->translate("SettingsDialog", + reader.attributes().value("description").toLatin1()); + + shortcutsDescription.insert(key, desc); + if (!Settings::settings->contains(key)) + Settings::settings->setValue(key, defBind); + } + } + } + + Settings::settings->endGroup(); + shortcuts.close(); +} + +QSharedPointer +ShortcutHandler::current() +{ + static QSharedPointer handler = + QSharedPointer::create(); + return handler; +} ShortcutHandler::ShortcutHandler(QObject* parent) : QObject(parent) { - foreach (const QString& key, m_shortcutsDescription.keys()) { - QKeySequence seq = - qvariant_cast(m_settings->value("Shortcuts/" + key)); - m_shortcuts.insert(key, new QShortcut(seq, parent)); +} + +void +ShortcutHandler::createShortcuts(QObject* context) +{ + foreach (const QString& key, shortcutsDescription.keys()) { + QKeySequence seq = qvariant_cast( + Settings::settings->value("Shortcuts/" + key)); + m_shortcuts.insert(key, new QShortcut(seq, context)); } m_shortcuts.value("TogglePlayback")->setContext(Qt::ApplicationShortcut); m_shortcuts.value("BookmarkCurrent")->setContext(Qt::ApplicationShortcut); @@ -131,5 +180,5 @@ void ShortcutHandler::shortcutChanged(QString key) { m_shortcuts.value(key)->setKey( - qvariant_cast(m_settings->value("Shortcuts/" + key))); + qvariant_cast(Settings::settings->value("Shortcuts/" + key))); } diff --git a/src/utils/shortcuthandler.h b/src/utils/shortcuthandler.h index e99595f0..7b6263a2 100644 --- a/src/utils/shortcuthandler.h +++ b/src/utils/shortcuthandler.h @@ -6,7 +6,7 @@ #ifndef SHORTCUTHANDLER_H #define SHORTCUTHANDLER_H -#include "../globals.h" +#include #include #include #include @@ -22,14 +22,16 @@ class ShortcutHandler : public QObject { Q_OBJECT public: + static QMap shortcutsDescription; + static void populateDescriptionMap(); + static QSharedPointer current(); /** * @brief class constructor * @param parent - pointer to parent widget that will recieve the shortcut * events */ explicit ShortcutHandler(QObject* parent = nullptr); - - void setContext(QWidget* newContext); + void createShortcuts(QObject* context); public slots: /** @@ -66,9 +68,6 @@ public slots: void openAdvancedCopy(); private: - const QSettings* m_settings = Globals::settings; - const QMap& m_shortcutsDescription = - Globals::shortcutDescription; /** * @brief connect different QShortcut signals to their * corresponding signal in ShortcutHandler diff --git a/src/utils/stylemanager.cpp b/src/utils/stylemanager.cpp new file mode 100644 index 00000000..372da4dd --- /dev/null +++ b/src/utils/stylemanager.cpp @@ -0,0 +1,80 @@ +#include "stylemanager.h" +#include "settings.h" +#include +#include +#include +#include + +QDir StyleManager::themeResources; +QSharedPointer StyleManager::awesome; + +void +StyleManager::loadTheme() +{ + qApp->setStyle(QStyleFactory::create("Fusion")); + + QPalette themeColors; + QFile styles, palette; + switch (Settings::themeId) { + case 0: + themeResources.setPath(":/resources/light/"); + styles.setFileName(themeResources.filePath("light.qss")); + palette.setFileName(themeResources.filePath("light.xml")); + Settings::darkMode = false; + break; + + case 1: + themeResources.setPath(":/resources/light/"); + styles.setFileName(themeResources.filePath("sepia.qss")); + palette.setFileName(themeResources.filePath("sepia.xml")); + Settings::darkMode = false; + break; + + case 2: + themeResources.setPath(":/resources/dark/"); + styles.setFileName(themeResources.filePath("dark.qss")); + palette.setFileName(themeResources.filePath("dark.xml")); + Settings::darkMode = true; + break; + } + + if (!palette.open(QIODevice::ReadOnly | QIODevice::Text)) { + qCritical() << "Couldn't Read Color palette xml"; + } + + QXmlStreamReader paletteReader(&palette); + QPalette::ColorGroup group = QPalette::All; + while (!paletteReader.atEnd() && !paletteReader.hasError()) { + QXmlStreamReader::TokenType token = paletteReader.readNext(); + if (token == QXmlStreamReader::StartElement) { + if (paletteReader.name().toString() == "enabled") + group = QPalette::All; + else if (paletteReader.name().toString() == "disabled") + group = QPalette::Disabled; + // color element + else { + QPalette::ColorRole role; + int red, green, blue; + role = static_cast( + paletteReader.attributes().value("role").toInt()); + red = paletteReader.attributes().value("red").toInt(); + green = paletteReader.attributes().value("green").toInt(); + blue = paletteReader.attributes().value("blue").toInt(); + + themeColors.setColor(group, role, QColor(red, green, blue)); + } + } + } + + palette.close(); + qApp->setPalette(themeColors); + + // load stylesheet + if (styles.open(QIODevice::ReadOnly)) { + qApp->setStyleSheet(styles.readAll()); + styles.close(); + } + + awesome = QSharedPointer::create(); + awesome->initFontAwesome(); +} diff --git a/src/utils/stylemanager.h b/src/utils/stylemanager.h new file mode 100644 index 00000000..3ffa8064 --- /dev/null +++ b/src/utils/stylemanager.h @@ -0,0 +1,16 @@ +#ifndef STYLEMANAGER_H +#define STYLEMANAGER_H + +#include +#include +#include + +class StyleManager +{ +public: + static QSharedPointer awesome; + static QDir themeResources; + static void loadTheme(); +}; + +#endif // STYLEMANAGER_H diff --git a/src/utils/systemtray.h b/src/utils/systemtray.h index f816e8f8..3c35e4ec 100644 --- a/src/utils/systemtray.h +++ b/src/utils/systemtray.h @@ -6,7 +6,6 @@ #ifndef SYSTEMTRAY_H #define SYSTEMTRAY_H -#include "../globals.h" #include "dbmanager.h" #include #include @@ -30,8 +29,8 @@ class SystemTray : public QObject * @brief Class constructor * @param parent - pointer to parent widget */ - explicit SystemTray(QObject* parent = nullptr); - ~SystemTray(); + explicit SystemTray(QObject* parent = nullptr); + ~SystemTray(); /** * @brief send a desktop notification message @@ -84,7 +83,7 @@ class SystemTray : public QObject void openAbout(); private: - DBManager* m_dbMgr = DBManager::instance(); + QSharedPointer m_dbMgr = DBManager::current(); /** * @brief adds system tray actions and set their connections */ diff --git a/src/utils/verseplayer.cpp b/src/utils/verseplayer.cpp index 8de5850e..98c5cda0 100644 --- a/src/utils/verseplayer.cpp +++ b/src/utils/verseplayer.cpp @@ -12,7 +12,7 @@ VersePlayer::VersePlayer(QObject* parent, int reciterIdx) { setAudioOutput(m_output); - m_reciterDir.cd(m_recitersList.at(m_reciter).baseDirName); + m_reciterDir.cd(m_recitersList.at(m_reciter)->baseDirName()); loadActiveVerse(); } @@ -84,7 +84,7 @@ VersePlayer::changeReciter(int reciterIdx) stop(); if (reciterIdx != m_reciter) { m_reciterDir.cdUp(); - m_reciterDir.cd(m_recitersList.at(reciterIdx).baseDirName); + m_reciterDir.cd(m_recitersList.at(reciterIdx)->baseDirName()); m_reciter = reciterIdx; } @@ -111,7 +111,7 @@ bool VersePlayer::loadActiveVerse() { if (m_activeVerse->number() == 0) { - setSource(QUrl::fromLocalFile(m_recitersList.at(m_reciter).basmallahPath)); + setSource(QUrl::fromLocalFile(m_recitersList.at(m_reciter)->basmallahPath())); return true; } @@ -121,7 +121,7 @@ VersePlayer::loadActiveVerse() QString VersePlayer::reciterName() const { - return m_recitersList.at(m_reciter).displayName; + return m_recitersList.at(m_reciter)->displayName(); } QAudioOutput* diff --git a/src/utils/verseplayer.h b/src/utils/verseplayer.h index aaaa8a76..0ae68aa3 100644 --- a/src/utils/verseplayer.h +++ b/src/utils/verseplayer.h @@ -6,9 +6,9 @@ #ifndef VERSEPLAYER_H #define VERSEPLAYER_H -#include "../globals.h" +#include "../types/reciter.h" +#include "../types/verse.h" #include "dbmanager.h" -#include "verse.h" #include #include #include @@ -117,9 +117,9 @@ public slots: private: Verse* m_activeVerse = Verse::current(); - QDir m_reciterDir = Globals::downloadsDir.absoluteFilePath("recitations"); - const QList& m_recitersList = Globals::recitersList; - DBManager* m_dbMgr = DBManager::instance(); + QSharedPointer m_dbMgr = DBManager::current(); + QDir m_reciterDir = DirManager::downloadsDir->absoluteFilePath("recitations"); + const QList>& m_recitersList = Reciter::reciters; /** * @brief boolean indicating whether the player is on or off, 'on' implies * that playback should continue in case of verse change diff --git a/src/widgets/aboutdialog.h b/src/widgets/aboutdialog.h index 14cba813..3af4363d 100644 --- a/src/widgets/aboutdialog.h +++ b/src/widgets/aboutdialog.h @@ -1,7 +1,7 @@ #ifndef ABOUTDIALOG_H #define ABOUTDIALOG_H -#include "../globals.h" +#include "../utils/settings.h" #include namespace Ui { @@ -18,7 +18,7 @@ class AboutDialog : public QDialog private: Ui::AboutDialog* ui; - QLocale::Language m_lang = Globals::language; + QLocale::Language m_lang = Settings::language; }; #endif // ABOUTDIALOG_H diff --git a/src/widgets/betaqaviewer.cpp b/src/widgets/betaqaviewer.cpp index 2c54e910..a7d08c32 100644 --- a/src/widgets/betaqaviewer.cpp +++ b/src/widgets/betaqaviewer.cpp @@ -51,7 +51,7 @@ BetaqaViewer::showSurah(int surah) void BetaqaViewer::center() { - int w = std::max((int)(parentWidget()->width() * 0.4), 600); + int w = std::max((int)(parentWidget()->width() * 0.6), 600); int h = std::max((int)(parentWidget()->height() * 0.8), 600); this->resize(w, h); m_sizeAnim->setEndValue(size()); diff --git a/src/widgets/betaqaviewer.h b/src/widgets/betaqaviewer.h index 7337c85d..5511acca 100644 --- a/src/widgets/betaqaviewer.h +++ b/src/widgets/betaqaviewer.h @@ -1,7 +1,6 @@ #ifndef BETAQAVIEWER_H #define BETAQAVIEWER_H -#include "../globals.h" #include "../utils/dbmanager.h" #include #include @@ -20,15 +19,18 @@ class BetaqaViewer : public QWidget explicit BetaqaViewer(QWidget* parent = nullptr); ~BetaqaViewer(); - void showSurah(int surah); void center(); +public slots: + void showSurah(int surah); + protected: void focusOutEvent(QFocusEvent* event); private: + QSharedPointer m_dbMgr = DBManager::current(); Ui::BetaqaViewer* ui; - DBManager* m_dbMgr = DBManager::instance(); + int m_surah = -1; QGraphicsDropShadowEffect* m_shadowEffect = nullptr; QPropertyAnimation* m_sizeAnim = nullptr; diff --git a/src/widgets/downloadprogressbar.cpp b/src/widgets/downloadprogressbar.cpp index 5a7916e8..70b78643 100644 --- a/src/widgets/downloadprogressbar.cpp +++ b/src/widgets/downloadprogressbar.cpp @@ -14,7 +14,7 @@ DownloadProgressBar::DownloadProgressBar(QWidget* parent, setStyling(downloading); setMaximum(max); setValue(0); - if (type == File) + if (type == DownloadManager::File) setFormat("%v / %m " + qApp->translate("DownloadManager", "KB")); else setFormat("%v / %m"); diff --git a/src/widgets/downloadprogressbar.h b/src/widgets/downloadprogressbar.h index f785a87e..26b7abf3 100644 --- a/src/widgets/downloadprogressbar.h +++ b/src/widgets/downloadprogressbar.h @@ -6,8 +6,9 @@ #ifndef DOWNLOADPROGRESSBAR_H #define DOWNLOADPROGRESSBAR_H -#include "../globals.h" +#include "../utils/downloadmanager.h" #include +typedef DownloadManager::DownloadType DownloadType; /** * @brief DownloadProgressBar class is a modified QProgressBar to change its diff --git a/src/widgets/notificationpopup.cpp b/src/widgets/notificationpopup.cpp index a5c9846e..08feba92 100644 --- a/src/widgets/notificationpopup.cpp +++ b/src/widgets/notificationpopup.cpp @@ -4,6 +4,8 @@ */ #include "notificationpopup.h" +#include "../utils/stylemanager.h" +#include using namespace fa; NotificationPopup::NotificationPopup(QWidget* parent) @@ -85,14 +87,14 @@ NotificationPopup::completedDownload(DownloadType type, { setStyleSheet(""); QString msg = tr("Download Completed") + ": "; - if (type == Recitation) - msg += m_recitersList.at(metainfo[0]).displayName + " - " + + if (type == DownloadManager::Recitation) + msg += m_recitersList.at(metainfo[0])->displayName() + " - " + m_dbMgr->surahNameList().at(metainfo[1] - 1); - else if (type == QCF) + else if (type == DownloadManager::QCF) msg += tr("QCF V2"); - else if (type == File) - msg += metainfo[0] ? m_trList.at(metainfo[1]).displayName - : m_tafasirList.at(metainfo[1]).displayName; + else if (type == DownloadManager::File) + msg += metainfo[0] ? m_trList.at(metainfo[1])->displayName() + : m_tafasirList.at(metainfo[1])->displayName(); this->notify(msg, success); } @@ -102,14 +104,14 @@ NotificationPopup::downloadError(DownloadType type, const QList& metainfo) { setStyleSheet("QFrame#Popup { background-color: #a50500 }"); QString msg = tr("Download Failed") + ": "; - if (type == Recitation) - msg += m_recitersList.at(metainfo[0]).displayName + " - " + + if (type == DownloadManager::Recitation) + msg += m_recitersList.at(metainfo[0])->displayName() + " - " + m_dbMgr->surahNameList().at(metainfo[1] - 1); - else if (type == QCF) + else if (type == DownloadManager::QCF) msg += tr("QCF V2"); - else if (type == File) - msg += metainfo[0] ? m_trList.at(metainfo[1]).displayName - : m_tafasirList.at(metainfo[1]).displayName; + else if (type == DownloadManager::File) + msg += metainfo[0] ? m_trList.at(metainfo[1])->displayName() + : m_tafasirList.at(metainfo[1])->displayName(); this->notify(msg, fail); } @@ -202,7 +204,7 @@ NotificationPopup::setNotificationIcon(Action icon) break; } - m_iconWidget->setFont(Globals::awesome->font(faStyle, 18)); + m_iconWidget->setFont(StyleManager::awesome->font(faStyle, 18)); m_iconWidget->setText(ico); } diff --git a/src/widgets/notificationpopup.h b/src/widgets/notificationpopup.h index 9c0848b1..e7a1b587 100644 --- a/src/widgets/notificationpopup.h +++ b/src/widgets/notificationpopup.h @@ -6,8 +6,8 @@ #ifndef NOTIFICATIONPOPUP_H #define NOTIFICATIONPOPUP_H -#include "../globals.h" #include "../utils/dbmanager.h" +#include "../utils/downloadmanager.h" #include #include #include @@ -17,6 +17,7 @@ #include #include #include +typedef DownloadManager::DownloadType DownloadType; /** * @brief NotificationPopup class represents an in-app popup for notifying the @@ -104,10 +105,10 @@ public slots: void checkUpdate(QString appVer); private: - const QList& m_recitersList = Globals::recitersList; - const QList& m_tafasirList = Globals::tafasirList; - const QList& m_trList = Globals::translationsList; - DBManager* m_dbMgr = DBManager::instance(); + QSharedPointer m_dbMgr = DBManager::current(); + QList>& m_recitersList = Reciter::reciters; + QList>& m_tafasirList = Tafsir::tafasir; + QList>& m_trList = Translation::translations; /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/widgets/quranpagebrowser.cpp b/src/widgets/quranpagebrowser.cpp index 2041e55a..813d966d 100644 --- a/src/widgets/quranpagebrowser.cpp +++ b/src/widgets/quranpagebrowser.cpp @@ -4,8 +4,11 @@ */ #include "quranpagebrowser.h" +#include "../utils/fontmanager.h" +#include "../utils/stylemanager.h" #include #include +#include using namespace fa; QuranPageBrowser::QuranPageBrowser(QWidget* parent, int initPage) @@ -19,7 +22,7 @@ QuranPageBrowser::QuranPageBrowser(QWidget* parent, int initPage) createActions(); updateFontSize(); - m_pageFont = Globals::pageFontname(initPage); + m_pageFont = FontManager::pageFontname(initPage); m_pageFormat.setAlignment(Qt::AlignCenter); m_pageFormat.setNonBreakableLines(true); m_pageFormat.setLayoutDirection(Qt::RightToLeft); @@ -106,7 +109,7 @@ QuranPageBrowser::surahFrame(int surah) p.setFont(QFont("QCF_BSML", 77)); p.drawText(baseImage.rect(), Qt::AlignCenter, frmText); - if (m_darkMode) + if (Settings::darkMode) baseImage.invertPixels(); return baseImage; @@ -154,7 +157,7 @@ QuranPageBrowser::constructPage(int pageNo, bool forceCustomSize) m_verseCoordinates.clear(); this->document()->clear(); - m_pageFont = Globals::pageFontname(pageNo); + m_pageFont = FontManager::pageFontname(pageNo); QTextCursor textCursor(this->document()); m_currPageHeader = this->pageHeader(m_page); @@ -209,7 +212,7 @@ QuranPageBrowser::constructPage(int pageNo, bool forceCustomSize) prevAnchor += 2; } else if (l.contains("bsml")) { QImage bsml(":/resources/basmalah.png"); - if (m_darkMode) + if (Settings::darkMode) bsml.invertPixels(); textCursor.insertBlock(m_pageFormat, m_bodyTextFormat); @@ -279,7 +282,7 @@ QuranPageBrowser::resetHighlight() { QTextCharFormat tcf; if (m_fgHighlight) - tcf.setForeground(m_darkMode ? Qt::white : Qt::black); + tcf.setForeground(Settings::darkMode ? Qt::white : Qt::black); else tcf.setBackground(Qt::transparent); @@ -358,14 +361,17 @@ QuranPageBrowser::createActions() m_tafsirAct = new QAction(tr("Tafsir"), this); m_actAddBookmark = new QAction(tr("Add Bookmark"), this); m_actRemBookmark = new QAction(tr("Remove Bookmark"), this); - m_zoomIn->setIcon(m_fa->icon(fa_solid, fa_magnifying_glass_plus)); - m_zoomOut->setIcon(m_fa->icon(fa_solid, fa_magnifying_glass_minus)); - m_playAct->setIcon(m_fa->icon(fa_solid, fa_play)); - m_selectAct->setIcon(m_fa->icon(fa_solid, fa_hand_pointer)); - m_tafsirAct->setIcon(m_fa->icon(fa_solid, fa_book_open)); - m_copyAct->setIcon(m_fa->icon(fa_solid, fa_clipboard)); - m_actAddBookmark->setIcon(m_fa->icon(fa_regular, fa_bookmark)); - m_actRemBookmark->setIcon(m_fa->icon(fa_solid, fa_bookmark)); + m_zoomIn->setIcon( + StyleManager::awesome->icon(fa_solid, fa_magnifying_glass_plus)); + m_zoomOut->setIcon( + StyleManager::awesome->icon(fa_solid, fa_magnifying_glass_minus)); + m_playAct->setIcon(StyleManager::awesome->icon(fa_solid, fa_play)); + m_selectAct->setIcon(StyleManager::awesome->icon(fa_solid, fa_hand_pointer)); + m_tafsirAct->setIcon(StyleManager::awesome->icon(fa_solid, fa_book_open)); + m_copyAct->setIcon(StyleManager::awesome->icon(fa_solid, fa_clipboard)); + m_actAddBookmark->setIcon( + StyleManager::awesome->icon(fa_regular, fa_bookmark)); + m_actRemBookmark->setIcon(StyleManager::awesome->icon(fa_solid, fa_bookmark)); connect(m_zoomIn, &QAction::triggered, this, &QuranPageBrowser::actionZoomIn); connect( m_zoomOut, &QAction::triggered, this, &QuranPageBrowser::actionZoomOut); diff --git a/src/widgets/quranpagebrowser.h b/src/widgets/quranpagebrowser.h index 1864da37..bab3c1a4 100644 --- a/src/widgets/quranpagebrowser.h +++ b/src/widgets/quranpagebrowser.h @@ -6,7 +6,6 @@ #ifndef QURANPAGEBROWSER_H #define QURANPAGEBROWSER_H -#include "../globals.h" #include "../utils/dbmanager.h" #include #include @@ -48,7 +47,6 @@ class QuranPageBrowser : public QTextBrowser * @param initPage - inital page to load */ QuranPageBrowser(QWidget* parent = nullptr, int initPage = 1); - /** * @brief sets m_fontSize to the fontsize in the settings file */ @@ -142,11 +140,9 @@ public slots: #endif private: - QSettings* const m_settings = Globals::settings; - const int m_qcfVer = Globals::qcfVersion; - const bool m_darkMode = Globals::darkMode; - DBManager* m_dbMgr = DBManager::instance(); - fa::QtAwesome* m_fa = Globals::awesome; + const int m_qcfVer = Settings::qcfVersion; + QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer const m_settings = Settings::settings; /** * @brief utility for creating menu actions for interacting with the widget */ diff --git a/src/widgets/versedialog.h b/src/widgets/versedialog.h index 1e91112f..de8e6cbc 100644 --- a/src/widgets/versedialog.h +++ b/src/widgets/versedialog.h @@ -1,9 +1,9 @@ #ifndef VERSEDIALOG_H #define VERSEDIALOG_H -#include "../globals.h" +#include "../types/verse.h" #include "../utils/dbmanager.h" -#include "../utils/verse.h" +#include "../utils/dirmanager.h" #include #include @@ -35,10 +35,9 @@ public slots: private: Ui::VerseDialog* ui; - const QSettings* m_settings = Globals::settings; - DBManager* m_dbMgr = DBManager::instance(); - QFile m_timestampFile = Globals::configDir.absoluteFilePath("votd.log"); - fa::QtAwesome* m_fa = Globals::awesome; + QSharedPointer m_dbMgr = DBManager::current(); + const QSharedPointer m_settings = Settings::settings; + QFile m_timestampFile = DirManager::configDir->absoluteFilePath("votd.log"); /** * @brief generate the verse of the day and set the votd html */ From 56ab4568019b2a8c720644288e67f45c38564cdd Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Fri, 16 Feb 2024 19:58:38 +0200 Subject: [PATCH 05/51] New Crowdin updates (#45) * Update Arabic translation * Update Turkish translation * Update Arabic translation --- dist/translations/qc_ar.ts | 275 +++++++++++++++++------------------ dist/translations/qc_tr.ts | 289 +++++++++++++++++++------------------ 2 files changed, 283 insertions(+), 281 deletions(-) diff --git a/dist/translations/qc_ar.ts b/dist/translations/qc_ar.ts index 6d3f3864..00da3fb2 100644 --- a/dist/translations/qc_ar.ts +++ b/dist/translations/qc_ar.ts @@ -558,216 +558,196 @@ - - + + Quran Companion رفيق القرآن - - Reciter - القارئ - - - - next - التالي - - - - previous - السابق - - - + View عرض - + Edit تحرير - + File ملف - + Help مساعدة - - + + Navigation التصفح - + Juz الجزء - + Page الصفحة - + Verse الآية - + Search surah بحث السور - + Preferences الإعدادات - + Download manager مدير التحميلات - + Exit خروج - + Find البحث - + Check for updates التحقق من وجود تحديثات - + Bookmarks العلامات - + About Quran Companion عن رفيق القرآن - - + + About Qt عن كيوت - + Tafsir التفسير - + Verse of the day آية اليوم - + Khatmah الختمات - + Advanced copy النسخ المتقدم - + Toggle reader view تبديل طريقة عرض القارئ - + Khatmah ختمه - + Default افتراضي - + There are currently no updates available. لا يوجد تحديثات متوفرة حاليا. - - - + + + Update info معلومات التحديث - + Updates available, do you want to open the update tool? هناك تحديثات متاحة، هل تود تشغيل مدير التحديثات؟ - + Updates info معلومات التحديث - + Updates are available, use the maintainance tool to install the latest updates. التحديثات متاحة، استخدم أداة الصيانة لتثبيت أحدث التحديثات. - + Now playing: يقرأ الآن: - + Surah سورة - + Recitation not found التلاوة غير موجودة - + The recitation files for the current surah is missing, would you like to download it? ملفات التلاوة الخاصة بالسورة الحالية غير متوفرة، هل تود الذَّهاب إلى صفحة التحميل؟ - - + + Files Missing الملفات مفقودة - + The selected font files are missing, would you like to download it? ملفات الخط المحدد مفقودة، هل ترغب في تحميلها؟ - + The selected tafsir is missing, would you like to download it? التفسير المحدد مفقود، هل ترغب في تحميله؟ - - - Verse Of The Day - آية اليوم - AboutDialog @@ -777,82 +757,82 @@ عن رفيق القرآن - + Quran Companion رفيق القرآن - + Version إصدار - + About عن البرنامَج - + A free, open-source Quran reader & player قارئ ومشغل للقرآن الكريم مجاني و مفتوح المصدر - + Useful Links روابط مفيدة - + Translators المترجمون - + Credits شكر وتقدير - + Recitations التلاوات - + Tafsir/Translations التفسير/الترجمات - + Surah Cards بطاقات السور - + Libraries المكتبات - + Licensed under the مرخص بموجب - + Waqf General Public License رخصة وقف العامة - + Project Homepage صفحة المشروع - + Report a bug/Request a feature التبليغ عن خلل / طلب ميزة - + Contribute to translations ساهم في الترجمة @@ -901,13 +881,13 @@ - + Surah: سورة: - + Verse: آية: @@ -940,12 +920,12 @@ إلى - + Invalid range نطاق غير صالح - + The entered verse range is invalid نطاق الآيات الذي تم إدخاله غير صالح @@ -1032,12 +1012,12 @@ - + Download Completed تم التحميل - + Download Failed فشل التحميل @@ -1065,69 +1045,31 @@ إنشاء ختمة جديدة - + Set as active تفعيل - + Remove إزالة - + Surah: سورة: - + Verse: آية: - + Khatmah ختمه - - NotificationManager - - - Play/Pause recitation - تشغيل/إيقاف التلاوة - - - - Show window - إظهار النافذة - - - - Hide window - إخفاء النافذة - - - - Preferences - الإعدادات - - - - Check for updates - التحقق من وجود تحديثات - - - - About - عن البرنامَج - - - - Exit - خروج - - NotificationPopup @@ -1172,6 +1114,14 @@ تحديث متاح + + PlayerControls + + + Reciter + القارئ + + QuranPageBrowser @@ -1215,6 +1165,19 @@ إزالة العلامة + + QuranReader + + + next + التالي + + + + previous + السابق + + SearchDialog @@ -1288,17 +1251,17 @@ السُور - + Search results ناتج بحث - + Surah: سورة: - + Verse: آية: @@ -1316,6 +1279,44 @@ تسلسل المفتاح المختار محجوزة فعلًا، حاول مرة أخرى + + SystemTray + + + Play/Pause recitation + تشغيل/إيقاف التلاوة + + + + Show window + إظهار النافذة + + + + Hide window + إخفاء النافذة + + + + Preferences + الإعدادات + + + + Check for updates + التحقق من وجود تحديثات + + + + About + عن البرنامَج + + + + Exit + خروج + + TafsirDialog @@ -1324,32 +1325,32 @@ التفسير - + next التالي - + Left يسار - + previous السابق - + Right يمين - + Surah: سورة: - + Verse: آية: diff --git a/dist/translations/qc_tr.ts b/dist/translations/qc_tr.ts index a111af16..9af011aa 100644 --- a/dist/translations/qc_tr.ts +++ b/dist/translations/qc_tr.ts @@ -558,216 +558,196 @@ - - + + Quran Companion Kura-an'ı Kerim Arkadaşı - - Reciter - Okuyucu - - - - next - sonraki - - - - previous - önceki - - - + View Görünüm - + Edit Düzenle - + File Dosya - + Help Yardım - - + + Navigation Gezinme - + Juz Cüz - + Page Sayfa - + Verse Ayet - + Search surah Sure ara - + Preferences Ayarlar - + Download manager İndirme yöneticisi - + Exit Çıkış yap - + Find Bul - + Check for updates Güncellemeleri kontrol et - + Bookmarks Yer işaretleri - + About Quran Companion Kura-an'ı Kerim Arkadaşı hakkında - - + + About Qt Qt Hakkında - + Tafsir Tefsir - + Verse of the day Günün Ayeti - + Khatmah Hatim - + Advanced copy Gelişmiş kopyalama - + Toggle reader view Toggle reader view - + Khatmah Hatim - + Default Varsayılan - + There are currently no updates available. Yeni bir güncelleme yok. - - - + + + Update info Güncelleme bilgisi - + Updates available, do you want to open the update tool? Güncellemeler mevcut, güncelleme aracını açmak ister misiniz? - + Updates info Güncelleme bilgisi - + Updates are available, use the maintainance tool to install the latest updates. Güncellemeler mevcuttur, en son güncellemeleri yüklemek için bakım aracını kullanın. - + Now playing: Şimdi oynatılıyor: - + Surah Sure - + Recitation not found Kıraat bulunamadı - + The recitation files for the current surah is missing, would you like to download it? Güncel Surenin kıraat dosyaları eksik, indirmek ister misiniz? - - + + Files Missing Files Missing - + The selected font files are missing, would you like to download it? The selected font files are missing, would you like to download it? - + The selected tafsir is missing, would you like to download it? The selected tafsir is missing, would you like to download it? - - - Verse Of The Day - Günün Ayeti - AboutDialog @@ -777,82 +757,82 @@ Kura-an'ı Kerim Arkadaşı hakkında - + Quran Companion Kura-an'ı Kerim Arkadaşı - + Version Version - + About - Hakkında + About - + A free, open-source Quran reader & player A free, open-source Quran reader & player - + Useful Links Useful Links - + Translators Translators - + Credits Credits - + Recitations Recitations - + Tafsir/Translations Tafsir/Translations - + Surah Cards Surah Cards - + Libraries Libraries - + Licensed under the Licensed under the - + Waqf General Public License Waqf General Public License - + Project Homepage Project Homepage - + Report a bug/Request a feature Report a bug/Request a feature - + Contribute to translations Contribute to translations @@ -867,7 +847,7 @@ next - sonraki + next @@ -877,7 +857,7 @@ previous - önceki + previous @@ -901,13 +881,13 @@ - + Surah: Sure: - + Verse: Ayet: @@ -940,12 +920,12 @@ Alıcı - + Invalid range Geçersiz aralık - + The entered verse range is invalid Girilen Ayet aralığı geçersiz @@ -1032,12 +1012,12 @@ /sn - + Download Completed İndirme tamamlandı - + Download Failed İndirme Başarısız Oldu @@ -1065,69 +1045,31 @@ Yeni hatime başla - + Set as active Etkin olarak ayarla - + Remove Kaldır - + Surah: Sure: - + Verse: Ayet: - + Khatmah Hatim - - NotificationManager - - - Play/Pause recitation - Okumayı Oynat/Duraklat - - - - Show window - Pencereyi göster - - - - Hide window - Pencereyi gizle - - - - Preferences - Ayarlar - - - - Check for updates - Güncellemeleri kontrol et - - - - About - Hakkında - - - - Exit - Çıkış yap - - NotificationPopup @@ -1172,6 +1114,14 @@ Güncelleme var + + PlayerControls + + + Reciter + Reciter + + QuranPageBrowser @@ -1215,6 +1165,19 @@ Yer imini kaldır + + QuranReader + + + next + next + + + + previous + previous + + SearchDialog @@ -1245,7 +1208,7 @@ next - sonraki + next @@ -1255,7 +1218,7 @@ previous - önceki + previous @@ -1288,17 +1251,17 @@ Sureler - + Search results Arama sonuçları - + Surah: Sure: - + Verse: Ayet: @@ -1316,6 +1279,44 @@ The key sequence chosen is already reserved, try again + + SystemTray + + + Play/Pause recitation + Play/Pause recitation + + + + Show window + Show window + + + + Hide window + Hide window + + + + Preferences + Ayarlar + + + + Check for updates + Güncellemeleri kontrol et + + + + About + About + + + + Exit + Çıkış yap + + TafsirDialog @@ -1324,32 +1325,32 @@ Tefsir - + next - sonraki + next - + Left Sol - + previous - önceki + previous - + Right Sağ - + Surah: Sure: - + Verse: Ayet: From 00d0362976a106ae044b5685c621abf3f844e9a3 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Fri, 16 Feb 2024 20:00:37 +0200 Subject: [PATCH 06/51] Refactoring: add global utility functions and use QSharedPointer --- src/core/copydialog.h | 2 +- src/core/khatmahdialog.h | 2 +- src/core/mainwindow.cpp | 2 +- src/core/mainwindow.h | 78 +++++++++++++++++++++++-------------- src/core/playercontrols.h | 2 +- src/core/quranreader.h | 2 +- src/core/settingsdialog.cpp | 16 +------- src/core/settingsdialog.h | 9 +---- src/core/tafsirdialog.cpp | 10 ++--- src/types/verse.cpp | 6 +-- src/types/verse.h | 4 +- src/utils/fontmanager.cpp | 13 +++++++ src/utils/fontmanager.h | 7 ++++ src/utils/verseplayer.cpp | 2 +- src/utils/verseplayer.h | 4 +- 15 files changed, 89 insertions(+), 70 deletions(-) diff --git a/src/core/copydialog.h b/src/core/copydialog.h index 9aa661a6..d63e79e7 100644 --- a/src/core/copydialog.h +++ b/src/core/copydialog.h @@ -38,7 +38,7 @@ public slots: private: Ui::CopyDialog* ui; - Verse* m_currVerse = Verse::current(); + QSharedPointer m_currVerse = Verse::current(); QSharedPointer m_dbMgr = DBManager::current(); void copyRange(); diff --git a/src/core/khatmahdialog.h b/src/core/khatmahdialog.h index 2f9be19a..cd90af14 100644 --- a/src/core/khatmahdialog.h +++ b/src/core/khatmahdialog.h @@ -72,7 +72,7 @@ private slots: void setActiveKhatmah(); private: - const Verse* m_currVerse = Verse::current(); + const QSharedPointer m_currVerse = Verse::current(); QSharedPointer m_dbMgr = DBManager::current(); QSharedPointer m_settings = Settings::settings; /** diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index aa6ef4f0..20a12609 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -15,7 +15,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_process(new QProcess(this)) - , m_shortcutHandler(ShortcutHandler::current()) + , m_verseValidator(new QIntValidator(this)) { ui->setupUi(this); loadIcons(); diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 2925ae93..777dfb3f 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -55,11 +55,21 @@ class MainWindow : public QMainWindow * @brief class constructor * @param parent - pointer to parent widget */ - explicit MainWindow(QWidget* parent = nullptr); + explicit MainWindow(QWidget* parent); ~MainWindow(); public slots: + /** + * @brief currentVerseChanged + * + * MODIFIED + */ void currentVerseChanged(); + /** + * @brief currentSurahChanged + * + * MODIFIED + */ void currentSurahChanged(); /** * @brief check if there are updates using the maintainence tool if available, @@ -243,8 +253,10 @@ private slots: private: Ui::MainWindow* ui; - Verse* m_currVerse = Verse::current(); + QSharedPointer m_currVerse = Verse::current(); QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_shortcutHandler = + ShortcutHandler::current(); QSharedPointer m_settings = Settings::settings; const QList>& m_reciters = Reciter::reciters; const QList>& m_tafasir = Tafsir::tafasir; @@ -320,82 +332,88 @@ private slots: * change of juz combobox index */ bool m_internalJuzChange = false; - QuranReader* m_reader = nullptr; - PlayerControls* m_playerControls = nullptr; /** - * @brief ShortcutHandler instance for handling shortcuts + * @brief QList for surah names as it appears in the navigation dock QListView + */ + QStringList m_surahList; + /** + * @brief model used with the navigation dock QListView to display complete + * list of surahs + */ + QStringListModel m_surahListModel; + /** + * @brief m_reader + * + * MODIFIED + */ + QPointer m_reader; + /** + * @brief m_playerControls + * + * MODIFIED */ - QSharedPointer m_shortcutHandler = nullptr; + QPointer m_playerControls; /** * @brief pointer to SystemTray instance */ - SystemTray* m_systemTray = nullptr; + QPointer m_systemTray; /** * @brief pointer to NotificationPopup instance */ - NotificationPopup* m_popup = nullptr; + QPointer m_popup; /** * @brief pointer to VersePlayer instance */ - VersePlayer* m_player = nullptr; + QPointer m_player; /** * @brief pointer to TafsirDialog instance */ - TafsirDialog* m_tafsirDlg = nullptr; + QPointer m_tafsirDlg; /** * @brief pointer to SearchDialog instance */ - SearchDialog* m_searchDlg = nullptr; + QPointer m_searchDlg; /** * @brief pointer to SettingsDialog instance */ - SettingsDialog* m_settingsDlg = nullptr; + QPointer m_settingsDlg; /** * @brief pointer to BookmarksDialog instance */ - BookmarksDialog* m_bookmarksDlg = nullptr; + QPointer m_bookmarksDlg; /** * @brief pointer to KhatmahDialog instance */ - KhatmahDialog* m_khatmahDlg = nullptr; + QPointer m_khatmahDlg; /** * @brief pointer to CopyDialog instance */ - CopyDialog* m_cpyDlg = nullptr; + QPointer m_cpyDlg; /** * @brief pointer to DownloaderDialog instance */ - DownloaderDialog* m_downloaderDlg = nullptr; + QPointer m_downloaderDlg; /** * @brief pointer to DownloadManager instance */ - DownloadManager* m_downManPtr = nullptr; + QPointer m_downManPtr; /** * @brief pointer to the surah card (betaqa) widget */ - BetaqaViewer* m_betaqaViewer = nullptr; + QPointer m_betaqaViewer; /** * @brief pointer to the votd dialog */ - VerseDialog* m_verseDlg = nullptr; + QPointer m_verseDlg; /** * @brief pointer to the QProcess instance of the maintainence tool that * checks for updates */ - QProcess* m_process = nullptr; + QPointer m_process; /** * @brief pointer to the validator for the editable verse combobox to ensure * the number entered is within the surah verse range */ - QIntValidator* m_verseValidator = new QIntValidator(this); - /** - * @brief QList for surah names as it appears in the navigation dock QListView - */ - QStringList m_surahList; - /** - * @brief model used with the navigation dock QListView to display complete - * list of surahs - */ - QStringListModel m_surahListModel; + QPointer m_verseValidator; }; #endif // MAINWINDOW_H diff --git a/src/core/playercontrols.h b/src/core/playercontrols.h index 9f9271ce..b32f0d47 100644 --- a/src/core/playercontrols.h +++ b/src/core/playercontrols.h @@ -78,7 +78,7 @@ private slots: private: Ui::PlayerControls* ui; - Verse* m_currVerse = Verse::current(); + QSharedPointer m_currVerse = Verse::current(); QSharedPointer m_dbMgr = DBManager::current(); QSharedPointer const m_settings = Settings::settings; const QList>& m_reciters = Reciter::reciters; diff --git a/src/core/quranreader.h b/src/core/quranreader.h index 571d8de6..8d9758e3 100644 --- a/src/core/quranreader.h +++ b/src/core/quranreader.h @@ -144,7 +144,7 @@ private slots: private: Ui::QuranReader* ui; - Verse* m_currVerse = Verse::current(); + QSharedPointer m_currVerse = Verse::current(); QSharedPointer m_dbMgr = DBManager::current(); QSharedPointer m_settings = Settings::settings; const QList>& m_recitersList = Reciter::reciters; diff --git a/src/core/settingsdialog.cpp b/src/core/settingsdialog.cpp index 7094c1ca..dbd02e81 100644 --- a/src/core/settingsdialog.cpp +++ b/src/core/settingsdialog.cpp @@ -4,6 +4,7 @@ */ #include "settingsdialog.h" +#include "../utils/fontmanager.h" #include "../utils/stylemanager.h" #include "../widgets/shortcutdelegate.h" #include "ui_settingsdialog.h" @@ -148,19 +149,6 @@ SettingsDialog::checkShortcuts() } } -bool -SettingsDialog::qcfExists() -{ - QString filename = "QCFV2/QCF2%0.ttf"; - for (int i = 1; i <= 604; i++) { - if (!m_downloadsDir.exists( - filename.arg(QString::number(i).rightJustified(3, '0')))) - return false; - } - - return true; -} - void SettingsDialog::updateTheme(int themeIdx) { @@ -235,7 +223,7 @@ SettingsDialog::updateReaderMode(int idx) void SettingsDialog::updateQuranFont(int qcfV) { - if (qcfV == 2 && !qcfExists()) { + if (qcfV == 2 && !FontManager::qcfExists()) { emit qcf2Missing(); ui->cmbQCF->setCurrentIndex(0); return; diff --git a/src/core/settingsdialog.h b/src/core/settingsdialog.h index ee381147..8b695b65 100644 --- a/src/core/settingsdialog.h +++ b/src/core/settingsdialog.h @@ -231,7 +231,7 @@ public slots: const QList>& m_trList = Translation::translations; const QMap& m_shortcutDescription = - ShortcutHandler::shortcutsDescription; + ShortcutHandler::shortcutsDescription; /** * @brief connects signals and slots for different UI * components and shortcuts. @@ -259,13 +259,6 @@ public slots: * @brief check if any shortcut was changed and updated it */ void checkShortcuts(); - /** - * @brief check if QCF2 font files exist - * - * @return true - all 604 QCF2 files are found - * @return false - one of the files is missing - */ - bool qcfExists(); /** * @brief QCF font size used in constructing Quran page. */ diff --git a/src/core/tafsirdialog.cpp b/src/core/tafsirdialog.cpp index 17308d04..85969e7e 100644 --- a/src/core/tafsirdialog.cpp +++ b/src/core/tafsirdialog.cpp @@ -117,16 +117,16 @@ TafsirDialog::setShownVerse(const Verse& newShownVerse) } void -TafsirDialog::closeEvent(QCloseEvent* event) +TafsirDialog::showEvent(QShowEvent* event) { - this->hide(); + updateContentComboBox(); + QDialog::showEvent(event); } void -TafsirDialog::showEvent(QShowEvent* event) +TafsirDialog::closeEvent(QCloseEvent* event) { - updateContentComboBox(); - QDialog::showEvent(event); + this->hide(); } TafsirDialog::~TafsirDialog() diff --git a/src/types/verse.cpp b/src/types/verse.cpp index 6bba7bef..3fa4f1a0 100644 --- a/src/types/verse.cpp +++ b/src/types/verse.cpp @@ -1,10 +1,10 @@ #include "verse.h" -Verse* +QSharedPointer Verse::current() { - static Verse m_current = Verse(); - return &m_current; + static QSharedPointer current = QSharedPointer::create(); + return current; } QList diff --git a/src/types/verse.h b/src/types/verse.h index 6177f2d9..aa215486 100644 --- a/src/types/verse.h +++ b/src/types/verse.h @@ -1,8 +1,8 @@ #ifndef VERSE_H #define VERSE_H -#include "../utils/settings.h" #include "../utils/dbmanager.h" +#include "../utils/settings.h" #include /** @@ -14,7 +14,7 @@ class Verse { public: - static Verse* current(); + static QSharedPointer current(); static QList fromList(QList> lst); Verse(); diff --git a/src/utils/fontmanager.cpp b/src/utils/fontmanager.cpp index c7f98ffb..69228b88 100644 --- a/src/utils/fontmanager.cpp +++ b/src/utils/fontmanager.cpp @@ -73,3 +73,16 @@ FontManager::verseFontname(Settings::VerseType type, int page) } return fontname; } + +bool +FontManager::qcfExists() +{ + QString filename = "QCFV2/QCF2%0.ttf"; + for (int i = 1; i <= 604; i++) { + if (!DirManager::downloadsDir->exists( + filename.arg(QString::number(i).rightJustified(3, '0')))) + return false; + } + + return true; +} diff --git a/src/utils/fontmanager.h b/src/utils/fontmanager.h index 1078394a..8509eaf2 100644 --- a/src/utils/fontmanager.h +++ b/src/utils/fontmanager.h @@ -12,6 +12,13 @@ class FontManager static QString verseFontname(Settings::VerseType type, int page); static void loadQcf(); static void loadUiFonts(); + /** + * @brief check if QCF2 font files exist + * + * @return true - all 604 QCF2 files are found + * @return false - one of the files is missing + */ + static bool qcfExists(); }; #endif // FONTMANAGER_H diff --git a/src/utils/verseplayer.cpp b/src/utils/verseplayer.cpp index 98c5cda0..07105921 100644 --- a/src/utils/verseplayer.cpp +++ b/src/utils/verseplayer.cpp @@ -57,7 +57,7 @@ VersePlayer::setPlayerVolume(qreal volume) } QString -VersePlayer::constructVerseFilename(const Verse* v) +VersePlayer::constructVerseFilename(const QSharedPointer v) { // construct verse mp3 filename e.g. 002005.mp3 QString filename; diff --git a/src/utils/verseplayer.h b/src/utils/verseplayer.h index 0ae68aa3..446bde20 100644 --- a/src/utils/verseplayer.h +++ b/src/utils/verseplayer.h @@ -39,7 +39,7 @@ class VersePlayer : public QMediaPlayer * @param v - ::Verse to get the filename of * @return QString of the filename */ - QString constructVerseFilename(const Verse* v); + QString constructVerseFilename(const QSharedPointer v); /** * @brief load the verse mp3 from the current reciter directory and set the * m_verseFile variable @@ -116,7 +116,7 @@ public slots: void missingVerseFile(int reciterIdx, int surah); private: - Verse* m_activeVerse = Verse::current(); + QSharedPointer m_activeVerse = Verse::current(); QSharedPointer m_dbMgr = DBManager::current(); QDir m_reciterDir = DirManager::downloadsDir->absoluteFilePath("recitations"); const QList>& m_recitersList = Reciter::reciters; From 5a7c38b3d5f8170ceb62bf15a06c9d14ab3b42f2 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Fri, 16 Feb 2024 20:59:28 +0200 Subject: [PATCH 07/51] Refactoring: more refactoring + add tafsir changing from within TafsirDialog --- src/core/bookmarksdialog.cpp | 4 ++-- src/core/bookmarksdialog.h | 2 +- src/core/khatmahdialog.h | 2 +- src/core/mainwindow.cpp | 10 +++++----- src/core/mainwindow.ui | 4 ++-- src/core/playercontrols.ui | 4 ++-- src/core/quranreader.cpp | 18 +---------------- src/core/quranreader.h | 12 ++---------- src/core/searchdialog.h | 2 +- src/core/settingsdialog.cpp | 4 ++-- src/core/settingsdialog.h | 2 +- src/core/tafsirdialog.cpp | 32 ++++++++++++++++++++++++------- src/core/tafsirdialog.h | 15 ++++++++++++++- src/utils/dbmanager.cpp | 24 +++++++++++++++++++---- src/utils/dbmanager.h | 22 +++++++++++++++++---- src/utils/downloadmanager.cpp | 2 +- src/utils/downloadmanager.h | 2 +- src/widgets/notificationpopup.cpp | 4 ++-- src/widgets/notificationpopup.h | 2 +- src/widgets/versedialog.h | 2 +- 20 files changed, 103 insertions(+), 66 deletions(-) diff --git a/src/core/bookmarksdialog.cpp b/src/core/bookmarksdialog.cpp index 6713c43f..40d02b84 100644 --- a/src/core/bookmarksdialog.cpp +++ b/src/core/bookmarksdialog.cpp @@ -212,8 +212,8 @@ void BookmarksDialog::btnGoToVerse() { QStringList info = sender()->parent()->objectName().split('-'); - Verse verse{ info.at(0).toInt(), info.at(1).toInt(), info.at(2).toInt() }; - emit navigateToVerse(verse.toList()); + Verse verse(info.at(0).toInt(), info.at(1).toInt(), info.at(2).toInt()); + emit navigateToVerse(verse); } void diff --git a/src/core/bookmarksdialog.h b/src/core/bookmarksdialog.h index ea0adf8d..d3f13338 100644 --- a/src/core/bookmarksdialog.h +++ b/src/core/bookmarksdialog.h @@ -74,7 +74,7 @@ class BookmarksDialog : public QDialog * navigation and selection of a verse. * @param v - ::Verse to navigate to */ - void navigateToVerse(Verse v); + void navigateToVerse(const Verse& v); public slots: /** diff --git a/src/core/khatmahdialog.h b/src/core/khatmahdialog.h index cd90af14..d1a78921 100644 --- a/src/core/khatmahdialog.h +++ b/src/core/khatmahdialog.h @@ -39,7 +39,7 @@ class KhatmahDialog : public QDialog * @brief Emitted when changing the active khatmah * @param v - ::Verse to navigate to */ - void navigateToVerse(Verse v); + void navigateToVerse(const Verse& v); protected: /** diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 20a12609..bd137d34 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -364,12 +364,12 @@ MainWindow::setupConnections() &QuranReader::addSideContent); connect(m_settingsDlg, &SettingsDialog::tafsirChanged, - m_reader, - &QuranReader::updateLoadedTafsir); + m_dbMgr.data(), + &DBManager::updateLoadedTafsir); connect(m_settingsDlg, &SettingsDialog::translationChanged, - m_reader, - &QuranReader::updateLoadedTranslation); + m_dbMgr.data(), + &DBManager::updateLoadedTranslation); connect(m_settingsDlg, &SettingsDialog::sideFontChanged, m_reader, @@ -904,7 +904,7 @@ MainWindow::showVerseTafsir(Verse v) { static bool reload = false; if (reload) { - m_reader->updateLoadedTafsir(); + m_dbMgr->updateLoadedTafsir(); reload = false; } diff --git a/src/core/mainwindow.ui b/src/core/mainwindow.ui index f6129741..b2b55461 100644 --- a/src/core/mainwindow.ui +++ b/src/core/mainwindow.ui @@ -78,13 +78,13 @@ 0 - 4 + 6 0 - 4 + 6 diff --git a/src/core/playercontrols.ui b/src/core/playercontrols.ui index 90c39d5a..43350ab4 100644 --- a/src/core/playercontrols.ui +++ b/src/core/playercontrols.ui @@ -51,13 +51,13 @@ - 27 + 30 5 - 27 + 28 5 diff --git a/src/core/quranreader.cpp b/src/core/quranreader.cpp index 27bbd849..c2a8c536 100644 --- a/src/core/quranreader.cpp +++ b/src/core/quranreader.cpp @@ -17,8 +17,6 @@ QuranReader::QuranReader(QWidget* parent, VersePlayer* player) loadIcons(); loadReader(); updateHighlight(); - updateLoadedTafsir(); - updateLoadedTranslation(); updateSideFont(); updateVerseType(); redrawQuranPage(true); @@ -150,20 +148,6 @@ QuranReader::toggleReaderView() ui->frmPageContent->setVisible(true); } -void -QuranReader::updateLoadedTafsir() -{ - int currTafsir = m_settings->value("Reader/Tafsir").toInt(); - m_dbMgr->setCurrentTafsir(currTafsir); -} - -void -QuranReader::updateLoadedTranslation() -{ - int currTrans = m_settings->value("Reader/Translation").toInt(); - m_dbMgr->setCurrentTranslation(currTrans); -} - void QuranReader::updateSideFont() { @@ -311,7 +295,7 @@ QuranReader::setHighlightedFrame() } void -QuranReader::navigateToVerse(Verse v) +QuranReader::navigateToVerse(const Verse& v) { gotoPage(v.page(), false); diff --git a/src/core/quranreader.h b/src/core/quranreader.h index 8d9758e3..17c602f1 100644 --- a/src/core/quranreader.h +++ b/src/core/quranreader.h @@ -36,19 +36,11 @@ public slots: * @brief navigate to the given Verse and update UI elements accordingly * @param v - Verse to navigate to */ - void navigateToVerse(Verse v); + void navigateToVerse(const Verse& v); /** * @brief toggle the main reader view by hiding one of the panels */ void toggleReaderView(); - /** - * @brief set tafsir to the one in the settings, update the selected db - */ - void updateLoadedTafsir(); - /** - * @brief set translation to the one in the settings, update the selected db - */ - void updateLoadedTranslation(); /** * @brief redraw the current Quran page * @param manualSz - boolean flag to force the use of the manually set @@ -148,7 +140,7 @@ private slots: QSharedPointer m_dbMgr = DBManager::current(); QSharedPointer m_settings = Settings::settings; const QList>& m_recitersList = Reciter::reciters; - const QList>& m_tafasirList = Tafsir::tafasir; + const QList>& m_tafasir = Tafsir::tafasir; const ReaderMode& m_readerMode = Settings::readerMode; void setupConnections(); /** diff --git a/src/core/searchdialog.h b/src/core/searchdialog.h index 22ca1e78..fc8893c4 100644 --- a/src/core/searchdialog.h +++ b/src/core/searchdialog.h @@ -74,7 +74,7 @@ public slots: * navigation and selection of that verse. * @param v - ::Verse to navigate to */ - void navigateToVerse(Verse v); + void navigateToVerse(const Verse& v); protected: /** @brief Re-implementation of QWidget::closeEvent() in order to hide the diff --git a/src/core/settingsdialog.cpp b/src/core/settingsdialog.cpp index dbd02e81..f1452169 100644 --- a/src/core/settingsdialog.cpp +++ b/src/core/settingsdialog.cpp @@ -70,8 +70,8 @@ void SettingsDialog::updateContentCombobox() { ui->cmbTafsir->clear(); - for (int i = 0; i < m_tafasirList.size(); i++) { - const QSharedPointer& t = m_tafasirList[i]; + for (int i = 0; i < m_tafasir.size(); i++) { + const QSharedPointer& t = m_tafasir[i]; if (Tafsir::tafsirExists(t)) ui->cmbTafsir->addItem(t->displayName(), i); } diff --git a/src/core/settingsdialog.h b/src/core/settingsdialog.h index 8b695b65..77460078 100644 --- a/src/core/settingsdialog.h +++ b/src/core/settingsdialog.h @@ -227,7 +227,7 @@ public slots: const QLocale::Language m_languageCode = Settings::language; QSharedPointer const m_settings = Settings::settings; const QDir& m_downloadsDir = *DirManager::downloadsDir; - const QList>& m_tafasirList = Tafsir::tafasir; + const QList>& m_tafasir = Tafsir::tafasir; const QList>& m_trList = Translation::translations; const QMap& m_shortcutDescription = diff --git a/src/core/tafsirdialog.cpp b/src/core/tafsirdialog.cpp index 85969e7e..9123372f 100644 --- a/src/core/tafsirdialog.cpp +++ b/src/core/tafsirdialog.cpp @@ -31,6 +31,19 @@ TafsirDialog::TafsirDialog(QWidget* parent) setupConnections(); } +void +TafsirDialog::setupConnections() +{ + connect( + ui->btnNext, &QPushButton::clicked, this, &TafsirDialog::btnNextClicked); + connect( + ui->btnPrev, &QPushButton::clicked, this, &TafsirDialog::btnPrevClicked); + connect(ui->cmbTafsir, + &QComboBox::currentIndexChanged, + this, + &TafsirDialog::tafsirChanged); +} + void TafsirDialog::btnNextClicked() { @@ -48,26 +61,31 @@ TafsirDialog::btnPrevClicked() } void -TafsirDialog::setupConnections() +TafsirDialog::tafsirChanged() { - connect( - ui->btnNext, &QPushButton::clicked, this, &TafsirDialog::btnNextClicked); - connect( - ui->btnPrev, &QPushButton::clicked, this, &TafsirDialog::btnPrevClicked); + if (m_internalTafsirLoading) + return; + + m_tafsir = ui->cmbTafsir->currentData().toInt(); + m_settings->setValue("Reader/Tafsir", m_tafsir); + m_dbMgr->updateLoadedTafsir(); + loadVerseTafsir(); } void TafsirDialog::updateContentComboBox() { + m_internalTafsirLoading = true; ui->cmbTafsir->clear(); - for (int i = 0; i < m_tafasirList.size(); i++) { - const QSharedPointer t = m_tafasirList.at(i); + for (int i = 0; i < m_tafasir.size(); i++) { + const QSharedPointer& t = m_tafasir.at(i); if (Tafsir::tafsirExists(t)) ui->cmbTafsir->addItem(t->displayName(), i); } m_tafsir = ui->cmbTafsir->findData(m_settings->value("Reader/Tafsir")); ui->cmbTafsir->setCurrentIndex(m_tafsir); + m_internalTafsirLoading = false; } void diff --git a/src/core/tafsirdialog.h b/src/core/tafsirdialog.h index e357a7ee..a603a1e4 100644 --- a/src/core/tafsirdialog.h +++ b/src/core/tafsirdialog.h @@ -69,13 +69,17 @@ private slots: * tafsir. */ void btnPrevClicked(); + /** + * @brief tafsirChanged + */ + void tafsirChanged(); private: Ui::TafsirDialog* ui; QSharedPointer m_dbMgr = DBManager::current(); const int m_qcfVer = Settings::qcfVersion; const QSharedPointer m_settings = Settings::settings; - const QList>& m_tafasirList = Tafsir::tafasir; + const QList>& m_tafasir = Tafsir::tafasir; const QList>& m_trList = Translation::translations; /** @@ -83,7 +87,16 @@ private slots: * components and shortcuts. */ void setupConnections(); + /** + * @brief updateContentComboBox + * + * MODIFIED + */ void updateContentComboBox(); + /** + * @brief m_internalTafsirLoading + */ + bool m_internalTafsirLoading = false; /** * @brief Tafsir enum value mapped to the tafsir index in the * combobox. diff --git a/src/utils/dbmanager.cpp b/src/utils/dbmanager.cpp index f80f1787..594d77cf 100644 --- a/src/utils/dbmanager.cpp +++ b/src/utils/dbmanager.cpp @@ -28,9 +28,11 @@ DBManager::DBManager(QObject* parent) QSqlDatabase::addDatabase("QSQLITE", "TafsirCon"); QSqlDatabase::addDatabase("QSQLITE", "TranslationCon"); - for (int i = 1; i <= 114; i++) { + for (int i = 1; i <= 114; i++) m_surahNames.append(getSurahName(i)); - } + + updateLoadedTafsir(); + updateLoadedTranslation(); } /* ---------------- Database handling ---------------- */ @@ -80,10 +82,10 @@ DBManager::setOpenDatabase(Database db, QString filePath) void DBManager::setCurrentTafsir(int tafsirIdx) { - if (tafsirIdx < 0 || tafsirIdx >= m_tafasirList.size()) + if (tafsirIdx < 0 || tafsirIdx >= m_tafasir.size()) return; - m_currTafsir = m_tafasirList[tafsirIdx]; + m_currTafsir = m_tafasir[tafsirIdx]; const QDir& baseDir = m_currTafsir->isExtra() ? *m_downloadsDir : *m_assetsDir; QString path = "tafasir/" + m_currTafsir->filename(); @@ -819,6 +821,20 @@ DBManager::getTranslation(const int sIdx, const int vIdx) return dbQuery.value(0).toString(); } +void +DBManager::updateLoadedTafsir() +{ + int currTafsir = m_settings->value("Reader/Tafsir").toInt(); + setCurrentTafsir(currTafsir); +} + +void +DBManager::updateLoadedTranslation() +{ + int currTrans = m_settings->value("Reader/Translation").toInt(); + setCurrentTranslation(currTrans); +} + void DBManager::setActiveKhatmah(const int id) { diff --git a/src/utils/dbmanager.h b/src/utils/dbmanager.h index 8be889ef..522040cd 100644 --- a/src/utils/dbmanager.h +++ b/src/utils/dbmanager.h @@ -322,6 +322,16 @@ class DBManager : public QObject */ VerseType getVerseType() const; +public slots: + /** + * @brief set tafsir to the one in the settings, update the selected db + */ + void updateLoadedTafsir(); + /** + * @brief set translation to the one in the settings, update the selected db + */ + void updateLoadedTranslation(); + signals: void bookmarkAdded(); void bookmarkRemoved(); @@ -332,7 +342,7 @@ class DBManager : public QObject const QSharedPointer m_assetsDir = DirManager::assetsDir; const QSharedPointer m_downloadsDir = DirManager::downloadsDir; const QSharedPointer m_settings = Settings::settings; - const QList>& m_tafasirList = Tafsir::tafasir; + const QList>& m_tafasir = Tafsir::tafasir; const QList>& m_translationsList = Translation::translations; const QString m_bookmarksFilepath = @@ -350,16 +360,20 @@ class DBManager : public QObject * databases */ QSqlDatabase m_openDBCon; - + /** + * @brief m_verseType + * + * MODIFIED + */ VerseType m_verseType = Settings::qcf; /** * @brief the current active DBManager::Tafasir */ - QSharedPointer m_currTafsir = nullptr; + QSharedPointer m_currTafsir; /** * @brief the current active DBManager::Translation */ - QSharedPointer m_currTrans = nullptr; + QSharedPointer m_currTrans; /** * @brief path to the currently active tafsir database file */ diff --git a/src/utils/downloadmanager.cpp b/src/utils/downloadmanager.cpp index ef34f844..c895d66b 100644 --- a/src/utils/downloadmanager.cpp +++ b/src/utils/downloadmanager.cpp @@ -98,7 +98,7 @@ DownloadManager::enqeueTask(QPair info) if (info.first) path = "translations/" + m_trList.at(info.second)->filename(); else - path = "tafasir/" + m_tafasirList.at(info.second)->filename(); + path = "tafasir/" + m_tafasir.at(info.second)->filename(); DownloadTask t; t.metainfo = { info.first, info.second, 0 }; t.metainfo.squeeze(); diff --git a/src/utils/downloadmanager.h b/src/utils/downloadmanager.h index c82c04df..fab4d40d 100644 --- a/src/utils/downloadmanager.h +++ b/src/utils/downloadmanager.h @@ -194,7 +194,7 @@ public slots: QSharedPointer m_downloadsDir = DirManager::downloadsDir; QSharedPointer m_dbMgr = DBManager::current(); const QList>& m_recitersList = Reciter::reciters; - const QList>& m_tafasirList = Tafsir::tafasir; + const QList>& m_tafasir = Tafsir::tafasir; const QList>& m_trList = Translation::translations; /** diff --git a/src/widgets/notificationpopup.cpp b/src/widgets/notificationpopup.cpp index 08feba92..95d7cd9a 100644 --- a/src/widgets/notificationpopup.cpp +++ b/src/widgets/notificationpopup.cpp @@ -94,7 +94,7 @@ NotificationPopup::completedDownload(DownloadType type, msg += tr("QCF V2"); else if (type == DownloadManager::File) msg += metainfo[0] ? m_trList.at(metainfo[1])->displayName() - : m_tafasirList.at(metainfo[1])->displayName(); + : m_tafasir.at(metainfo[1])->displayName(); this->notify(msg, success); } @@ -111,7 +111,7 @@ NotificationPopup::downloadError(DownloadType type, const QList& metainfo) msg += tr("QCF V2"); else if (type == DownloadManager::File) msg += metainfo[0] ? m_trList.at(metainfo[1])->displayName() - : m_tafasirList.at(metainfo[1])->displayName(); + : m_tafasir.at(metainfo[1])->displayName(); this->notify(msg, fail); } diff --git a/src/widgets/notificationpopup.h b/src/widgets/notificationpopup.h index e7a1b587..23a66f64 100644 --- a/src/widgets/notificationpopup.h +++ b/src/widgets/notificationpopup.h @@ -107,7 +107,7 @@ public slots: private: QSharedPointer m_dbMgr = DBManager::current(); QList>& m_recitersList = Reciter::reciters; - QList>& m_tafasirList = Tafsir::tafasir; + QList>& m_tafasir = Tafsir::tafasir; QList>& m_trList = Translation::translations; /** * @brief connects signals and slots for different UI diff --git a/src/widgets/versedialog.h b/src/widgets/versedialog.h index de8e6cbc..a882cd73 100644 --- a/src/widgets/versedialog.h +++ b/src/widgets/versedialog.h @@ -27,7 +27,7 @@ public slots: void showVOTD(bool startup); signals: - void navigateToVerse(Verse v); + void navigateToVerse(const Verse& v); protected: void mouseReleaseEvent(QMouseEvent* event); From 2686a2ac1a233b312ad5cdbfbdd45dc8d76b9199 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Fri, 16 Feb 2024 21:18:18 +0200 Subject: [PATCH 08/51] SettingsDialog: move tafsir changing to TafsirDialog --- src/core/mainwindow.cpp | 4 ---- src/core/settingsdialog.cpp | 18 ------------------ src/core/settingsdialog.h | 17 ----------------- src/core/settingsdialog.ui | 14 -------------- src/core/tafsirdialog.cpp | 2 +- 5 files changed, 1 insertion(+), 54 deletions(-) diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index bd137d34..83fd809d 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -362,10 +362,6 @@ MainWindow::setupConnections() &SettingsDialog::redrawSideContent, m_reader, &QuranReader::addSideContent); - connect(m_settingsDlg, - &SettingsDialog::tafsirChanged, - m_dbMgr.data(), - &DBManager::updateLoadedTafsir); connect(m_settingsDlg, &SettingsDialog::translationChanged, m_dbMgr.data(), diff --git a/src/core/settingsdialog.cpp b/src/core/settingsdialog.cpp index f1452169..7cc60506 100644 --- a/src/core/settingsdialog.cpp +++ b/src/core/settingsdialog.cpp @@ -69,12 +69,6 @@ SettingsDialog::fillLanguageCombobox() void SettingsDialog::updateContentCombobox() { - ui->cmbTafsir->clear(); - for (int i = 0; i < m_tafasir.size(); i++) { - const QSharedPointer& t = m_tafasir[i]; - if (Tafsir::tafsirExists(t)) - ui->cmbTafsir->addItem(t->displayName(), i); - } ui->cmbTranslation->clear(); for (int i = 0; i < m_trList.size(); i++) { const QSharedPointer& tr = m_trList[i]; @@ -82,10 +76,8 @@ SettingsDialog::updateContentCombobox() ui->cmbTranslation->addItem(tr->displayName(), i); } - m_tafsir = ui->cmbTafsir->findData(m_settings->value("Reader/Tafsir")); m_translation = ui->cmbTranslation->findData(m_settings->value("Reader/Translation")); - ui->cmbTafsir->setCurrentIndex(m_tafsir); ui->cmbTranslation->setCurrentIndex(m_translation); } @@ -191,13 +183,6 @@ SettingsDialog::updateFileWarning(bool on) m_settings->setValue("MissingFileWarning", on); } -void -SettingsDialog::updateTafsir(int idx) -{ - m_settings->setValue("Reader/Tafsir", idx); - emit tafsirChanged(); -} - void SettingsDialog::updateTranslation(int idx) { @@ -326,9 +311,6 @@ SettingsDialog::applyAllChanges() if (ui->chkFgHighlight->isChecked() != m_fgHighlight) updateFgHighlight(ui->chkFgHighlight->isChecked()); - if (ui->cmbTafsir->currentIndex() != m_tafsir) - updateTafsir(ui->cmbTafsir->currentData().toInt()); - if (ui->cmbTranslation->currentIndex() != m_translation) updateTranslation(ui->cmbTranslation->currentData().toInt()); diff --git a/src/core/settingsdialog.h b/src/core/settingsdialog.h index 77460078..87476d82 100644 --- a/src/core/settingsdialog.h +++ b/src/core/settingsdialog.h @@ -76,11 +76,6 @@ public slots: * @param on - boolean flag representing the new setting value */ void updateFileWarning(bool on); - /** - * @brief Update the selected tafsir - * @param idx - index of the tafsir value in DBManager::Tafsir enum - */ - void updateTafsir(int idx); /** * @brief Update the selected translation * @param idx - index of the translation value in DBManager::Translation enum @@ -173,11 +168,6 @@ public slots: * Quran page construction. */ void redrawQuranPage(bool manual); - /** - * @fn tafsirChanged() - * @brief signal emitted to notify changes in the displayed tafsir - */ - void tafsirChanged(); /** * @fn translationChanged() * @brief signal emitted to notify changes in the displayed translation @@ -268,13 +258,6 @@ public slots: * SettingsDialog::m_audioDevices. */ int m_audioOutIdx; - /** - * @brief DBManager::Tafsir enum value mapped to the tafsir index in the - * combobox. - * - * MODIFIED - */ - int m_tafsir; /** * @brief DBManager::Translation enum value mapped to the translation index in * the combobox. diff --git a/src/core/settingsdialog.ui b/src/core/settingsdialog.ui index ee6c7a64..0c9c5f92 100644 --- a/src/core/settingsdialog.ui +++ b/src/core/settingsdialog.ui @@ -578,20 +578,6 @@ - - - - - - Tafsir - - - - - - - - diff --git a/src/core/tafsirdialog.cpp b/src/core/tafsirdialog.cpp index 9123372f..b07080ae 100644 --- a/src/core/tafsirdialog.cpp +++ b/src/core/tafsirdialog.cpp @@ -68,7 +68,7 @@ TafsirDialog::tafsirChanged() m_tafsir = ui->cmbTafsir->currentData().toInt(); m_settings->setValue("Reader/Tafsir", m_tafsir); - m_dbMgr->updateLoadedTafsir(); + m_dbMgr->setCurrentTafsir(m_tafsir); loadVerseTafsir(); } From 4f5779f55648c484ddd33e0d6299710775655a08 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sat, 17 Feb 2024 21:04:16 +0200 Subject: [PATCH 09/51] refactor: use QPointer instead for QObject --- src/core/bookmarksdialog.h | 8 +++----- src/core/copydialog.cpp | 1 + src/core/copydialog.h | 5 +++-- src/core/downloaderdialog.h | 17 +++++++---------- src/core/khatmahdialog.cpp | 3 +-- src/core/khatmahdialog.h | 12 +++++------- src/core/playercontrols.h | 4 ++-- src/core/quranreader.h | 12 ++++++------ src/core/searchdialog.h | 8 +++----- src/core/settingsdialog.h | 8 +++----- src/core/tafsirdialog.h | 2 ++ src/utils/downloadmanager.h | 5 +++-- src/utils/systemtray.h | 5 +++-- src/utils/verseplayer.h | 3 ++- src/widgets/betaqaviewer.h | 7 ++++--- src/widgets/notificationpopup.h | 9 +++++---- src/widgets/quranpagebrowser.cpp | 4 ++-- src/widgets/quranpagebrowser.h | 19 ++++++++++--------- 18 files changed, 65 insertions(+), 67 deletions(-) diff --git a/src/core/bookmarksdialog.h b/src/core/bookmarksdialog.h index d3f13338..28596526 100644 --- a/src/core/bookmarksdialog.h +++ b/src/core/bookmarksdialog.h @@ -11,6 +11,7 @@ #include "../utils/settings.h" #include #include +#include #include #include #include @@ -116,6 +117,7 @@ private slots: void surahSelected(const QModelIndex& index); private: + Ui::BookmarksDialog* ui; const int m_qcfVer = Settings::qcfVersion; QSharedPointer m_dbMgr = DBManager::current(); /** @@ -138,10 +140,6 @@ private slots: * bookmarks). */ int m_shownSurah = 0; - /** - * @brief pointer to access ui elements generated from .ui files. - */ - Ui::BookmarksDialog* ui; /** * @brief ::Verse QList for all bookmarked verses. */ @@ -153,7 +151,7 @@ private slots: /** * @brief QFrames for shown verses. */ - QList m_frames; + QList> m_frames; /** * @brief model for surahs of bookmarked verses. */ diff --git a/src/core/copydialog.cpp b/src/core/copydialog.cpp index 53883ce7..c08f6618 100644 --- a/src/core/copydialog.cpp +++ b/src/core/copydialog.cpp @@ -4,6 +4,7 @@ CopyDialog::CopyDialog(QWidget* parent) : QDialog(parent) , ui(new Ui::CopyDialog) + , m_verseValidator(new QIntValidator(this)) { ui->setupUi(this); ui->cmbCopyFrom->setValidator(m_verseValidator); diff --git a/src/core/copydialog.h b/src/core/copydialog.h index d63e79e7..d6997108 100644 --- a/src/core/copydialog.h +++ b/src/core/copydialog.h @@ -1,12 +1,13 @@ #ifndef COPYDIALOG_H #define COPYDIALOG_H -#include "../utils/dbmanager.h" #include "../types/verse.h" +#include "../utils/dbmanager.h" #include #include #include #include +#include namespace Ui { class CopyDialog; @@ -42,7 +43,7 @@ public slots: QSharedPointer m_dbMgr = DBManager::current(); void copyRange(); - QIntValidator* m_verseValidator = new QIntValidator(this); + QPointer m_verseValidator; }; #endif // COPYDIALOG_H diff --git a/src/core/downloaderdialog.h b/src/core/downloaderdialog.h index 29ecec99..f4a0396b 100644 --- a/src/core/downloaderdialog.h +++ b/src/core/downloaderdialog.h @@ -117,6 +117,7 @@ private slots: void closeEvent(QCloseEvent* event); private: + Ui::DownloaderDialog* ui; QSharedPointer m_dbMgr = DBManager::current(); const int m_languageCode = Settings::language; const QList>& m_reciters = Reciter::reciters; @@ -163,36 +164,32 @@ private slots: * @param surah - surah number */ void removeFromDownloading(int reciter, int surah); - /** - * @brief Pointer to access ui elements generated from .ui files. - */ - Ui::DownloaderDialog* ui; /** * @brief Pointer to the currently active DownloadProgressBar to update as * verses are downloaded. */ - DownloadProgressBar* m_currentBar; + QPointer m_currentBar; /** * @brief Pointer to DownloadManager instance. */ - DownloadManager* m_downloaderPtr; + QPointer m_downloaderPtr; /** * @brief Pointer to QLabel which contains the state and information for the * currently active download. */ - QLabel* m_currentLb; + QPointer m_currentLb; /** * @brief Pointer to QLabel which contains the download speed/state. */ - QLabel* m_currDownSpeedLb; + QPointer m_currDownSpeedLb; /** * @brief QFrames for the currently active & queued download tasks. */ - QList m_frameLst; + QList> m_frameLst; /** * @brief QFrames for the downloaded tasks. */ - QList m_finishedFrames; + QList> m_finishedFrames; /** * @brief Model for the recitation download QTreeView. */ diff --git a/src/core/khatmahdialog.cpp b/src/core/khatmahdialog.cpp index 75147a43..e534e949 100644 --- a/src/core/khatmahdialog.cpp +++ b/src/core/khatmahdialog.cpp @@ -19,8 +19,7 @@ KhatmahDialog::KhatmahDialog(QWidget* parent) &KhatmahDialog::startNewKhatmah); } -InputField* -KhatmahDialog::loadKhatmah(const int id) +QPointer KhatmahDialog::loadKhatmah(const int id) { QList vInfo(3); m_dbMgr->getKhatmahPos(id, vInfo); diff --git a/src/core/khatmahdialog.h b/src/core/khatmahdialog.h index d1a78921..26555261 100644 --- a/src/core/khatmahdialog.h +++ b/src/core/khatmahdialog.h @@ -6,6 +6,7 @@ #include "../widgets/inputfield.h" #include #include +#include #include namespace Ui { @@ -72,6 +73,7 @@ private slots: void setActiveKhatmah(); private: + Ui::KhatmahDialog* ui; const QSharedPointer m_currVerse = Verse::current(); QSharedPointer m_dbMgr = DBManager::current(); QSharedPointer m_settings = Settings::settings; @@ -84,19 +86,15 @@ private slots: * @param id - id of khatmah to load * @return InputField* - pointer to the InputField of the loaded khatmah name */ - InputField* loadKhatmah(const int id); - /** - * @brief pointer to access ui elements generated from .ui files. - */ - Ui::KhatmahDialog* ui; + QPointer loadKhatmah(const int id); /** * @brief pointer to the current active khatmah QFrame */ - QFrame* m_currActive = nullptr; + QPointer m_currActive; /** * @brief QList of all QFrame(s) loaded for the different khatmah entries */ - QList m_frmLst; + QList> m_frmLst; /** * @brief QList of all available khatmah id(s) */ diff --git a/src/core/playercontrols.h b/src/core/playercontrols.h index b32f0d47..c28c044a 100644 --- a/src/core/playercontrols.h +++ b/src/core/playercontrols.h @@ -97,8 +97,8 @@ private slots: /** * @brief pointer to VersePlayer instance */ - VersePlayer* m_player = nullptr; - QuranReader* m_reader = nullptr; + QPointer m_player = nullptr; + QPointer m_reader = nullptr; }; #endif // PLAYERCONTROLS_H diff --git a/src/core/quranreader.h b/src/core/quranreader.h index 17c602f1..f76a83a1 100644 --- a/src/core/quranreader.h +++ b/src/core/quranreader.h @@ -194,22 +194,22 @@ private slots: * @brief QScrollArea used in single page mode to display verses & * translation */ - QScrollArea* m_scrlVerseByVerse = nullptr; + QPointer m_scrlVerseByVerse = nullptr; /** * @brief pointer to currently active QuranPageBrowser instance, must be one * of the values in m_quranBrowsers array */ - QuranPageBrowser* m_activeQuranBrowser = nullptr; + QPointer m_activeQuranBrowser = nullptr; /** * @brief array of QuranPageBrowser instances used in different modes, index 0 * is used in both modes */ - QuranPageBrowser* m_quranBrowsers[2]{}; + QPointer m_quranBrowsers[2]{}; /** * @brief QList of QFrame pointers to VerseFrame elements in the single page * mode side panel */ - QList m_verseFrameList; + QList> m_verseFrameList; /** * @brief pointer to the currently active page ::Verse list */ @@ -222,8 +222,8 @@ private slots: /** * @brief pointer to the currently highlighted VerseFrame in the side panel */ - VerseFrame* m_highlightedFrm = nullptr; - VersePlayer* m_player = nullptr; + QPointer m_highlightedFrm; + QPointer m_player; /** * @brief QFont used in the side panel translation */ diff --git a/src/core/searchdialog.h b/src/core/searchdialog.h index fc8893c4..6363a3ac 100644 --- a/src/core/searchdialog.h +++ b/src/core/searchdialog.h @@ -10,6 +10,7 @@ #include "../utils/dbmanager.h" #include "../widgets/verseframe.h" #include +#include #include #include #include @@ -95,6 +96,7 @@ private slots: void btnTransferClicked(); private: + Ui::SearchDialog* ui; const QLocale::Language m_lang = Settings::language; QSharedPointer m_dbMgr = DBManager::current(); /** @@ -112,15 +114,11 @@ private slots: * SearchDialog::m_currResults. */ int m_startResult = 0; - /** - * @brief Pointer to access ui elements generated from .ui files. - */ - Ui::SearchDialog* ui; /** * @brief QList for all visible HighlightFrame widgets containing search * results. */ - QList m_lbLst; + QList> m_lbLst; /** * @brief ::Verse QList for the current search results. */ diff --git a/src/core/settingsdialog.h b/src/core/settingsdialog.h index 87476d82..485e64ea 100644 --- a/src/core/settingsdialog.h +++ b/src/core/settingsdialog.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -211,6 +212,7 @@ public slots: void closeEvent(QCloseEvent* event); private: + Ui::SettingsDialog* ui; const int m_qcfVer = Settings::qcfVersion; const int m_themeIdx = Settings::themeId; const ReaderMode m_readerMode = Settings::readerMode; @@ -305,14 +307,10 @@ public slots: * @brief boolean flag to indicate that foreground highlighting is active. */ bool m_fgHighlight = true; - /** - * @brief Pointer to access ui elements generated from .ui files. - */ - Ui::SettingsDialog* ui; /** * @brief pointer to VersePlayer instance. */ - VersePlayer* m_vPlayerPtr = nullptr; + QPointer m_vPlayerPtr; /** * @brief model used by the shortcuts QTableView */ diff --git a/src/core/tafsirdialog.h b/src/core/tafsirdialog.h index a603a1e4..5e9f05ae 100644 --- a/src/core/tafsirdialog.h +++ b/src/core/tafsirdialog.h @@ -71,6 +71,8 @@ private slots: void btnPrevClicked(); /** * @brief tafsirChanged + * + * MODIFIED */ void tafsirChanged(); diff --git a/src/utils/downloadmanager.h b/src/utils/downloadmanager.h index fab4d40d..b98715b9 100644 --- a/src/utils/downloadmanager.h +++ b/src/utils/downloadmanager.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -247,12 +248,12 @@ public slots: /** * @brief QNetworkReply for version info request */ - QNetworkReply* m_versionReply; + QPointer m_versionReply; /** * @brief QNetwrokAccessManager instance responsible for sending download * requests */ - QNetworkAccessManager* m_netMan; + QPointer m_netMan; /** * @brief the currently active DownloadTask */ diff --git a/src/utils/systemtray.h b/src/utils/systemtray.h index 3c35e4ec..4e30c1d5 100644 --- a/src/utils/systemtray.h +++ b/src/utils/systemtray.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -91,11 +92,11 @@ class SystemTray : public QObject /** * @brief system tray context menu */ - QMenu* m_trayMenu; + QPointer m_trayMenu; /** * @brief QSystemTrayIcon instance */ - QSystemTrayIcon* m_sysTray; + QPointer m_sysTray; }; #endif // SYSTEMTRAY_H diff --git a/src/utils/verseplayer.h b/src/utils/verseplayer.h index 446bde20..4e1fe1b0 100644 --- a/src/utils/verseplayer.h +++ b/src/utils/verseplayer.h @@ -15,6 +15,7 @@ #include #include #include +#include /*! * @brief VersePlayer class is responsible for the playback of Quran verse @@ -136,7 +137,7 @@ public slots: /** * @brief QAudioOutput used for playback */ - QAudioOutput* m_output; + QPointer m_output; }; #endif // VERSEPLAYER_H diff --git a/src/widgets/betaqaviewer.h b/src/widgets/betaqaviewer.h index 5511acca..f8b72499 100644 --- a/src/widgets/betaqaviewer.h +++ b/src/widgets/betaqaviewer.h @@ -3,6 +3,7 @@ #include "../utils/dbmanager.h" #include +#include #include #include #include @@ -28,12 +29,12 @@ public slots: void focusOutEvent(QFocusEvent* event); private: - QSharedPointer m_dbMgr = DBManager::current(); Ui::BetaqaViewer* ui; + QSharedPointer m_dbMgr = DBManager::current(); int m_surah = -1; - QGraphicsDropShadowEffect* m_shadowEffect = nullptr; - QPropertyAnimation* m_sizeAnim = nullptr; + QPointer m_shadowEffect; + QPointer m_sizeAnim; }; #endif // BETAQAVIEWER_H diff --git a/src/widgets/notificationpopup.h b/src/widgets/notificationpopup.h index 23a66f64..faf81789 100644 --- a/src/widgets/notificationpopup.h +++ b/src/widgets/notificationpopup.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -119,10 +120,10 @@ public slots: * @param icon - NotificationPopup::Action entry */ void setNotificationIcon(Action icon); - QLabel* m_iconWidget; - QLabel* m_textWidget; - QPropertyAnimation* m_fadeoutAnim; - QGraphicsOpacityEffect* m_opacityEffect; + QPointer m_iconWidget; + QPointer m_textWidget; + QPointer m_fadeoutAnim; + QPointer m_opacityEffect; Qt::DockWidgetArea m_dockArea; QPoint m_notificationPos; QTimer m_notificationPeriod; diff --git a/src/widgets/quranpagebrowser.cpp b/src/widgets/quranpagebrowser.cpp index 813d966d..d3af4113 100644 --- a/src/widgets/quranpagebrowser.cpp +++ b/src/widgets/quranpagebrowser.cpp @@ -13,8 +13,8 @@ using namespace fa; QuranPageBrowser::QuranPageBrowser(QWidget* parent, int initPage) : QTextBrowser(parent) - , m_highlighter{ new QTextCursor(document()) } - , m_highlightColor{ QBrush(qApp->palette().color(QPalette::Highlight)) } + , m_highlighter(new QTextCursor(document())) + , m_highlightColor(QBrush(qApp->palette().color(QPalette::Highlight))) { setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setTextInteractionFlags(Qt::TextInteractionFlag::LinksAccessibleByMouse); diff --git a/src/widgets/quranpagebrowser.h b/src/widgets/quranpagebrowser.h index bab3c1a4..71b913ae 100644 --- a/src/widgets/quranpagebrowser.h +++ b/src/widgets/quranpagebrowser.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -220,39 +221,39 @@ public slots: /** * @brief QAction for zoom-in functionality */ - QAction* m_zoomIn; + QPointer m_zoomIn; /** * @brief QAction for zoom-out functionality */ - QAction* m_zoomOut; + QPointer m_zoomOut; /** * @brief QAction for copy functionality */ - QAction* m_copyAct; + QPointer m_copyAct; /** * @brief QAction for verse selection functionality */ - QAction* m_selectAct; + QPointer m_selectAct; /** * @brief QAction for verse playback functionality */ - QAction* m_playAct; + QPointer m_playAct; /** * @brief QAction for showing tafsir functionality */ - QAction* m_tafsirAct; + QPointer m_tafsirAct; /** * @brief QAction for bookmark addition functionality */ - QAction* m_actAddBookmark; + QPointer m_actAddBookmark; /** * @brief QAction for bookmark removal functionality */ - QAction* m_actRemBookmark; + QPointer m_actRemBookmark; /** * @brief QTextCursor used in highlighting verses */ - QTextCursor* m_highlighter; + QSharedPointer m_highlighter; /** * @brief page format properties used in inserting lines */ From 6eb6a3adbb4638382333295c182637e0a946d3be Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sat, 17 Feb 2024 23:40:26 +0200 Subject: [PATCH 10/51] feat: change TafsirDialog into ContentDialog Added option to view translations to TafsirDialog, changed it into ContentDialog for consistency --- CMakeLists.txt | 6 +- resources/dark/dark.qss | 2 +- resources/light/light.qss | 2 +- resources/light/sepia.qss | 2 +- resources/shortcuts.xml | 2 +- src/core/contentdialog.cpp | 251 ++++++++++++++++++ src/core/contentdialog.h | 176 ++++++++++++ .../{tafsirdialog.ui => contentdialog.ui} | 41 ++- src/core/mainwindow.cpp | 4 +- src/core/mainwindow.h | 10 +- src/core/settingsdialog.cpp | 4 +- src/core/settingsdialog.h | 2 +- src/core/tafsirdialog.cpp | 153 ----------- src/core/tafsirdialog.h | 119 --------- src/utils/dbmanager.cpp | 38 ++- src/utils/dbmanager.h | 21 +- src/utils/downloadmanager.cpp | 2 +- src/utils/downloadmanager.h | 2 +- src/utils/shortcuthandler.cpp | 2 +- src/widgets/notificationpopup.cpp | 4 +- src/widgets/notificationpopup.h | 2 +- 21 files changed, 523 insertions(+), 322 deletions(-) create mode 100644 src/core/contentdialog.cpp create mode 100644 src/core/contentdialog.h rename src/core/{tafsirdialog.ui => contentdialog.ui} (80%) delete mode 100644 src/core/tafsirdialog.cpp delete mode 100644 src/core/tafsirdialog.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a11bc382..75f0128b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,9 +59,9 @@ set(PROJECT_SOURCES src/core/bookmarksdialog.h src/core/bookmarksdialog.cpp src/core/bookmarksdialog.ui - src/core/tafsirdialog.h - src/core/tafsirdialog.cpp - src/core/tafsirdialog.ui + src/core/contentdialog.h + src/core/contentdialog.cpp + src/core/contentdialog.ui src/core/khatmahdialog.h src/core/khatmahdialog.cpp src/core/khatmahdialog.ui diff --git a/resources/dark/dark.qss b/resources/dark/dark.qss index 479bcdc5..06ba21ef 100644 --- a/resources/dark/dark.qss +++ b/resources/dark/dark.qss @@ -71,7 +71,7 @@ QWidget#shortcutsTab { border: none; } -QAbstractScrollArea#tedTafsir { +QAbstractScrollArea#tedContent { border: 1px solid #222222; border-radius: 4px; } diff --git a/resources/light/light.qss b/resources/light/light.qss index ba262853..72e8163c 100644 --- a/resources/light/light.qss +++ b/resources/light/light.qss @@ -71,7 +71,7 @@ QWidget#searchTab { border: none; } -QTextEdit#tedTafsir { +QTextEdit#tedContent { border: 1px solid #e3e3e3; background-color: #fafafa; border-radius: 4px; diff --git a/resources/light/sepia.qss b/resources/light/sepia.qss index caac9edb..0cffe5fd 100644 --- a/resources/light/sepia.qss +++ b/resources/light/sepia.qss @@ -67,7 +67,7 @@ QWidget#searchTab { border: none; } -QTextEdit#tedTafsir { +QTextEdit#tedContent { border: 1px solid #efe3cd; background-color: #f7ebd5; border-radius: 4px; diff --git a/resources/shortcuts.xml b/resources/shortcuts.xml index 56de5308..12598fe0 100644 --- a/resources/shortcuts.xml +++ b/resources/shortcuts.xml @@ -23,7 +23,7 @@ - diff --git a/src/core/contentdialog.cpp b/src/core/contentdialog.cpp new file mode 100644 index 00000000..aa419cd7 --- /dev/null +++ b/src/core/contentdialog.cpp @@ -0,0 +1,251 @@ +/** + * @file tafsirdialog.cpp + * @brief Implementation file for ContentDialog + */ + +#include "contentdialog.h" +#include "../types/tafsir.h" +#include "../utils/fontmanager.h" +#include "../utils/stylemanager.h" +#include "ui_contentdialog.h" + +ContentDialog::ContentDialog(QWidget* parent) + : QDialog(parent) + , ui(new Ui::ContentDialog) +{ + setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_book_open)); + ui->setupUi(this); + ui->frmNav->setLayoutDirection(Qt::LeftToRight); + ui->btnNext->setIcon( + StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_left)); + ui->btnPrev->setIcon( + StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_right)); + + if (m_qcfVer == 1) + m_fontSZ = 18; + else + m_fontSZ = 16; + + // connectors + setupConnections(); +} + +void +ContentDialog::setupConnections() +{ + connect( + ui->btnNext, &QPushButton::clicked, this, &ContentDialog::btnNextClicked); + connect( + ui->btnPrev, &QPushButton::clicked, this, &ContentDialog::btnPrevClicked); + connect(ui->cmbType, + &QComboBox::currentIndexChanged, + this, + &ContentDialog::typeChanged); + connect(ui->cmbContent, + &QComboBox::currentIndexChanged, + this, + &ContentDialog::contentChanged); +} + +void +ContentDialog::setSideFont() +{ + QFont sideFont = + qvariant_cast(m_settings->value("Reader/SideContentFont")); + ui->tedContent->setFont(sideFont); +} + +void +ContentDialog::setShownVerse(const Verse& newShownVerse) +{ + m_shownVerse = newShownVerse; + + if (!m_shownVerse.number()) + m_shownVerse.setNumber(1); + + QString title = tr("Surah: ") + m_dbMgr->getSurahName(m_shownVerse.surah()) + + " - " + tr("Verse: ") + + QString::number(m_shownVerse.number()); + QString glyphs = + m_dbMgr->getVerseGlyphs(m_shownVerse.surah(), m_shownVerse.number()); + QString fontFamily = + FontManager::verseFontname(m_dbMgr->getVerseType(), m_shownVerse.page()); + + ui->lbVerseInfo->setText(title); + ui->lbVerseText->setWordWrap(true); + ui->lbVerseText->setFont(QFont(fontFamily, m_fontSZ)); + ui->lbVerseText->setText(glyphs); + + if (m_shownVerse.surah() == 1 && m_shownVerse.number() == 1) + ui->btnPrev->setDisabled(true); + else if (m_shownVerse.surah() == 114 && m_shownVerse.number() == 6) + ui->btnNext->setDisabled(true); + else { + ui->btnPrev->setDisabled(false); + ui->btnNext->setDisabled(false); + } +} +void +ContentDialog::btnNextClicked() +{ + setShownVerse(m_shownVerse.next()); + loadVerseTafsir(); +} + +void +ContentDialog::btnPrevClicked() +{ + if (m_shownVerse.number() == 1) + m_shownVerse.setNumber(0); + + setShownVerse(m_shownVerse.prev()); + loadVerseTafsir(); +} + +void +ContentDialog::typeChanged() +{ + if (m_internalLoading) + return; + + m_currMode = (Mode)ui->cmbType->currentIndex(); + loadContent(m_currMode); +} + +void +ContentDialog::contentChanged() +{ + if (m_internalLoading) + return; + + switch (m_currMode) { + case Mode::Tafsir: + tafsirChanged(); + break; + case Mode::Translation: + translationChanged(); + break; + default: + break; + } +} + +void +ContentDialog::tafsirChanged() +{ + int tafsirIdx = ui->cmbContent->currentData().toInt(); + m_settings->setValue("Reader/Tafsir", tafsirIdx); + if (m_dbMgr->setCurrentTafsir(tafsirIdx)) + loadVerseTafsir(); +} + +void +ContentDialog::translationChanged() +{ + int trIdx = ui->cmbContent->currentData().toInt(); + if (m_dbMgr->setCurrentTranslation(trIdx)) + loadVerseTranslation(); +} + +void +ContentDialog::loadContent(Mode mode) +{ + m_internalLoading = true; + + setSideFont(); + ui->cmbType->setCurrentIndex((int)mode); + updateContentComboBox(mode); + switch (mode) { + case Mode::Tafsir: + loadVerseTafsir(); + break; + case Mode::Translation: + loadVerseTranslation(); + break; + case Mode::Thoughts: + loadVerseThoughts(); + break; + } + m_currMode = mode; + + m_internalLoading = false; +} + +void +ContentDialog::updateContentComboBox(Mode mode) +{ + ui->cmbContent->clear(); + switch (mode) { + case Mode::Tafsir: + ui->cmbContent->setDisabled(false); + cmbLoadTafasir(); + break; + case Mode::Translation: + ui->cmbContent->setDisabled(false); + cmbLoadTranslations(); + break; + case Mode::Thoughts: + ui->cmbContent->setDisabled(true); + break; + } +} + +void +ContentDialog::cmbLoadTafasir() +{ + for (int i = 0; i < m_tafasir.size(); i++) { + const QSharedPointer<::Tafsir>& t = m_tafasir.at(i); + if (Tafsir::tafsirExists(t)) + ui->cmbContent->addItem(t->displayName(), i); + } + + int tafsirIdx = ui->cmbContent->findData(m_settings->value("Reader/Tafsir")); + ui->cmbContent->setCurrentIndex(tafsirIdx); +} + +void +ContentDialog::cmbLoadTranslations() +{ + for (int i = 0; i < m_translations.size(); i++) { + const QSharedPointer<::Translation>& tr = m_translations[i]; + if (Translation::translationExists(tr)) + ui->cmbContent->addItem(tr->displayName(), i); + } + + int trIdx = ui->cmbContent->findData(m_settings->value("Reader/Translation")); + ui->cmbContent->setCurrentIndex(trIdx); +} + +void +ContentDialog::loadVerseTafsir() +{ + if (m_dbMgr->currTafsir()->isText()) + ui->tedContent->setText( + m_dbMgr->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); + else + ui->tedContent->setHtml( + m_dbMgr->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); +} + +void +ContentDialog::loadVerseTranslation() +{ + ui->tedContent->setText( + m_dbMgr->getTranslation(m_shownVerse.surah(), m_shownVerse.number())); +} + +void +ContentDialog::loadVerseThoughts() +{ +} + +void +ContentDialog::closeEvent(QCloseEvent* event) +{ + this->hide(); +} + +ContentDialog::~ContentDialog() +{ + delete ui; +} diff --git a/src/core/contentdialog.h b/src/core/contentdialog.h new file mode 100644 index 00000000..a5c23ccf --- /dev/null +++ b/src/core/contentdialog.h @@ -0,0 +1,176 @@ +/** + * @file contentdialog.h + * @brief Header file for ContentDialog + */ + +#ifndef CONTENTDIALOG_H +#define CONTENTDIALOG_H + +#include "../types/verse.h" +#include "../utils/dbmanager.h" +#include +#include +#include + +namespace Ui { +class ContentDialog; +} + +/** + * @brief ContentDialog interface for reading Quran tafsir. + * @details Tafsir is shown for a single verse at a time. Navigation between + * verses is independant of the main Quran reader navigation for easier + * navigation. Tafsir is displayed using the side content font set in the + * Settings::settings. + */ +class ContentDialog : public QDialog +{ + Q_OBJECT + +public: + enum Mode + { + Tafsir, + Translation, + Thoughts + }; + /** + * @brief Class constructor + * @param parent - pointer to parent widget + */ + explicit ContentDialog(QWidget* parent = nullptr); + ~ContentDialog(); + /** + * @brief setter member for ContentDialog::m_shownVerse + * @param newShownVerse + */ + void setShownVerse(const Verse& newShownVerse); + /** + * @brief loadContent + * @param mode + * + * MODIFIED + */ + void loadContent(Mode mode); + +protected: + /** @brief Re-implementation of QWidget::closeEvent() in order to hide the + * window instead of closing it. + * @param event + */ + void closeEvent(QCloseEvent* event); + +private slots: + /** + * @brief contentChanged + * + * MODIFIED + */ + void contentChanged(); + /** + * @brief typeChanged + * + * MODIFIED + */ + void typeChanged(); + /** + * @brief increment the ContentDialog::m_shownVerse and load the new verse + * tafsir. + */ + void btnNextClicked(); + /** + * @brief decrement the ContentDialog::m_shownVerse and load the new verse + * tafsir. + */ + void btnPrevClicked(); + +private: + Ui::ContentDialog* ui; + QSharedPointer m_dbMgr = DBManager::current(); + const int m_qcfVer = Settings::qcfVersion; + const QSharedPointer m_settings = Settings::settings; + const QList>& m_tafasir = Tafsir::tafasir; + const QList>& m_translations = + Translation::translations; + /** + * @brief connects signals and slots for different UI + * components and shortcuts. + */ + void setupConnections(); + /** + * @brief setSideFont + * + * MODIFIED + */ + void setSideFont(); + /** + * @brief updateContentComboBox + * + * MODIFIED + */ + void updateContentComboBox(Mode mode); + /** + * @brief cmbLoadTafasir + * + * MODIFIED + */ + void cmbLoadTafasir(); + /** + * @brief cmbLoadTranslations + * + * MODIFIED + */ + void cmbLoadTranslations(); + /** + * @brief tafsirChanged + * + * MODIFIED + */ + void tafsirChanged(); + /** + * @brief translationChanged + * + * MODIFIED + */ + void translationChanged(); + /** + * @brief loads the info and tafsir of ContentDialog::m_shownVerse + * + * MODIFIED + */ + void loadVerseTafsir(); + /** + * @brief loadVerseTranslation + * + * MODIFIED + */ + void loadVerseTranslation(); + /** + * @brief loadVerseThoughts + * + * MODIFIED + */ + void loadVerseThoughts(); + /** + * @brief m_currMode + * + * MODIFIED + */ + Mode m_currMode = Mode::Tafsir; + int m_tafsir; + int m_translation; + /** + * @brief m_internalLoading + */ + bool m_internalLoading = false; + /** + * @brief fixed font size for the verse text displayed above the tafsir. + */ + int m_fontSZ; + /** + * @brief ::Verse instance representing the shown verse. + */ + Verse m_shownVerse; +}; + +#endif // CONTENTDIALOG_H diff --git a/src/core/tafsirdialog.ui b/src/core/contentdialog.ui similarity index 80% rename from src/core/tafsirdialog.ui rename to src/core/contentdialog.ui index 643aeef8..7de9a942 100644 --- a/src/core/tafsirdialog.ui +++ b/src/core/contentdialog.ui @@ -1,7 +1,7 @@ - TafsirDialog - + ContentDialog + 0 @@ -11,7 +11,7 @@ - Tafsir + Content @@ -24,7 +24,38 @@ - + + + + 0 + 0 + + + + + 150 + 0 + + + + + Tafsir + + + + + Translation + + + + + Thoughts + + + + + + @@ -46,7 +77,7 @@ - + PakType Naskh Basic diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 83fd809d..a900a2d3 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -914,11 +914,11 @@ MainWindow::showVerseTafsir(Verse v) } if (m_tafsirDlg == nullptr) { - m_tafsirDlg = new TafsirDialog(this); + m_tafsirDlg = new ContentDialog(this); } m_tafsirDlg->setShownVerse(v); - m_tafsirDlg->loadVerseTafsir(); + m_tafsirDlg->loadContent(ContentDialog::Tafsir); m_tafsirDlg->show(); } diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 777dfb3f..76b41e3f 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -22,7 +22,7 @@ #include "quranreader.h" #include "searchdialog.h" #include "settingsdialog.h" -#include "tafsirdialog.h" +#include "contentdialog.h" #include #include #include @@ -177,7 +177,7 @@ private slots: */ void actionAdvancedCopyTriggered(); /** - * @brief open the TafsirDialog for the current ::Verse + * @brief open the ContentDialog for the current ::Verse */ void actionTafsirTriggered(); /** @@ -226,7 +226,7 @@ private slots: */ void navigateToSurah(QModelIndex& index); /** - * @brief open TafsirDialog with the shown verse set to the given ::Verse + * @brief open ContentDialog with the shown verse set to the given ::Verse * @param v - ::Verse to show the tafsir of */ void showVerseTafsir(Verse v); @@ -366,9 +366,9 @@ private slots: */ QPointer m_player; /** - * @brief pointer to TafsirDialog instance + * @brief pointer to ContentDialog instance */ - QPointer m_tafsirDlg; + QPointer m_tafsirDlg; /** * @brief pointer to SearchDialog instance */ diff --git a/src/core/settingsdialog.cpp b/src/core/settingsdialog.cpp index 7cc60506..2760738d 100644 --- a/src/core/settingsdialog.cpp +++ b/src/core/settingsdialog.cpp @@ -70,8 +70,8 @@ void SettingsDialog::updateContentCombobox() { ui->cmbTranslation->clear(); - for (int i = 0; i < m_trList.size(); i++) { - const QSharedPointer& tr = m_trList[i]; + for (int i = 0; i < m_translations.size(); i++) { + const QSharedPointer& tr = m_translations[i]; if (Translation::translationExists(tr)) ui->cmbTranslation->addItem(tr->displayName(), i); } diff --git a/src/core/settingsdialog.h b/src/core/settingsdialog.h index 485e64ea..ed94d660 100644 --- a/src/core/settingsdialog.h +++ b/src/core/settingsdialog.h @@ -220,7 +220,7 @@ public slots: QSharedPointer const m_settings = Settings::settings; const QDir& m_downloadsDir = *DirManager::downloadsDir; const QList>& m_tafasir = Tafsir::tafasir; - const QList>& m_trList = + const QList>& m_translations = Translation::translations; const QMap& m_shortcutDescription = ShortcutHandler::shortcutsDescription; diff --git a/src/core/tafsirdialog.cpp b/src/core/tafsirdialog.cpp deleted file mode 100644 index b07080ae..00000000 --- a/src/core/tafsirdialog.cpp +++ /dev/null @@ -1,153 +0,0 @@ -/** - * @file tafsirdialog.cpp - * @brief Implementation file for TafsirDialog - */ - -#include "tafsirdialog.h" -#include "../types/tafsir.h" -#include "../utils/fontmanager.h" -#include "../utils/stylemanager.h" -#include "ui_tafsirdialog.h" - -TafsirDialog::TafsirDialog(QWidget* parent) - : QDialog(parent) - , ui(new Ui::TafsirDialog) -{ - setWindowTitle(qApp->translate("SettingsDialog", "Tafsir")); - setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_book_open)); - ui->setupUi(this); - ui->frmNav->setLayoutDirection(Qt::LeftToRight); - ui->btnNext->setIcon( - StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_left)); - ui->btnPrev->setIcon( - StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_right)); - - if (m_qcfVer == 1) - m_fontSZ = 18; - else - m_fontSZ = 16; - - // connectors - setupConnections(); -} - -void -TafsirDialog::setupConnections() -{ - connect( - ui->btnNext, &QPushButton::clicked, this, &TafsirDialog::btnNextClicked); - connect( - ui->btnPrev, &QPushButton::clicked, this, &TafsirDialog::btnPrevClicked); - connect(ui->cmbTafsir, - &QComboBox::currentIndexChanged, - this, - &TafsirDialog::tafsirChanged); -} - -void -TafsirDialog::btnNextClicked() -{ - m_shownVerse = m_shownVerse.next(); - loadVerseTafsir(); -} - -void -TafsirDialog::btnPrevClicked() -{ - if (m_shownVerse.number() == 1) - m_shownVerse.setNumber(0); - m_shownVerse = m_shownVerse.prev(); - loadVerseTafsir(); -} - -void -TafsirDialog::tafsirChanged() -{ - if (m_internalTafsirLoading) - return; - - m_tafsir = ui->cmbTafsir->currentData().toInt(); - m_settings->setValue("Reader/Tafsir", m_tafsir); - m_dbMgr->setCurrentTafsir(m_tafsir); - loadVerseTafsir(); -} - -void -TafsirDialog::updateContentComboBox() -{ - m_internalTafsirLoading = true; - ui->cmbTafsir->clear(); - for (int i = 0; i < m_tafasir.size(); i++) { - const QSharedPointer& t = m_tafasir.at(i); - if (Tafsir::tafsirExists(t)) - ui->cmbTafsir->addItem(t->displayName(), i); - } - - m_tafsir = ui->cmbTafsir->findData(m_settings->value("Reader/Tafsir")); - ui->cmbTafsir->setCurrentIndex(m_tafsir); - m_internalTafsirLoading = false; -} - -void -TafsirDialog::loadVerseTafsir() -{ - if (!m_shownVerse.number()) - m_shownVerse.setNumber(1); - - QString title = tr("Surah: ") + m_dbMgr->getSurahName(m_shownVerse.surah()) + - " - " + tr("Verse: ") + - QString::number(m_shownVerse.number()); - QString glyphs = - m_dbMgr->getVerseGlyphs(m_shownVerse.surah(), m_shownVerse.number()); - QString fontFamily = - FontManager::verseFontname(m_dbMgr->getVerseType(), m_shownVerse.page()); - - ui->lbVerseInfo->setText(title); - ui->lbVerseText->setWordWrap(true); - ui->lbVerseText->setFont(QFont(fontFamily, m_fontSZ)); - ui->lbVerseText->setText(glyphs); - - QFont sideFont = - qvariant_cast(m_settings->value("Reader/SideContentFont")); - ui->tedTafsir->setFont(sideFont); - - if (m_dbMgr->currTafsir()->isText()) - ui->tedTafsir->setText( - m_dbMgr->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); - else - ui->tedTafsir->setHtml( - m_dbMgr->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); - - if (m_shownVerse.surah() == 1 && m_shownVerse.number() == 1) - ui->btnPrev->setDisabled(true); - else if (m_shownVerse.surah() == 114 && m_shownVerse.number() == 6) - ui->btnNext->setDisabled(true); - else { - ui->btnPrev->setDisabled(false); - ui->btnNext->setDisabled(false); - } -} - -void -TafsirDialog::setShownVerse(const Verse& newShownVerse) -{ - m_shownVerse = newShownVerse; -} - -void -TafsirDialog::showEvent(QShowEvent* event) -{ - updateContentComboBox(); - QDialog::showEvent(event); -} - -void -TafsirDialog::closeEvent(QCloseEvent* event) -{ - this->hide(); -} - -TafsirDialog::~TafsirDialog() -{ - delete ui; -} diff --git a/src/core/tafsirdialog.h b/src/core/tafsirdialog.h deleted file mode 100644 index 5e9f05ae..00000000 --- a/src/core/tafsirdialog.h +++ /dev/null @@ -1,119 +0,0 @@ -/** - * @file tafsirdialog.h - * @brief Header file for TafsirDialog - */ - -#ifndef TAFSIRDIALOG_H -#define TAFSIRDIALOG_H - -#include "../types/verse.h" -#include "../utils/dbmanager.h" -#include -#include -#include - -namespace Ui { -class TafsirDialog; -} - -/** - * @brief TafsirDialog interface for reading Quran tafsir. - * @details Tafsir is shown for a single verse at a time. Navigation between - * verses is independant of the main Quran reader navigation for easier - * navigation. Tafsir is displayed using the side content font set in the - * Settings::settings. - */ -class TafsirDialog : public QDialog -{ - Q_OBJECT - -public: - /** - * @brief Class constructor - * @param parent - pointer to parent widget - */ - explicit TafsirDialog(QWidget* parent = nullptr); - ~TafsirDialog(); - - /** - * @brief loads the info and tafsir of TafsirDialog::m_shownVerse - */ - void loadVerseTafsir(); - /** - * @brief setter member for TafsirDialog::m_shownVerse - * @param newShownVerse - */ - void setShownVerse(const Verse& newShownVerse); - -protected: - /** @brief Re-implementation of QWidget::closeEvent() in order to hide the - * window instead of closing it. - * @param event - */ - void closeEvent(QCloseEvent* event); - /** - * @brief Re-implementation of QWidget::showEvent() in order to change the - * dialog title to match the current tafsir. - * @param event - */ - void showEvent(QShowEvent* event); - -private slots: - /** - * @brief increment the TafsirDialog::m_shownVerse and load the new verse - * tafsir. - */ - void btnNextClicked(); - /** - * @brief decrement the TafsirDialog::m_shownVerse and load the new verse - * tafsir. - */ - void btnPrevClicked(); - /** - * @brief tafsirChanged - * - * MODIFIED - */ - void tafsirChanged(); - -private: - Ui::TafsirDialog* ui; - QSharedPointer m_dbMgr = DBManager::current(); - const int m_qcfVer = Settings::qcfVersion; - const QSharedPointer m_settings = Settings::settings; - const QList>& m_tafasir = Tafsir::tafasir; - const QList>& m_trList = - Translation::translations; - /** - * @brief connects signals and slots for different UI - * components and shortcuts. - */ - void setupConnections(); - /** - * @brief updateContentComboBox - * - * MODIFIED - */ - void updateContentComboBox(); - /** - * @brief m_internalTafsirLoading - */ - bool m_internalTafsirLoading = false; - /** - * @brief Tafsir enum value mapped to the tafsir index in the - * combobox. - * - * MODIFIED - */ - int m_tafsir; - /** - * @brief fixed font size for the verse text displayed above the tafsir. - */ - int m_fontSZ; - /** - * @brief ::Verse instance representing the shown verse. - */ - Verse m_shownVerse; -}; - -#endif // TAFSIRDIALOG_H diff --git a/src/utils/dbmanager.cpp b/src/utils/dbmanager.cpp index 594d77cf..8748f659 100644 --- a/src/utils/dbmanager.cpp +++ b/src/utils/dbmanager.cpp @@ -38,7 +38,7 @@ DBManager::DBManager(QObject* parent) /* ---------------- Database handling ---------------- */ void -DBManager::setOpenDatabase(Database db, QString filePath) +DBManager::setOpenDatabase(Database db, QString path) { if (m_currentDb == db) return; @@ -74,36 +74,50 @@ DBManager::setOpenDatabase(Database db, QString filePath) break; } - m_openDBCon.setDatabaseName(filePath); + updateOpenedDbFile(path); +} + +void +DBManager::updateOpenedDbFile(const QString& filepath) +{ + m_openDBCon.setDatabaseName(filepath); if (!m_openDBCon.open()) qFatal("Couldn't open Database!"); } -void +bool DBManager::setCurrentTafsir(int tafsirIdx) { if (tafsirIdx < 0 || tafsirIdx >= m_tafasir.size()) - return; + return false; m_currTafsir = m_tafasir[tafsirIdx]; const QDir& baseDir = m_currTafsir->isExtra() ? *m_downloadsDir : *m_assetsDir; QString path = "tafasir/" + m_currTafsir->filename(); - if (baseDir.exists(path)) - m_tafsirDbPath.setFile(baseDir.filePath(path)); + if (!baseDir.exists(path)) + return false; + + m_tafsirDbPath.setFile(baseDir.filePath(path)); + updateOpenedDbFile(m_tafsirDbPath.absoluteFilePath()); + return true; } -void +bool DBManager::setCurrentTranslation(int translationIdx) { - if (translationIdx < 0 || translationIdx >= m_translationsList.size()) - return; + if (translationIdx < 0 || translationIdx >= m_translations.size()) + return false; - m_currTrans = m_translationsList[translationIdx]; + m_currTrans = m_translations[translationIdx]; const QDir& baseDir = m_currTrans->isExtra() ? *m_downloadsDir : *m_assetsDir; QString path = "translations/" + m_currTrans->filename(); - if (baseDir.exists(path)) - m_transDbPath.setFile(baseDir.filePath(path)); + if (!baseDir.exists(path)) + return false; + + m_transDbPath.setFile(baseDir.filePath(path)); + updateOpenedDbFile(m_transDbPath.absoluteFilePath()); + return true; } /* ---------------- Page-related methods ---------------- */ diff --git a/src/utils/dbmanager.h b/src/utils/dbmanager.h index 522040cd..fb6683bb 100644 --- a/src/utils/dbmanager.h +++ b/src/utils/dbmanager.h @@ -61,19 +61,12 @@ class DBManager : public QObject * @brief sets the active tafsir * @param tafsirName - DBManager::Tafsir entry */ - void setCurrentTafsir(int tafsirIdx); + bool setCurrentTafsir(int tafsirIdx); /** * @brief sets the active translation * @param translationName - DBManager::Translation entry */ - void setCurrentTranslation(int translationIdx); - /** - * @brief sets the currently active sqlite database file and opens - * corresponding connection based on the DBManager::Database type - * @param db - DBManager::Database entry - * @param filePath - path to the database file - */ - void setOpenDatabase(Database db, QString filePath); + bool setCurrentTranslation(int translationIdx); /** * @brief gets the surah number and juz number of the first verse in the page, * used to display page header information @@ -343,10 +336,18 @@ public slots: const QSharedPointer m_downloadsDir = DirManager::downloadsDir; const QSharedPointer m_settings = Settings::settings; const QList>& m_tafasir = Tafsir::tafasir; - const QList>& m_translationsList = + const QList>& m_translations = Translation::translations; const QString m_bookmarksFilepath = DirManager::configDir->absoluteFilePath("bookmarks.db"); + /** + * @brief sets the currently active sqlite database file and opens + * corresponding connection based on the DBManager::Database type + * @param db - DBManager::Database entry + * @param filePath - path to the database file + */ + void setOpenDatabase(Database db, QString path); + void updateOpenedDbFile(const QString& filepath); /** * @brief integer id of the current active khatmah */ diff --git a/src/utils/downloadmanager.cpp b/src/utils/downloadmanager.cpp index c895d66b..b005e52f 100644 --- a/src/utils/downloadmanager.cpp +++ b/src/utils/downloadmanager.cpp @@ -96,7 +96,7 @@ DownloadManager::enqeueTask(QPair info) "https://github.com/0xzer0x/quran-companion/raw/main/extras/"; QString path; if (info.first) - path = "translations/" + m_trList.at(info.second)->filename(); + path = "translations/" + m_translations.at(info.second)->filename(); else path = "tafasir/" + m_tafasir.at(info.second)->filename(); DownloadTask t; diff --git a/src/utils/downloadmanager.h b/src/utils/downloadmanager.h index b98715b9..67ecf292 100644 --- a/src/utils/downloadmanager.h +++ b/src/utils/downloadmanager.h @@ -196,7 +196,7 @@ public slots: QSharedPointer m_dbMgr = DBManager::current(); const QList>& m_recitersList = Reciter::reciters; const QList>& m_tafasir = Tafsir::tafasir; - const QList>& m_trList = + const QList>& m_translations = Translation::translations; /** * @brief generate download url for specified verse using the reciter download diff --git a/src/utils/shortcuthandler.cpp b/src/utils/shortcuthandler.cpp index 53defb65..d07bf8a7 100644 --- a/src/utils/shortcuthandler.cpp +++ b/src/utils/shortcuthandler.cpp @@ -166,7 +166,7 @@ ShortcutHandler::setupConnections() &QShortcut::activated, this, &ShortcutHandler::openSettings); - connect(m_shortcuts.value("TafsirDialog"), + connect(m_shortcuts.value("ContentDialog"), &QShortcut::activated, this, &ShortcutHandler::openTafsir); diff --git a/src/widgets/notificationpopup.cpp b/src/widgets/notificationpopup.cpp index 95d7cd9a..91c49409 100644 --- a/src/widgets/notificationpopup.cpp +++ b/src/widgets/notificationpopup.cpp @@ -93,7 +93,7 @@ NotificationPopup::completedDownload(DownloadType type, else if (type == DownloadManager::QCF) msg += tr("QCF V2"); else if (type == DownloadManager::File) - msg += metainfo[0] ? m_trList.at(metainfo[1])->displayName() + msg += metainfo[0] ? m_translations.at(metainfo[1])->displayName() : m_tafasir.at(metainfo[1])->displayName(); this->notify(msg, success); @@ -110,7 +110,7 @@ NotificationPopup::downloadError(DownloadType type, const QList& metainfo) else if (type == DownloadManager::QCF) msg += tr("QCF V2"); else if (type == DownloadManager::File) - msg += metainfo[0] ? m_trList.at(metainfo[1])->displayName() + msg += metainfo[0] ? m_translations.at(metainfo[1])->displayName() : m_tafasir.at(metainfo[1])->displayName(); this->notify(msg, fail); diff --git a/src/widgets/notificationpopup.h b/src/widgets/notificationpopup.h index faf81789..15dc4970 100644 --- a/src/widgets/notificationpopup.h +++ b/src/widgets/notificationpopup.h @@ -109,7 +109,7 @@ public slots: QSharedPointer m_dbMgr = DBManager::current(); QList>& m_recitersList = Reciter::reciters; QList>& m_tafasir = Tafsir::tafasir; - QList>& m_trList = Translation::translations; + QList>& m_translations = Translation::translations; /** * @brief connects signals and slots for different UI * components and shortcuts. From a4b5638d1bbcba406b333211a1dce7a01f422428 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sun, 18 Feb 2024 21:06:50 +0200 Subject: [PATCH 11/51] feat: add thoughts feature to store notes for each verse Added ability to store plain text notes for any verse through ContentDialog --- src/core/contentdialog.cpp | 20 ++++++++++++--- src/core/contentdialog.h | 4 +++ src/utils/dbmanager.cpp | 50 +++++++++++++++++++++++++++++++++++--- src/utils/dbmanager.h | 17 +++++++++++-- 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/src/core/contentdialog.cpp b/src/core/contentdialog.cpp index aa419cd7..3b65eaa6 100644 --- a/src/core/contentdialog.cpp +++ b/src/core/contentdialog.cpp @@ -108,8 +108,9 @@ ContentDialog::typeChanged() if (m_internalLoading) return; - m_currMode = (Mode)ui->cmbType->currentIndex(); - loadContent(m_currMode); + if (m_currMode == Mode::Thoughts) + saveVerseThoughts(); + loadContent((Mode)ui->cmbType->currentIndex()); } void @@ -151,7 +152,6 @@ void ContentDialog::loadContent(Mode mode) { m_internalLoading = true; - setSideFont(); ui->cmbType->setCurrentIndex((int)mode); updateContentComboBox(mode); @@ -167,7 +167,6 @@ ContentDialog::loadContent(Mode mode) break; } m_currMode = mode; - m_internalLoading = false; } @@ -237,11 +236,24 @@ ContentDialog::loadVerseTranslation() void ContentDialog::loadVerseThoughts() { + ui->tedContent->setText(m_dbMgr->getThoughts(m_shownVerse.toList())); + ui->tedContent->setReadOnly(false); + ui->tedContent->setCursorWidth(1); +} + +void +ContentDialog::saveVerseThoughts() +{ + ui->tedContent->setCursorWidth(0); + ui->tedContent->setReadOnly(true); + m_dbMgr->saveThoughts(m_shownVerse.toList(), ui->tedContent->toPlainText()); } void ContentDialog::closeEvent(QCloseEvent* event) { + if (m_currMode == Mode::Thoughts) + saveVerseThoughts(); this->hide(); } diff --git a/src/core/contentdialog.h b/src/core/contentdialog.h index a5c23ccf..33f51400 100644 --- a/src/core/contentdialog.h +++ b/src/core/contentdialog.h @@ -151,6 +151,10 @@ private slots: * MODIFIED */ void loadVerseThoughts(); + /** + * MODIFIED + */ + void saveVerseThoughts(); /** * @brief m_currMode * diff --git a/src/utils/dbmanager.cpp b/src/utils/dbmanager.cpp index 8748f659..7fc639d6 100644 --- a/src/utils/dbmanager.cpp +++ b/src/utils/dbmanager.cpp @@ -74,11 +74,11 @@ DBManager::setOpenDatabase(Database db, QString path) break; } - updateOpenedDbFile(path); + updateOpenDbFile(path); } void -DBManager::updateOpenedDbFile(const QString& filepath) +DBManager::updateOpenDbFile(const QString& filepath) { m_openDBCon.setDatabaseName(filepath); if (!m_openDBCon.open()) @@ -99,7 +99,7 @@ DBManager::setCurrentTafsir(int tafsirIdx) return false; m_tafsirDbPath.setFile(baseDir.filePath(path)); - updateOpenedDbFile(m_tafsirDbPath.absoluteFilePath()); + updateOpenDbFile(m_tafsirDbPath.absoluteFilePath()); return true; } @@ -116,7 +116,7 @@ DBManager::setCurrentTranslation(int translationIdx) return false; m_transDbPath.setFile(baseDir.filePath(path)); - updateOpenedDbFile(m_transDbPath.absoluteFilePath()); + updateOpenDbFile(m_transDbPath.absoluteFilePath()); return true; } @@ -835,6 +835,48 @@ DBManager::getTranslation(const int sIdx, const int vIdx) return dbQuery.value(0).toString(); } +void +DBManager::saveThoughts(QList vInfo, const QString& text) +{ + int id = getVerseId(vInfo[1], vInfo[2]); + setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + QSqlQuery dbQuery(m_openDBCon); + dbQuery.exec("CREATE TABLE IF NOT EXISTS thoughts(id INTEGER PRIMARY KEY " + "UNIQUE," + "page INTEGER, surah INTEGER, number INTEGER, text TEXT)"); + + dbQuery.prepare("REPLACE INTO thoughts(id, page, surah, number, text) " + "VALUES(:i, :p, :s, :n, :t)"); + dbQuery.bindValue(0, id); + dbQuery.bindValue(1, vInfo[0]); + dbQuery.bindValue(2, vInfo[1]); + dbQuery.bindValue(3, vInfo[2]); + dbQuery.bindValue(4, text); + + if (!dbQuery.exec()) + qCritical() << "SQL statement execution error:" << dbQuery.lastError(); + + m_openDBCon.commit(); +} + +QString +DBManager::getThoughts(QList vInfo) +{ + setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + QSqlQuery dbQuery(m_openDBCon); + dbQuery.prepare( + "SELECT text FROM thoughts WHERE page=:p AND surah=:s AND number=:n"); + dbQuery.bindValue(0, vInfo[0]); + dbQuery.bindValue(1, vInfo[1]); + dbQuery.bindValue(2, vInfo[2]); + + if (!dbQuery.exec()) + qCritical() << "SQL statement execution error:" << dbQuery.lastError(); + + dbQuery.next(); + return dbQuery.value(0).toString(); +} + void DBManager::updateLoadedTafsir() { diff --git a/src/utils/dbmanager.h b/src/utils/dbmanager.h index fb6683bb..5eb769c5 100644 --- a/src/utils/dbmanager.h +++ b/src/utils/dbmanager.h @@ -314,6 +314,14 @@ class DBManager : public QObject * @return VerseType */ VerseType getVerseType() const; + /** + * MODIFIED + */ + void saveThoughts(QList vInfo, const QString& text); + /** + * MODIFIED + */ + QString getThoughts(QList vInfo); public slots: /** @@ -347,7 +355,10 @@ public slots: * @param filePath - path to the database file */ void setOpenDatabase(Database db, QString path); - void updateOpenedDbFile(const QString& filepath); + /** + * MODIFIED + */ + void updateOpenDbFile(const QString& filepath); /** * @brief integer id of the current active khatmah */ @@ -391,7 +402,9 @@ public slots: * @brief path to the QCF glyphs database file */ QFileInfo m_glyphsDbPath; - + /** + * MODIFIED + */ QFileInfo m_betaqatDbPath; /** * @brief QList of sura names (Arabic if UI language is Arabic, Otherwise From b64f3a845fae8954ccc0d6166bbed57c398e0f39 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Mon, 19 Feb 2024 17:07:07 +0200 Subject: [PATCH 12/51] refactor: remove unnecessary Qt major version checking from cmakelists.txt --- CMakeLists.txt | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 75f0128b..4e7f7978 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,10 +12,8 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(QT NAMES Qt6 REQUIRED COMPONENTS Widgets Sql Multimedia Network - LinguistTools) -find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Sql Multimedia - Network LinguistTools) +find_package(Qt6 REQUIRED COMPONENTS Widgets Sql Multimedia Network + LinguistTools) if(WIN32) set(Vulkan_INCLUDE_DIR "$ENV{VULKAN_SDK}\\Include\\vulkan") @@ -114,17 +112,11 @@ set(PROJECT_SOURCES resources.qrc qurancompanion.rc) -if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) - qt_add_executable(quran-companion MANUAL_FINALIZATION ${PROJECT_SOURCES}) -else() - add_executable(quran-companion ${PROJECT_SOURCES}) -endif() +qt_add_executable(quran-companion MANUAL_FINALIZATION ${PROJECT_SOURCES}) target_link_libraries( - quran-companion - PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Sql - Qt${QT_VERSION_MAJOR}::Multimedia Qt${QT_VERSION_MAJOR}::Network - QtAwesome) + quran-companion PRIVATE Qt6::Widgets Qt6::Sql Qt6::Multimedia Qt6::Network + QtAwesome) if(WIN32) set_target_properties(quran-companion PROPERTIES WIN32_EXECUTABLE TRUE) From b3e91fb22054f47a6ea050e407928578873616fe Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Mon, 19 Feb 2024 17:08:24 +0200 Subject: [PATCH 13/51] feat: add translation and thoughts verse lmb menu Added actions to show the translation and thoughts of the verse clicked in the content dialog --- src/core/contentdialog.cpp | 50 ++++++++++ src/core/contentdialog.h | 37 +++++--- src/core/khatmahdialog.cpp | 4 +- src/core/mainwindow.cpp | 154 ++++++++++++++++--------------- src/core/mainwindow.h | 20 ++-- src/core/quranreader.cpp | 18 ++-- src/core/quranreader.h | 4 +- src/utils/dbmanager.cpp | 98 +++++++++++--------- src/utils/dbmanager.h | 59 ++++++------ src/widgets/quranpagebrowser.cpp | 73 ++++++++------- src/widgets/quranpagebrowser.h | 40 +++++--- 11 files changed, 336 insertions(+), 221 deletions(-) diff --git a/src/core/contentdialog.cpp b/src/core/contentdialog.cpp index 3b65eaa6..893073b7 100644 --- a/src/core/contentdialog.cpp +++ b/src/core/contentdialog.cpp @@ -47,6 +47,56 @@ ContentDialog::setupConnections() &ContentDialog::contentChanged); } +void +ContentDialog::showVerseTafsir(const Verse& v) +{ + static bool reload = false; + if (reload) { + m_dbMgr->updateLoadedTafsir(); + reload = false; + } + + if (!Tafsir::tafsirExists(m_dbMgr->currTafsir())) { + int i = m_tafasir.indexOf(m_dbMgr->currTafsir()); + reload = true; + emit missingTafsir(i); + return; + } + + setShownVerse(v); + loadContent(Mode::Tafsir); + show(); +} + +void +ContentDialog::showVerseTranslation(const Verse& v) +{ + static bool reload = false; + if (reload) { + m_dbMgr->updateLoadedTranslation(); + reload = false; + } + + if (!Translation::translationExists(m_dbMgr->currTranslation())) { + int i = m_translations.indexOf(m_dbMgr->currTranslation()); + reload = true; + emit missingTranslation(i); + return; + } + + setShownVerse(v); + loadContent(Mode::Translation); + show(); +} + +void +ContentDialog::showVerseThoughts(const Verse& v) +{ + setShownVerse(v); + loadContent(Mode::Thoughts); + show(); +} + void ContentDialog::setSideFont() { diff --git a/src/core/contentdialog.h b/src/core/contentdialog.h index 33f51400..9173bfa1 100644 --- a/src/core/contentdialog.h +++ b/src/core/contentdialog.h @@ -40,18 +40,15 @@ class ContentDialog : public QDialog */ explicit ContentDialog(QWidget* parent = nullptr); ~ContentDialog(); + +public slots: /** - * @brief setter member for ContentDialog::m_shownVerse - * @param newShownVerse - */ - void setShownVerse(const Verse& newShownVerse); - /** - * @brief loadContent - * @param mode - * - * MODIFIED + * @brief open ContentDialog with the shown verse set to the given ::Verse + * @param v - ::Verse to show the tafsir of */ - void loadContent(Mode mode); + void showVerseTafsir(const Verse& v); + void showVerseTranslation(const Verse& v); + void showVerseThoughts(const Verse& v); protected: /** @brief Re-implementation of QWidget::closeEvent() in order to hide the @@ -60,6 +57,10 @@ class ContentDialog : public QDialog */ void closeEvent(QCloseEvent* event); +signals: + void missingTafsir(int idx); + void missingTranslation(int idx); + private slots: /** * @brief contentChanged @@ -92,17 +93,29 @@ private slots: const QList>& m_tafasir = Tafsir::tafasir; const QList>& m_translations = Translation::translations; + /** + * @brief setSideFont + * + * MODIFIED + */ + void setSideFont(); /** * @brief connects signals and slots for different UI * components and shortcuts. */ void setupConnections(); /** - * @brief setSideFont + * @brief setter member for ContentDialog::m_shownVerse + * @param newShownVerse + */ + void setShownVerse(const Verse& newShownVerse); + /** + * @brief loadContent + * @param mode * * MODIFIED */ - void setSideFont(); + void loadContent(Mode mode); /** * @brief updateContentComboBox * diff --git a/src/core/khatmahdialog.cpp b/src/core/khatmahdialog.cpp index e534e949..ae3f6acb 100644 --- a/src/core/khatmahdialog.cpp +++ b/src/core/khatmahdialog.cpp @@ -22,7 +22,7 @@ KhatmahDialog::KhatmahDialog(QWidget* parent) QPointer KhatmahDialog::loadKhatmah(const int id) { QList vInfo(3); - m_dbMgr->getKhatmahPos(id, vInfo); + m_dbMgr->loadVerse(id, vInfo); QFrame* frame = new QFrame(ui->scrlDialogContent); // property used for styling frame->setProperty("khatmah", true); @@ -150,7 +150,7 @@ KhatmahDialog::setActiveKhatmah() m_settings->setValue("Reader/Khatmah", id); m_dbMgr->saveActiveKhatmah(m_currVerse->toList()); m_dbMgr->setActiveKhatmah(id.toInt()); - m_dbMgr->getKhatmahPos(id.toInt(), vInfo); + m_dbMgr->loadVerse(id.toInt(), vInfo); newActive->findChild("activate")->setEnabled(false); newActive->findChild("remove")->setEnabled(false); diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index a900a2d3..7a9a34d6 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -20,7 +20,7 @@ MainWindow::MainWindow(QWidget* parent) ui->setupUi(this); loadIcons(); loadVerse(); - init(); + loadComponents(); if (m_settings->value("WindowState").isNull()) m_settings->setValue("WindowState", saveState()); @@ -31,7 +31,7 @@ MainWindow::MainWindow(QWidget* parent) setupShortcuts(); setupConnections(); setupSurahsDock(); - setupMenubarToggle(); + setupMenubarButton(); this->show(); m_popup->setDockArea(dockWidgetArea(ui->sideDock)); @@ -68,7 +68,7 @@ MainWindow::loadVerse() int id = m_settings->value("Reader/Khatmah").toInt(); QList vInfo = m_currVerse->toList(); m_dbMgr->setActiveKhatmah(id); - if (!m_dbMgr->getKhatmahPos(id, vInfo)) { + if (!m_dbMgr->loadVerse(id, vInfo)) { QString name = id ? tr("Khatmah ") + QString::number(id) : tr("Default"); m_dbMgr->addKhatmah(vInfo, name, id); } @@ -76,6 +76,48 @@ MainWindow::loadVerse() m_currVerse->update(vInfo); } +void +MainWindow::loadComponents() +{ + m_player = new VersePlayer(this, m_settings->value("Reciter", 0).toInt()); + m_reader = new QuranReader(this, m_player); + m_playerControls = new PlayerControls(this, m_player, m_reader); + m_settingsDlg = new SettingsDialog(this, m_player); + m_popup = new NotificationPopup(this); + m_betaqaViewer = new BetaqaViewer(this); + m_verseDlg = new VerseDialog(this); + m_downManPtr = new DownloadManager(this); + m_cpyDlg = new CopyDialog(this); + m_systemTray = new SystemTray(this); + m_contentDlg = new ContentDialog(this); + + QHBoxLayout* controls = new QHBoxLayout(); + QFrame* controlsFrame = new QFrame(this); + controls->setAlignment(Qt::AlignCenter); + controls->setContentsMargins(0, 0, 0, 0); + controls->setSpacing(0); + controls->addStretch(1); + controls->addWidget(m_playerControls); + controls->addStretch(1); + controlsFrame->setLayout(controls); + ui->scrollAreaWidgetContents->layout()->addWidget(controlsFrame); + ui->scrollAreaWidgetContents->layout()->addWidget(m_reader); + + ui->cmbPage->setValidator(new QIntValidator(1, 604, this)); + + setVerseComboBoxRange(true); + + for (int i = 1; i < 605; i++) { + ui->cmbPage->addItem(QString::number(i)); + } + + // sets without emitting signal + setCmbVerseIdx(m_currVerse->number() - 1); + setCmbJuzIdx(m_dbMgr->getJuzOfPage(m_currVerse->page()) - 1); + + ui->cmbPage->setCurrentIndex(m_currVerse->page() - 1); +} + void MainWindow::checkForUpdates() { @@ -412,53 +454,28 @@ MainWindow::setupConnections() &CopyDialog::copyVerseText); connect(m_reader, &QuranReader::showVerseTafsir, - this, - &MainWindow::showVerseTafsir); + m_contentDlg, + &ContentDialog::showVerseTafsir); + connect(m_reader, + &QuranReader::showVerseTranslation, + m_contentDlg, + &ContentDialog::showVerseTranslation); + connect(m_reader, + &QuranReader::showVerseThoughts, + m_contentDlg, + &ContentDialog::showVerseThoughts); connect(m_reader, &QuranReader::showBetaqa, m_betaqaViewer, &BetaqaViewer::showSurah); -} - -void -MainWindow::init() -{ - m_player = new VersePlayer(this, m_settings->value("Reciter", 0).toInt()); - m_reader = new QuranReader(this, m_player); - m_playerControls = new PlayerControls(this, m_player, m_reader); - m_settingsDlg = new SettingsDialog(this, m_player); - m_popup = new NotificationPopup(this); - m_betaqaViewer = new BetaqaViewer(this); - m_verseDlg = new VerseDialog(this); - m_downManPtr = new DownloadManager(this); - m_cpyDlg = new CopyDialog(this); - - QHBoxLayout* controls = new QHBoxLayout(); - QFrame* controlsFrame = new QFrame(this); - controls->setAlignment(Qt::AlignCenter); - controls->setContentsMargins(0, 0, 0, 0); - controls->setSpacing(0); - controls->addStretch(1); - controls->addWidget(m_playerControls); - controls->addStretch(1); - controlsFrame->setLayout(controls); - ui->scrollAreaWidgetContents->layout()->addWidget(controlsFrame); - ui->scrollAreaWidgetContents->layout()->addWidget(m_reader); - - ui->cmbPage->setValidator(new QIntValidator(1, 604, this)); - m_systemTray = new SystemTray(this); - - setVerseComboBoxRange(true); - - for (int i = 1; i < 605; i++) { - ui->cmbPage->addItem(QString::number(i)); - } - - // sets without emitting signal - setCmbVerseIdx(m_currVerse->number() - 1); - setCmbJuzIdx(m_dbMgr->getJuzOfPage(m_currVerse->page()) - 1); - - ui->cmbPage->setCurrentIndex(m_currVerse->page() - 1); + connect(m_contentDlg, + &ContentDialog::missingTafsir, + this, + &MainWindow::missingTafsir); + connect(m_contentDlg, + &ContentDialog::missingTranslation, + this, + &MainWindow::missingTranslation); } void @@ -482,7 +499,7 @@ MainWindow::setupSurahsDock() } void -MainWindow::setupMenubarToggle() +MainWindow::setupMenubarButton() { QPushButton* toggleNav = new QPushButton(this); toggleNav->setObjectName("btnToggleNav"); @@ -712,6 +729,20 @@ MainWindow::missingTafsir(int idx) } } +void +MainWindow::missingTranslation(int idx) +{ + QMessageBox::StandardButton btn = QMessageBox::question( + this, + tr("Files Missing"), + tr("The selected translation is missing, would you like to download it?")); + + if (btn == QMessageBox::Yes) { + actionDMTriggered(); + m_downloaderDlg->selectDownload(DownloadManager::File, { 1, idx }); + } +} + void MainWindow::missingRecitationFileWarn(int reciterIdx, int surah) { @@ -785,7 +816,7 @@ MainWindow::actionAdvancedCopyTriggered() void MainWindow::actionTafsirTriggered() { - showVerseTafsir(*m_currVerse); + m_contentDlg->showVerseTafsir(*m_currVerse); } void @@ -895,33 +926,6 @@ MainWindow::toggleNavDock() ui->sideDock->toggleViewAction()->toggle(); } -void -MainWindow::showVerseTafsir(Verse v) -{ - static bool reload = false; - if (reload) { - m_dbMgr->updateLoadedTafsir(); - reload = false; - } - - if (!Tafsir::tafsirExists(m_dbMgr->currTafsir())) { - int i; - for (i = 0; i < m_tafasir.size(); i++) - if (m_dbMgr->currTafsir() == m_tafasir[i]) - break; - reload = true; - return missingTafsir(i); - } - - if (m_tafsirDlg == nullptr) { - m_tafsirDlg = new ContentDialog(this); - } - - m_tafsirDlg->setShownVerse(v); - m_tafsirDlg->loadContent(ContentDialog::Tafsir); - m_tafsirDlg->show(); -} - void MainWindow::resizeEvent(QResizeEvent* event) { diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 76b41e3f..bb6e0cc3 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -15,6 +15,7 @@ #include "../widgets/notificationpopup.h" #include "../widgets/versedialog.h" #include "bookmarksdialog.h" +#include "contentdialog.h" #include "copydialog.h" #include "downloaderdialog.h" #include "khatmahdialog.h" @@ -22,7 +23,6 @@ #include "quranreader.h" #include "searchdialog.h" #include "settingsdialog.h" -#include "contentdialog.h" #include #include #include @@ -141,6 +141,13 @@ private slots: * @param idx - index of tafsir in Globals::tafasirList */ void missingTafsir(int idx); + /** + * @brief missingTranslation + * @param idx + * + * MODIFIED + */ + void missingTranslation(int idx); /** * @brief move to the next verse as the playback of the current verse ends * @param status - the status of the current verse recitation media @@ -225,11 +232,6 @@ private slots: * model */ void navigateToSurah(QModelIndex& index); - /** - * @brief open ContentDialog with the shown verse set to the given ::Verse - * @param v - ::Verse to show the tafsir of - */ - void showVerseTafsir(Verse v); /** * @brief search for the surahs with the given argument when the text in the * side dock search box is changed @@ -263,7 +265,7 @@ private slots: /** * @brief initalizes different parts used by the app */ - void init(); + void loadComponents(); /** * @brief load icons for different UI elements */ @@ -290,7 +292,7 @@ private slots: * @brief set the QPushButton the menubar that toggles the navigation dock and * connect to the appropriate menubar action */ - void setupMenubarToggle(); + void setupMenubarButton(); /** * @brief sync the surahs QListView in the navigation dock to match the * currently active ::Verse in the VersePlayer @@ -368,7 +370,7 @@ private slots: /** * @brief pointer to ContentDialog instance */ - QPointer m_tafsirDlg; + QPointer m_contentDlg; /** * @brief pointer to SearchDialog instance */ diff --git a/src/core/quranreader.cpp b/src/core/quranreader.cpp index c2a8c536..2522d1f4 100644 --- a/src/core/quranreader.cpp +++ b/src/core/quranreader.cpp @@ -409,26 +409,32 @@ QuranReader::verseAnchorClicked(const QUrl& hrefUrl) senderBrowser->lmbVerseMenu(m_dbMgr->isBookmarked(v.toList())); switch (chosenAction) { - case QuranPageBrowser::play: + case QuranPageBrowser::Play: selectVerse(browerIdx, idx); highlightCurrentVerse(); m_player->playCurrentVerse(); break; - case QuranPageBrowser::select: + case QuranPageBrowser::Select: selectVerse(browerIdx, idx); m_player->loadActiveVerse(); highlightCurrentVerse(); break; - case QuranPageBrowser::tafsir: + case QuranPageBrowser::Tafsir: emit showVerseTafsir(v); break; - case QuranPageBrowser::copy: + case QuranPageBrowser::Translation: + emit showVerseTranslation(v); + break; + case QuranPageBrowser::Thoughts: + emit showVerseThoughts(v); + break; + case QuranPageBrowser::Copy: emit copyVerseText(v); break; - case QuranPageBrowser::addBookmark: + case QuranPageBrowser::AddBookmark: m_dbMgr->addBookmark(v.toList()); break; - case QuranPageBrowser::removeBookmark: + case QuranPageBrowser::RemoveBookmark: if (m_dbMgr->removeBookmark(v.toList())) break; default: diff --git a/src/core/quranreader.h b/src/core/quranreader.h index f76a83a1..3f478dbb 100644 --- a/src/core/quranreader.h +++ b/src/core/quranreader.h @@ -99,8 +99,10 @@ public slots: void currentVerseChanged(); void currentSurahChanged(); void showBetaqa(int surah); - void showVerseTafsir(const Verse& v); void copyVerseText(const Verse& v); + void showVerseTafsir(const Verse& v); + void showVerseTranslation(const Verse& v); + void showVerseThoughts(const Verse& v); private slots: /** diff --git a/src/utils/dbmanager.cpp b/src/utils/dbmanager.cpp index 7fc639d6..5e6874d1 100644 --- a/src/utils/dbmanager.cpp +++ b/src/utils/dbmanager.cpp @@ -46,30 +46,30 @@ DBManager::setOpenDatabase(Database db, QString path) m_currentDb = db; m_openDBCon.close(); switch (db) { - case null: + case Null: break; - case quran: + case Quran: m_openDBCon = QSqlDatabase::database("QuranCon"); break; - case glyphs: + case Glyphs: m_openDBCon = QSqlDatabase::database("GlyphsCon"); break; - case bookmarks: + case Bookmarks: m_openDBCon = QSqlDatabase::database("BookmarksCon"); break; - case tafsir: + case Tafsir: m_openDBCon = QSqlDatabase::database("TafsirCon"); break; - case translation: + case Translation: m_openDBCon = QSqlDatabase::database("TranslationCon"); break; - case betaqat: + case Betaqat: m_openDBCon = QSqlDatabase::database("BetaqatCon"); break; } @@ -109,9 +109,9 @@ DBManager::setCurrentTranslation(int translationIdx) if (translationIdx < 0 || translationIdx >= m_translations.size()) return false; - m_currTrans = m_translations[translationIdx]; - const QDir& baseDir = m_currTrans->isExtra() ? *m_downloadsDir : *m_assetsDir; - QString path = "translations/" + m_currTrans->filename(); + m_currTr = m_translations[translationIdx]; + const QDir& baseDir = m_currTr->isExtra() ? *m_downloadsDir : *m_assetsDir; + QString path = "translations/" + m_currTr->filename(); if (!baseDir.exists(path)) return false; @@ -125,7 +125,7 @@ DBManager::setCurrentTranslation(int translationIdx) QPair DBManager::getPageMetadata(const int page) { - setOpenDatabase(Database::quran, m_quranDbPath.filePath()); + setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); dbQuery.prepare( "SELECT sura_no,jozz FROM verses_v1 WHERE page=:p ORDER BY id"); @@ -142,7 +142,7 @@ DBManager::getPageMetadata(const int page) QStringList DBManager::getPageLines(const int page) { - setOpenDatabase(Database::glyphs, m_glyphsDbPath.filePath()); + setOpenDatabase(Database::Glyphs, m_glyphsDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); QString query = "SELECT %0 FROM pages WHERE page_no=%1"; @@ -162,7 +162,7 @@ QList> DBManager::getVerseInfoList(int page) { QList> viList; - setOpenDatabase(Database::quran, m_quranDbPath.filePath()); + setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); QString query = @@ -184,7 +184,7 @@ DBManager::getVerseInfoList(int page) int DBManager::getJuzStartPage(const int juz) { - setOpenDatabase(Database::glyphs, m_glyphsDbPath.filePath()); + setOpenDatabase(Database::Glyphs, m_glyphsDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); QString query = @@ -203,7 +203,7 @@ int DBManager::getJuzOfPage(const int page) { // returns the jozz number which the passed page belongs to - setOpenDatabase(Database::glyphs, m_glyphsDbPath.filePath()); + setOpenDatabase(Database::Glyphs, m_glyphsDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); QString query = @@ -223,7 +223,7 @@ DBManager::getJuzOfPage(const int page) QString DBManager::getSurahNameGlyph(const int sura) { - setOpenDatabase(Database::glyphs, m_glyphsDbPath.filePath()); + setOpenDatabase(Database::Glyphs, m_glyphsDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); dbQuery.prepare("SELECT qcf_v1 FROM surah_glyphs WHERE surah=:i"); @@ -240,7 +240,7 @@ DBManager::getSurahNameGlyph(const int sura) QString DBManager::getJuzGlyph(const int juz) { - setOpenDatabase(Database::glyphs, m_glyphsDbPath.filePath()); + setOpenDatabase(Database::Glyphs, m_glyphsDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); dbQuery.prepare("SELECT text FROM juz_glyphs WHERE juz=:j"); @@ -260,7 +260,7 @@ DBManager::getVerseGlyphs(const int sIdx, const int vIdx) if (m_verseType != VerseType::qcf) return getVerseText(sIdx, vIdx); - setOpenDatabase(Database::glyphs, m_glyphsDbPath.filePath()); + setOpenDatabase(Database::Glyphs, m_glyphsDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); @@ -283,7 +283,7 @@ DBManager::getVerseGlyphs(const int sIdx, const int vIdx) QString DBManager::getSurahName(const int sIdx, bool ar) { - setOpenDatabase(Database::quran, m_quranDbPath.filePath()); + setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); if (m_languageCode == QLocale::Arabic || ar) @@ -303,7 +303,7 @@ DBManager::getSurahName(const int sIdx, bool ar) QString DBManager::getBetaqa(const int surah) { - setOpenDatabase(betaqat, m_betaqatDbPath.filePath()); + setOpenDatabase(Betaqat, m_betaqatDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); if (m_languageCode == QLocale::Arabic) @@ -323,7 +323,7 @@ DBManager::getBetaqa(const int surah) int DBManager::getVerseId(const int sIdx, const int vIdx) { - setOpenDatabase(Database::quran, m_quranDbPath.filePath()); + setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); dbQuery.prepare("SELECT id FROM verses_v1 WHERE sura_no=:s AND aya_no=:v"); dbQuery.bindValue(0, sIdx); @@ -340,7 +340,7 @@ DBManager::getVerseId(const int sIdx, const int vIdx) QList DBManager::getVerseById(const int id) { - setOpenDatabase(Database::quran, m_quranDbPath.filePath()); + setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); dbQuery.prepare("SELECT page,sura_no,aya_no FROM verses_v1 WHERE id=:i"); dbQuery.bindValue(0, id); @@ -358,7 +358,7 @@ DBManager::getVerseById(const int id) int DBManager::getSurahVerseCount(const int surahIdx) { - setOpenDatabase(Database::quran, m_quranDbPath.filePath()); + setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); dbQuery.prepare( @@ -377,7 +377,7 @@ DBManager::getSurahVerseCount(const int surahIdx) int DBManager::getSurahStartPage(int surahIdx) { - setOpenDatabase(Database::quran, m_quranDbPath.filePath()); + setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); dbQuery.prepare("SELECT page FROM verses_v1 WHERE sura_no=:sn AND aya_no=1"); @@ -404,7 +404,7 @@ DBManager::searchSurahs(QString searchText, const bool whole) { QList> results; - setOpenDatabase(Database::quran, m_quranDbPath.filePath()); + setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); QString q = "SELECT page,sura_no,aya_no FROM verses_v" + @@ -440,7 +440,7 @@ QList DBManager::searchSurahNames(QString text) { QList results; - setOpenDatabase(Database::quran, m_quranDbPath.filePath()); + setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); QString q = "SELECT DISTINCT sura_no FROM verses_v1 WHERE (sura_name_ar like '%" + @@ -464,9 +464,9 @@ DBManager::searchSurahNames(QString text) /* ---------------- Verse-related methods ---------------- */ bool -DBManager::getKhatmahPos(const int khatmahId, QList& vInfo) +DBManager::loadVerse(const int khatmahId, QList& vInfo) { - setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + setOpenDatabase(Database::Bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); QString q = QString::asprintf( @@ -487,7 +487,7 @@ DBManager::getKhatmahPos(const int khatmahId, QList& vInfo) int DBManager::addKhatmah(QList vInfo, const QString name, const int id) { - setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + setOpenDatabase(Database::Bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); dbQuery.exec( "CREATE TABLE IF NOT EXISTS khatmah(id INTEGER PRIMARY KEY " @@ -528,7 +528,7 @@ DBManager::addKhatmah(QList vInfo, const QString name, const int id) bool DBManager::editKhatmahName(const int khatmahId, QString newName) { - setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + setOpenDatabase(Database::Bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); QString q = "SELECT DISTINCT id FROM khatmah WHERE name='%0'"; if (!dbQuery.exec(q.arg(newName))) { @@ -553,7 +553,7 @@ DBManager::editKhatmahName(const int khatmahId, QString newName) void DBManager::removeKhatmah(const int id) { - setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + setOpenDatabase(Database::Bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); if (!dbQuery.exec(QString::asprintf("DELETE FROM khatmah WHERE id=%i", id))) qDebug() << "Couldn't execute query: " << dbQuery.lastQuery(); @@ -562,7 +562,7 @@ DBManager::removeKhatmah(const int id) bool DBManager::saveActiveKhatmah(QList vInfo) { - setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + setOpenDatabase(Database::Bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); QString q = QString::asprintf( "UPDATE khatmah SET page=%i, surah=%i, number=%i WHERE id=%i", @@ -583,7 +583,7 @@ DBManager::saveActiveKhatmah(QList vInfo) QString DBManager::getVerseText(const int sIdx, const int vIdx) { - setOpenDatabase(Database::quran, m_quranDbPath.filePath()); + setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); if (m_verseType == VerseType::annotated) dbQuery.prepare("SELECT aya_text_annotated FROM verses_v1 WHERE sura_no=:s " @@ -607,7 +607,7 @@ QList DBManager::getAllKhatmah() { QList res; - setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + setOpenDatabase(Database::Bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); if (!dbQuery.exec("SELECT id FROM khatmah")) qCritical() << "Couldn't execute sql query: " << dbQuery.lastQuery(); @@ -621,7 +621,7 @@ DBManager::getAllKhatmah() QString DBManager::getKhatmahName(const int id) { - setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + setOpenDatabase(Database::Bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); if (!dbQuery.exec("SELECT name FROM khatmah WHERE id=" + QString::number(id))) qCritical() << "Couldn't execute sql query: " << dbQuery.lastQuery(); @@ -633,7 +633,7 @@ DBManager::getKhatmahName(const int id) QList DBManager::randomVerse() { - setOpenDatabase(Database::quran, m_quranDbPath.filePath()); + setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); int id = QRandomGenerator::global()->bounded(1, 6237); @@ -653,7 +653,7 @@ DBManager::randomVerse() int DBManager::getVersePage(const int& surahIdx, const int& verse) { - setOpenDatabase(Database::quran, m_quranDbPath.filePath()); + setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); QString query = "SELECT page FROM verses_v%0 WHERE sura_no=%1 AND aya_no=%2"; @@ -675,7 +675,7 @@ DBManager::searchVerses(QString searchText, const bool whole) { QList> results; - setOpenDatabase(Database::quran, m_quranDbPath.filePath()); + setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); QString q = "SELECT page,sura_no,aya_no FROM verses_v" + @@ -709,7 +709,7 @@ QList> DBManager::bookmarkedVerses(int surahIdx) { QList> results; - setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + setOpenDatabase(Database::Bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); QString q = "SELECT page,surah,number FROM favorites"; if (surahIdx != -1) @@ -731,7 +731,7 @@ DBManager::bookmarkedVerses(int surahIdx) bool DBManager::isBookmarked(QList vInfo) { - setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + setOpenDatabase(Database::Bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); dbQuery.prepare( @@ -753,7 +753,7 @@ DBManager::isBookmarked(QList vInfo) bool DBManager::addBookmark(QList vInfo) { - setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + setOpenDatabase(Database::Bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); dbQuery.exec("CREATE TABLE IF NOT EXISTS favorites(id INTEGER PRIMARY KEY " "AUTOINCREMENT," @@ -778,7 +778,7 @@ DBManager::addBookmark(QList vInfo) bool DBManager::removeBookmark(QList vInfo) { - setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + setOpenDatabase(Database::Bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); dbQuery.prepare( "DELETE FROM favorites WHERE page=:p AND surah=:s AND number=:n"); @@ -800,7 +800,7 @@ DBManager::removeBookmark(QList vInfo) QString DBManager::getTafsir(const int sIdx, const int vIdx) { - setOpenDatabase(Database::tafsir, m_tafsirDbPath.filePath()); + setOpenDatabase(Database::Tafsir, m_tafsirDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); @@ -819,7 +819,7 @@ DBManager::getTafsir(const int sIdx, const int vIdx) QString DBManager::getTranslation(const int sIdx, const int vIdx) { - setOpenDatabase(Database::translation, m_transDbPath.filePath()); + setOpenDatabase(Database::Translation, m_transDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); @@ -839,7 +839,7 @@ void DBManager::saveThoughts(QList vInfo, const QString& text) { int id = getVerseId(vInfo[1], vInfo[2]); - setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + setOpenDatabase(Database::Bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); dbQuery.exec("CREATE TABLE IF NOT EXISTS thoughts(id INTEGER PRIMARY KEY " "UNIQUE," @@ -862,7 +862,7 @@ DBManager::saveThoughts(QList vInfo, const QString& text) QString DBManager::getThoughts(QList vInfo) { - setOpenDatabase(Database::bookmarks, m_bookmarksFilepath); + setOpenDatabase(Database::Bookmarks, m_bookmarksFilepath); QSqlQuery dbQuery(m_openDBCon); dbQuery.prepare( "SELECT text FROM thoughts WHERE page=:p AND surah=:s AND number=:n"); @@ -920,3 +920,9 @@ DBManager::currTafsir() const { return m_currTafsir; } + +QSharedPointer +DBManager::currTranslation() const +{ + return m_currTr; +} diff --git a/src/utils/dbmanager.h b/src/utils/dbmanager.h index 5eb769c5..57f7261c 100644 --- a/src/utils/dbmanager.h +++ b/src/utils/dbmanager.h @@ -38,13 +38,13 @@ class DBManager : public QObject */ enum Database { - null, ///< default value - quran, ///< (quran.db) main Quran database file - glyphs, ///< (glyphs.db) QCF glyphs database - betaqat, - bookmarks, ///< (bookmarks.db) bookmarked verses and khatmah database - tafsir, ///< currently selected tafsir database file - translation ///< currently selected translation database file + Null, ///< default value + Quran, ///< (quran.db) main Quran database file + Glyphs, ///< (glyphs.db) QCF glyphs database + Betaqat, + Bookmarks, ///< (bookmarks.db) bookmarked verses and khatmah database + Tafsir, ///< currently selected tafsir database file + Translation ///< currently selected translation database file }; /** @@ -135,7 +135,7 @@ class DBManager : public QObject * @return boolean indicating a successful operation (false in case of error * and in case id does not exist) */ - bool getKhatmahPos(const int khatmahId, QList& vInfo); + bool loadVerse(const int khatmahId, QList& vInfo); /** * @brief add a new khatmah/replace khatmah with given id with position of * ::Verse v @@ -289,16 +289,6 @@ class DBManager : public QObject * @return boolean indicating successful removal */ bool removeBookmark(QList vInfo); - /** - * @brief getter for m_currTafsir - * @return the currently set DBManager::Tafasir - */ - QSharedPointer currTafsir() const; - /** - * @brief getter for m_activeKhatmah - * @return the currently active khatmah id - */ - const int activeKhatmah() const; /** * @brief setter for m_activeKhatmah * @param id - id of the active khatmah @@ -309,19 +299,36 @@ class DBManager : public QObject * @param newVerseType */ void setVerseType(VerseType newVerseType); + /** + * MODIFIED + */ + void saveThoughts(QList vInfo, const QString& text); + /** + * MODIFIED + */ + QString getThoughts(QList vInfo); + /** + * @brief getter for m_activeKhatmah + * @return the currently active khatmah id + */ + const int activeKhatmah() const; /** * @brief getter for m_verseType * @return VerseType */ VerseType getVerseType() const; /** - * MODIFIED + * @brief getter for m_currTafsir + * @return the currently set DBManager::Tafasir */ - void saveThoughts(QList vInfo, const QString& text); + QSharedPointer<::Tafsir> currTafsir() const; /** + * @brief currTranslation + * @return + * * MODIFIED */ - QString getThoughts(QList vInfo); + QSharedPointer<::Translation> currTranslation() const; public slots: /** @@ -343,8 +350,8 @@ public slots: const QSharedPointer m_assetsDir = DirManager::assetsDir; const QSharedPointer m_downloadsDir = DirManager::downloadsDir; const QSharedPointer m_settings = Settings::settings; - const QList>& m_tafasir = Tafsir::tafasir; - const QList>& m_translations = + const QList>& m_tafasir = Tafsir::tafasir; + const QList>& m_translations = Translation::translations; const QString m_bookmarksFilepath = DirManager::configDir->absoluteFilePath("bookmarks.db"); @@ -366,7 +373,7 @@ public slots: /** * @brief the currently active database type */ - Database m_currentDb = null; + Database m_currentDb = Null; /** * @brief QSqlDatabase instance to interact with the different sqlite * databases @@ -381,11 +388,11 @@ public slots: /** * @brief the current active DBManager::Tafasir */ - QSharedPointer m_currTafsir; + QSharedPointer<::Tafsir> m_currTafsir; /** * @brief the current active DBManager::Translation */ - QSharedPointer m_currTrans; + QSharedPointer<::Translation> m_currTr; /** * @brief path to the currently active tafsir database file */ diff --git a/src/widgets/quranpagebrowser.cpp b/src/widgets/quranpagebrowser.cpp index d3af4113..d9e0c42a 100644 --- a/src/widgets/quranpagebrowser.cpp +++ b/src/widgets/quranpagebrowser.cpp @@ -296,11 +296,13 @@ QuranPageBrowser::Action QuranPageBrowser::lmbVerseMenu(bool favoriteVerse) { QMenu lmbMenu(this); - lmbMenu.addAction(m_playAct); - lmbMenu.addAction(m_selectAct); - lmbMenu.addAction(m_tafsirAct); + lmbMenu.addAction(m_actPlay); + lmbMenu.addAction(m_actSelect); + lmbMenu.addAction(m_actTafsir); + lmbMenu.addAction(m_actTranslation); + lmbMenu.addAction(m_actThoughts); lmbMenu.addSeparator(); - lmbMenu.addAction(m_copyAct); + lmbMenu.addAction(m_actCopy); if (favoriteVerse) { lmbMenu.addAction(m_actRemBookmark); } else { @@ -309,19 +311,23 @@ QuranPageBrowser::lmbVerseMenu(bool favoriteVerse) QAction* chosen = lmbMenu.exec(QCursor::pos()); - QuranPageBrowser::Action actionIdx = null; - if (chosen == m_playAct) - actionIdx = play; - else if (chosen == m_selectAct) - actionIdx = select; - else if (chosen == m_tafsirAct) - actionIdx = tafsir; - else if (chosen == m_copyAct) - actionIdx = copy; + QuranPageBrowser::Action actionIdx = Action::Null; + if (chosen == m_actPlay) + actionIdx = Action::Play; + else if (chosen == m_actSelect) + actionIdx = Action::Select; + else if (chosen == m_actTafsir) + actionIdx = Action::Tafsir; + else if (chosen == m_actTranslation) + actionIdx = Action::Translation; + else if (chosen == m_actThoughts) + actionIdx = Action::Thoughts; + else if (chosen == m_actCopy) + actionIdx = Action::Copy; else if (chosen == m_actAddBookmark) - actionIdx = addBookmark; + actionIdx = Action::AddBookmark; else if (chosen == m_actRemBookmark) - actionIdx = removeBookmark; + actionIdx = Action::RemoveBookmark; this->clearFocus(); return actionIdx; @@ -353,28 +359,33 @@ QuranPageBrowser::bestFitFontSize() void QuranPageBrowser::createActions() { - m_zoomIn = new QAction(tr("Zoom In"), this); - m_zoomOut = new QAction(tr("Zoom Out"), this); - m_copyAct = new QAction(tr("Copy Verse"), this); - m_selectAct = new QAction(tr("Select"), this); - m_playAct = new QAction(tr("Play"), this); - m_tafsirAct = new QAction(tr("Tafsir"), this); + m_actZoomIn = new QAction(tr("Zoom In"), this); + m_actZoomOut = new QAction(tr("Zoom Out"), this); + m_actCopy = new QAction(tr("Copy Verse"), this); + m_actSelect = new QAction(tr("Select"), this); + m_actPlay = new QAction(tr("Play"), this); + m_actTafsir = new QAction(tr("Tafsir"), this); + m_actTranslation = new QAction(tr("Translation"), this); + m_actThoughts = new QAction(tr("Thoughts"), this); m_actAddBookmark = new QAction(tr("Add Bookmark"), this); m_actRemBookmark = new QAction(tr("Remove Bookmark"), this); - m_zoomIn->setIcon( + m_actZoomIn->setIcon( StyleManager::awesome->icon(fa_solid, fa_magnifying_glass_plus)); - m_zoomOut->setIcon( + m_actZoomOut->setIcon( StyleManager::awesome->icon(fa_solid, fa_magnifying_glass_minus)); - m_playAct->setIcon(StyleManager::awesome->icon(fa_solid, fa_play)); - m_selectAct->setIcon(StyleManager::awesome->icon(fa_solid, fa_hand_pointer)); - m_tafsirAct->setIcon(StyleManager::awesome->icon(fa_solid, fa_book_open)); - m_copyAct->setIcon(StyleManager::awesome->icon(fa_solid, fa_clipboard)); + m_actPlay->setIcon(StyleManager::awesome->icon(fa_solid, fa_play)); + m_actSelect->setIcon(StyleManager::awesome->icon(fa_solid, fa_hand_pointer)); + m_actTafsir->setIcon(StyleManager::awesome->icon(fa_solid, fa_book_open)); + m_actTranslation->setIcon(StyleManager::awesome->icon(fa_solid, fa_language)); + m_actThoughts->setIcon(StyleManager::awesome->icon(fa_solid, fa_comment)); + m_actCopy->setIcon(StyleManager::awesome->icon(fa_solid, fa_clipboard)); m_actAddBookmark->setIcon( StyleManager::awesome->icon(fa_regular, fa_bookmark)); m_actRemBookmark->setIcon(StyleManager::awesome->icon(fa_solid, fa_bookmark)); - connect(m_zoomIn, &QAction::triggered, this, &QuranPageBrowser::actionZoomIn); connect( - m_zoomOut, &QAction::triggered, this, &QuranPageBrowser::actionZoomOut); + m_actZoomIn, &QAction::triggered, this, &QuranPageBrowser::actionZoomIn); + connect( + m_actZoomOut, &QAction::triggered, this, &QuranPageBrowser::actionZoomOut); } #ifndef QT_NO_CONTEXTMENU @@ -382,8 +393,8 @@ void QuranPageBrowser::contextMenuEvent(QContextMenuEvent* event) { QMenu menu(this); - menu.addAction(m_zoomIn); - menu.addAction(m_zoomOut); + menu.addAction(m_actZoomIn); + menu.addAction(m_actZoomOut); m_mousePos = event->pos(); m_mouseGlobalPos = event->globalPos(); diff --git a/src/widgets/quranpagebrowser.h b/src/widgets/quranpagebrowser.h index 71b913ae..c24f4874 100644 --- a/src/widgets/quranpagebrowser.h +++ b/src/widgets/quranpagebrowser.h @@ -33,13 +33,15 @@ class QuranPageBrowser : public QTextBrowser */ enum Action { - null, ///< no action (default) - play, ///< select the verse and start playback - select, ///< only select the verse - tafsir, ///< show the tafsir for the verse - copy, ///< copy the verse text to clipboard - addBookmark, ///< add the verse to bookmarks - removeBookmark ///< remove the verse from bookmarks + Null, ///< no action (default) + Play, ///< select the verse and start playback + Select, ///< only select the verse + Tafsir, ///< show the tafsir for the verse + Translation, ///< show the translation for the verse + Thoughts, ///< show user thoughts for the verse + Copy, ///< copy the verse text to clipboard + AddBookmark, ///< add the verse to bookmarks + RemoveBookmark ///< remove the verse from bookmarks }; /** * @brief class constructor @@ -221,27 +223,39 @@ public slots: /** * @brief QAction for zoom-in functionality */ - QPointer m_zoomIn; + QPointer m_actZoomIn; /** * @brief QAction for zoom-out functionality */ - QPointer m_zoomOut; + QPointer m_actZoomOut; /** * @brief QAction for copy functionality */ - QPointer m_copyAct; + QPointer m_actCopy; /** * @brief QAction for verse selection functionality */ - QPointer m_selectAct; + QPointer m_actSelect; /** * @brief QAction for verse playback functionality */ - QPointer m_playAct; + QPointer m_actPlay; /** * @brief QAction for showing tafsir functionality */ - QPointer m_tafsirAct; + QPointer m_actTafsir; + /** + * @brief m_actTranslation + * + * MODIFIED + */ + QPointer m_actTranslation; + /** + * @brief m_actThoughts + * + * MODIFIED + */ + QPointer m_actThoughts; /** * @brief QAction for bookmark addition functionality */ From 534c0b163c27f4b3652f07a31fae164e11564854 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Mon, 19 Feb 2024 22:26:24 +0200 Subject: [PATCH 14/51] refactor: create content class to remove duplicate code in tafsir and translation classes --- CMakeLists.txt | 2 ++ src/core/contentdialog.cpp | 8 ++++---- src/core/settingsdialog.cpp | 2 +- src/types/content.cpp | 26 ++++++++++++++++++++++++++ src/types/content.h | 21 +++++++++++++++++++++ src/types/tafsir.cpp | 36 +++++------------------------------- src/types/tafsir.h | 14 +++----------- src/types/translation.cpp | 35 ++++------------------------------- src/types/translation.h | 18 ++++-------------- 9 files changed, 70 insertions(+), 92 deletions(-) create mode 100644 src/types/content.cpp create mode 100644 src/types/content.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e7f7978..d3e97866 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,8 @@ set(PROJECT_SOURCES src/types/verse.cpp src/types/reciter.h src/types/reciter.cpp + src/types/content.h + src/types/content.cpp src/types/tafsir.h src/types/tafsir.cpp src/types/translation.h diff --git a/src/core/contentdialog.cpp b/src/core/contentdialog.cpp index 893073b7..a351e943 100644 --- a/src/core/contentdialog.cpp +++ b/src/core/contentdialog.cpp @@ -56,7 +56,7 @@ ContentDialog::showVerseTafsir(const Verse& v) reload = false; } - if (!Tafsir::tafsirExists(m_dbMgr->currTafsir())) { + if (!m_dbMgr->currTafsir()->isAvailable()) { int i = m_tafasir.indexOf(m_dbMgr->currTafsir()); reload = true; emit missingTafsir(i); @@ -77,7 +77,7 @@ ContentDialog::showVerseTranslation(const Verse& v) reload = false; } - if (!Translation::translationExists(m_dbMgr->currTranslation())) { + if (!m_dbMgr->currTranslation()->isAvailable()) { int i = m_translations.indexOf(m_dbMgr->currTranslation()); reload = true; emit missingTranslation(i); @@ -244,7 +244,7 @@ ContentDialog::cmbLoadTafasir() { for (int i = 0; i < m_tafasir.size(); i++) { const QSharedPointer<::Tafsir>& t = m_tafasir.at(i); - if (Tafsir::tafsirExists(t)) + if (t->isAvailable()) ui->cmbContent->addItem(t->displayName(), i); } @@ -257,7 +257,7 @@ ContentDialog::cmbLoadTranslations() { for (int i = 0; i < m_translations.size(); i++) { const QSharedPointer<::Translation>& tr = m_translations[i]; - if (Translation::translationExists(tr)) + if (tr->isAvailable()) ui->cmbContent->addItem(tr->displayName(), i); } diff --git a/src/core/settingsdialog.cpp b/src/core/settingsdialog.cpp index 2760738d..f862a5bf 100644 --- a/src/core/settingsdialog.cpp +++ b/src/core/settingsdialog.cpp @@ -72,7 +72,7 @@ SettingsDialog::updateContentCombobox() ui->cmbTranslation->clear(); for (int i = 0; i < m_translations.size(); i++) { const QSharedPointer& tr = m_translations[i]; - if (Translation::translationExists(tr)) + if (tr->isAvailable()) ui->cmbTranslation->addItem(tr->displayName(), i); } diff --git a/src/types/content.cpp b/src/types/content.cpp new file mode 100644 index 00000000..3067780c --- /dev/null +++ b/src/types/content.cpp @@ -0,0 +1,26 @@ +#include "content.h" + +Content::Content(QString display, QString filename, bool isExtra) + : m_displayName(display) + , m_filename(filename) + , m_isExtra(isExtra) +{ +} + +const QString& +Content::displayName() const +{ + return m_displayName; +} + +const QString& +Content::filename() const +{ + return m_filename; +} + +const bool& +Content::isExtra() const +{ + return m_isExtra; +} diff --git a/src/types/content.h b/src/types/content.h new file mode 100644 index 00000000..b8df8a11 --- /dev/null +++ b/src/types/content.h @@ -0,0 +1,21 @@ +#ifndef CONTENT_H +#define CONTENT_H + +#include + +class Content +{ +public: + explicit Content(QString display, QString filename, bool isExtra); + const QString& displayName() const; + const QString& filename() const; + const bool& isExtra() const; + virtual bool isAvailable() const = 0; + +private: + QString m_displayName; + QString m_filename; + bool m_isExtra; +}; + +#endif // CONTENT_H diff --git a/src/types/tafsir.cpp b/src/types/tafsir.cpp index a2b09465..f8c10a02 100644 --- a/src/types/tafsir.cpp +++ b/src/types/tafsir.cpp @@ -1,5 +1,6 @@ #include "tafsir.h" #include "../utils/dirmanager.h" +#include "content.h" #include #include #include @@ -35,38 +36,17 @@ Tafsir::populateTafasir() } Tafsir::Tafsir(QString display, QString filename, bool isText, bool isExtra) - : m_displayName(display) - , m_filename(filename) + : Content(display, filename, isExtra) , m_isText(isText) - , m_isExtra(isExtra) { } bool -Tafsir::tafsirExists(int idx) -{ - const QSharedPointer& t = tafasir.at(idx); - return tafsirExists(t); -} - -bool -Tafsir::tafsirExists(const QSharedPointer& t) +Tafsir::isAvailable() const { const QDir& baseDir = - t->isExtra() ? *DirManager::downloadsDir : *DirManager::assetsDir; - return baseDir.exists("tafasir/" + t->filename()); -} - -const QString& -Tafsir::displayName() const -{ - return m_displayName; -} - -const QString& -Tafsir::filename() const -{ - return m_filename; + isExtra() ? *DirManager::downloadsDir : *DirManager::assetsDir; + return baseDir.exists("tafasir/" + filename()); } const bool @@ -74,9 +54,3 @@ Tafsir::isText() const { return m_isText; } - -const bool -Tafsir::isExtra() const -{ - return m_isExtra; -} diff --git a/src/types/tafsir.h b/src/types/tafsir.h index e2a57d00..8455b494 100644 --- a/src/types/tafsir.h +++ b/src/types/tafsir.h @@ -1,31 +1,23 @@ #ifndef TAFSIR_H #define TAFSIR_H +#include "content.h" #include #include #include -class Tafsir +class Tafsir : public Content { public: static void populateTafasir(); static QList> tafasir; - static bool tafsirExists(int idx); - static bool tafsirExists(const QSharedPointer& t); - explicit Tafsir(QString display, QString filename, bool isText, bool isExtra); - - const QString& displayName() const; - const QString& filename() const; const bool isText() const; - const bool isExtra() const; + bool isAvailable() const; private: - QString m_displayName; - QString m_filename; bool m_isText; - bool m_isExtra; }; #endif // TAFSIR_H diff --git a/src/types/translation.cpp b/src/types/translation.cpp index f4ee7259..00315ecc 100644 --- a/src/types/translation.cpp +++ b/src/types/translation.cpp @@ -33,41 +33,14 @@ Translation::populateTranslations() } Translation::Translation(QString display, QString filename, bool isExtra) - : m_displayName(display) - , m_filename(filename) - , m_isExtra(isExtra) + : Content(display, filename, isExtra) { } bool -Translation::translationExists(int idx) -{ - const QSharedPointer& tr = translations.at(idx); - return translationExists(tr); -} - -bool -Translation::translationExists(const QSharedPointer& tr) +Translation::isAvailable() const { const QDir& baseDir = - tr->isExtra() ? *DirManager::downloadsDir : *DirManager::assetsDir; - return baseDir.exists("translations/" + tr->filename()); -} - -const QString& -Translation::displayName() const -{ - return m_displayName; -} - -const QString& -Translation::filename() const -{ - return m_filename; -} - -const bool& -Translation::isExtra() const -{ - return m_isExtra; + isExtra() ? *DirManager::downloadsDir : *DirManager::assetsDir; + return baseDir.exists("translations/" + filename()); } diff --git a/src/types/translation.h b/src/types/translation.h index 3f0de999..1106bd3c 100644 --- a/src/types/translation.h +++ b/src/types/translation.h @@ -1,29 +1,19 @@ #ifndef TRANSLATION_H #define TRANSLATION_H +#include "content.h" #include #include #include -class Translation +class Translation : public Content { public: - static QList> translations; static void populateTranslations(); - - static bool translationExists(int idx); - static bool translationExists(const QSharedPointer& tr); + static QList> translations; explicit Translation(QString display, QString filename, bool isExtra); - - const QString& displayName() const; - const QString& filename() const; - const bool& isExtra() const; - -private: - QString m_displayName; - QString m_filename; - bool m_isExtra; + bool isAvailable() const; }; #endif // TRANSLATION_H From 24845463e4781a024b1fcea10f061c6988c61f9e Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:03:32 +0200 Subject: [PATCH 15/51] content: added pickthall english translation --- extras/translations/en_pickthall.db | Bin 0 -> 2068480 bytes resources/files.xml | 1 + 2 files changed, 1 insertion(+) create mode 100644 extras/translations/en_pickthall.db diff --git a/extras/translations/en_pickthall.db b/extras/translations/en_pickthall.db new file mode 100644 index 0000000000000000000000000000000000000000..5f4377cbbd70090d91631edc477a08f555fc1f99 GIT binary patch literal 2068480 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%0DuOjzn48+cS#_qwUK=KixUV-x@1;3fP2rn3L< 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

W1kr#;u(wu5!&zU$n+`_f$@}8(u8(&lbhA>9C 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%0ACWZJz@h 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 WPlLn|jmhHAm7x;v4ld=FnEnK5z&1Jn literal 0 HcmV?d00001 diff --git a/resources/files.xml b/resources/files.xml index 59119258..2b8b1acc 100644 --- a/resources/files.xml +++ b/resources/files.xml @@ -29,6 +29,7 @@ + From 035f4f3c967fa3eb6d274803e6810588a593a684 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:05:23 +0200 Subject: [PATCH 16/51] refactor: remove unnecessary parent widget resizing --- src/core/quranreader.ui | 2 +- src/widgets/quranpagebrowser.cpp | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/quranreader.ui b/src/core/quranreader.ui index 2876e504..5fcbce07 100644 --- a/src/core/quranreader.ui +++ b/src/core/quranreader.ui @@ -138,7 +138,7 @@ 0 - + 0 diff --git a/src/widgets/quranpagebrowser.cpp b/src/widgets/quranpagebrowser.cpp index d9e0c42a..0b55eb8c 100644 --- a/src/widgets/quranpagebrowser.cpp +++ b/src/widgets/quranpagebrowser.cpp @@ -188,8 +188,7 @@ QuranPageBrowser::constructPage(int pageNo, bool forceCustomSize) setHref(&textCursor, 1, "#F" + QString::number(m_headerData.first)); } - this->parentWidget()->setMinimumWidth(m_pageLineSize.width() + 70); - this->setMinimumWidth(m_pageLineSize.width() + 70); + setMinimumWidth(m_pageLineSize.width() + 70); // page lines drawing int counter = 0, prevAnchor = pageNo < 3 ? 0 : m_currPageHeader.size() + 1; From 51b4b52b6e039efc4703d328b80f52f9f33f575c Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:16:43 +0200 Subject: [PATCH 17/51] fix: separate translation in content dialog from side panel translation --- src/core/contentdialog.cpp | 36 ++++++++++++++++-------------------- src/core/contentdialog.h | 8 ++++---- src/core/quranreader.cpp | 2 ++ src/types/verse.cpp | 8 ++++---- src/types/verse.h | 4 ++-- src/utils/dbmanager.cpp | 16 ++++++++++------ src/utils/dbmanager.h | 4 ++-- 7 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/core/contentdialog.cpp b/src/core/contentdialog.cpp index a351e943..24958b5e 100644 --- a/src/core/contentdialog.cpp +++ b/src/core/contentdialog.cpp @@ -12,6 +12,8 @@ ContentDialog::ContentDialog(QWidget* parent) : QDialog(parent) , ui(new Ui::ContentDialog) + , m_tafsir(m_settings->value("Reader/Tafsir").toInt()) + , m_translation(m_settings->value("Reader/Translation").toInt()) { setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_book_open)); ui->setupUi(this); @@ -110,9 +112,6 @@ ContentDialog::setShownVerse(const Verse& newShownVerse) { m_shownVerse = newShownVerse; - if (!m_shownVerse.number()) - m_shownVerse.setNumber(1); - QString title = tr("Surah: ") + m_dbMgr->getSurahName(m_shownVerse.surah()) + " - " + tr("Verse: ") + QString::number(m_shownVerse.number()); @@ -138,18 +137,15 @@ ContentDialog::setShownVerse(const Verse& newShownVerse) void ContentDialog::btnNextClicked() { - setShownVerse(m_shownVerse.next()); - loadVerseTafsir(); + setShownVerse(m_shownVerse.next(false)); + loadContent(m_currMode); } void ContentDialog::btnPrevClicked() { - if (m_shownVerse.number() == 1) - m_shownVerse.setNumber(0); - - setShownVerse(m_shownVerse.prev()); - loadVerseTafsir(); + setShownVerse(m_shownVerse.prev(false)); + loadContent(m_currMode); } void @@ -184,18 +180,17 @@ ContentDialog::contentChanged() void ContentDialog::tafsirChanged() { - int tafsirIdx = ui->cmbContent->currentData().toInt(); - m_settings->setValue("Reader/Tafsir", tafsirIdx); - if (m_dbMgr->setCurrentTafsir(tafsirIdx)) + m_tafsir = ui->cmbContent->currentData().toInt(); + m_settings->setValue("Reader/Tafsir", m_tafsir); + if (m_dbMgr->setCurrentTafsir(m_tafsir)) loadVerseTafsir(); } void ContentDialog::translationChanged() { - int trIdx = ui->cmbContent->currentData().toInt(); - if (m_dbMgr->setCurrentTranslation(trIdx)) - loadVerseTranslation(); + m_translation = ui->cmbContent->currentData().toInt(); + loadVerseTranslation(); } void @@ -248,8 +243,8 @@ ContentDialog::cmbLoadTafasir() ui->cmbContent->addItem(t->displayName(), i); } - int tafsirIdx = ui->cmbContent->findData(m_settings->value("Reader/Tafsir")); - ui->cmbContent->setCurrentIndex(tafsirIdx); + int idx = ui->cmbContent->findData(m_tafsir); + ui->cmbContent->setCurrentIndex(idx); } void @@ -261,8 +256,8 @@ ContentDialog::cmbLoadTranslations() ui->cmbContent->addItem(tr->displayName(), i); } - int trIdx = ui->cmbContent->findData(m_settings->value("Reader/Translation")); - ui->cmbContent->setCurrentIndex(trIdx); + int idx = ui->cmbContent->findData(m_translation); + ui->cmbContent->setCurrentIndex(idx); } void @@ -279,6 +274,7 @@ ContentDialog::loadVerseTafsir() void ContentDialog::loadVerseTranslation() { + m_dbMgr->setCurrentTranslation(m_translation); ui->tedContent->setText( m_dbMgr->getTranslation(m_shownVerse.surah(), m_shownVerse.number())); } diff --git a/src/core/contentdialog.h b/src/core/contentdialog.h index 9173bfa1..78816eb7 100644 --- a/src/core/contentdialog.h +++ b/src/core/contentdialog.h @@ -176,10 +176,6 @@ private slots: Mode m_currMode = Mode::Tafsir; int m_tafsir; int m_translation; - /** - * @brief m_internalLoading - */ - bool m_internalLoading = false; /** * @brief fixed font size for the verse text displayed above the tafsir. */ @@ -188,6 +184,10 @@ private slots: * @brief ::Verse instance representing the shown verse. */ Verse m_shownVerse; + /** + * @brief m_internalLoading + */ + bool m_internalLoading = false; }; #endif // CONTENTDIALOG_H diff --git a/src/core/quranreader.cpp b/src/core/quranreader.cpp index 2522d1f4..6e2a2fcb 100644 --- a/src/core/quranreader.cpp +++ b/src/core/quranreader.cpp @@ -217,6 +217,8 @@ QuranReader::addSideContent() if (m_dbMgr->getVerseType() == Settings::qcf) m_versesFont.setFamily(FontManager::pageFontname(m_currVerse->page())); + m_dbMgr->setCurrentTranslation( + m_settings->value("Reader/Translation").toInt()); for (int i = m_activeVList->size() - 1; i >= 0; i--) { const Verse* vInfo = &(m_activeVList->at(i)); diff --git a/src/types/verse.cpp b/src/types/verse.cpp index 3fa4f1a0..68a322bb 100644 --- a/src/types/verse.cpp +++ b/src/types/verse.cpp @@ -99,7 +99,7 @@ Verse::update(const QList& vInfo) } Verse -Verse::next() +Verse::next(bool basmalah) { if (!m_number) { m_number = 1; @@ -109,16 +109,16 @@ Verse::next() QList vInfo = m_dbMgr->getVerseById(m_dbMgr->getVerseId(m_surah, m_number) + 1); - if (vInfo[2] == 1 && vInfo[1] != 9 && vInfo[1] != 1) + if (vInfo[2] == 1 && vInfo[1] != 9 && vInfo[1] != 1 && basmalah) vInfo[2] = 0; return Verse(vInfo); } Verse -Verse::prev() +Verse::prev(bool basmalah) { - if (m_number == 1 && m_surah != 9 && m_surah != 1) { + if (m_number == 1 && m_surah != 9 && m_surah != 1 && basmalah) { m_number = 0; return *this; } diff --git a/src/types/verse.h b/src/types/verse.h index aa215486..7757e9cf 100644 --- a/src/types/verse.h +++ b/src/types/verse.h @@ -25,8 +25,8 @@ class Verse QList toList() const; void update(const Verse& v); void update(const QList& vInfo); - Verse next(); - Verse prev(); + Verse next(bool basmalah = true); + Verse prev(bool basmalah = true); Verse& operator=(const Verse& cp); Verse& operator=(const QList& vInfo); diff --git a/src/utils/dbmanager.cpp b/src/utils/dbmanager.cpp index 5e6874d1..88a880e2 100644 --- a/src/utils/dbmanager.cpp +++ b/src/utils/dbmanager.cpp @@ -86,12 +86,14 @@ DBManager::updateOpenDbFile(const QString& filepath) } bool -DBManager::setCurrentTafsir(int tafsirIdx) +DBManager::setCurrentTafsir(int idx) { - if (tafsirIdx < 0 || tafsirIdx >= m_tafasir.size()) + if (idx < 0 || idx >= m_tafasir.size()) return false; + if (m_currTafsir == m_tafasir[idx]) + return true; - m_currTafsir = m_tafasir[tafsirIdx]; + m_currTafsir = m_tafasir[idx]; const QDir& baseDir = m_currTafsir->isExtra() ? *m_downloadsDir : *m_assetsDir; QString path = "tafasir/" + m_currTafsir->filename(); @@ -104,12 +106,14 @@ DBManager::setCurrentTafsir(int tafsirIdx) } bool -DBManager::setCurrentTranslation(int translationIdx) +DBManager::setCurrentTranslation(int idx) { - if (translationIdx < 0 || translationIdx >= m_translations.size()) + if (idx < 0 || idx >= m_translations.size()) return false; + if (m_currTr == m_translations[idx]) + return true; - m_currTr = m_translations[translationIdx]; + m_currTr = m_translations[idx]; const QDir& baseDir = m_currTr->isExtra() ? *m_downloadsDir : *m_assetsDir; QString path = "translations/" + m_currTr->filename(); if (!baseDir.exists(path)) diff --git a/src/utils/dbmanager.h b/src/utils/dbmanager.h index 57f7261c..17b53cad 100644 --- a/src/utils/dbmanager.h +++ b/src/utils/dbmanager.h @@ -61,12 +61,12 @@ class DBManager : public QObject * @brief sets the active tafsir * @param tafsirName - DBManager::Tafsir entry */ - bool setCurrentTafsir(int tafsirIdx); + bool setCurrentTafsir(int idx); /** * @brief sets the active translation * @param translationName - DBManager::Translation entry */ - bool setCurrentTranslation(int translationIdx); + bool setCurrentTranslation(int idx); /** * @brief gets the surah number and juz number of the first verse in the page, * used to display page header information From fbf9b0f10df4b330aab7cd7f2cc35688d4bdaa56 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sat, 24 Feb 2024 09:13:34 +0200 Subject: [PATCH 18/51] refactor: break down downloadmanager into classes --- CMakeLists.txt | 25 ++++++- src/types/downloadtask.cpp | 31 ++++++++ src/types/downloadtask.h | 17 +++++ src/types/qcftask.cpp | 38 ++++++++++ src/types/qcftask.h | 24 +++++++ src/types/recitationtask.cpp | 64 +++++++++++++++++ src/types/recitationtask.h | 32 +++++++++ src/types/tafsirtask.cpp | 39 ++++++++++ src/types/tafsirtask.h | 26 +++++++ src/types/translationtask.cpp | 39 ++++++++++ src/types/translationtask.h | 27 +++++++ src/utils/contentjob.cpp | 77 ++++++++++++++++++++ src/utils/contentjob.h | 35 +++++++++ src/utils/downloadjob.h | 33 +++++++++ src/utils/jobmanager.cpp | 111 +++++++++++++++++++++++++++++ src/utils/jobmanager.h | 43 +++++++++++ src/utils/qcfjob.cpp | 115 ++++++++++++++++++++++++++++++ src/utils/qcfjob.h | 39 ++++++++++ src/utils/surahjob.cpp | 130 ++++++++++++++++++++++++++++++++++ src/utils/surahjob.h | 47 ++++++++++++ src/utils/taskdownloader.cpp | 116 ++++++++++++++++++++++++++++++ src/utils/taskdownloader.h | 39 ++++++++++ src/utils/translationjob.cpp | 59 +++++++++++++++ src/utils/translationjob.h | 33 +++++++++ 24 files changed, 1238 insertions(+), 1 deletion(-) create mode 100644 src/types/downloadtask.cpp create mode 100644 src/types/downloadtask.h create mode 100644 src/types/qcftask.cpp create mode 100644 src/types/qcftask.h create mode 100644 src/types/recitationtask.cpp create mode 100644 src/types/recitationtask.h create mode 100644 src/types/tafsirtask.cpp create mode 100644 src/types/tafsirtask.h create mode 100644 src/types/translationtask.cpp create mode 100644 src/types/translationtask.h create mode 100644 src/utils/contentjob.cpp create mode 100644 src/utils/contentjob.h create mode 100644 src/utils/downloadjob.h create mode 100644 src/utils/jobmanager.cpp create mode 100644 src/utils/jobmanager.h create mode 100644 src/utils/qcfjob.cpp create mode 100644 src/utils/qcfjob.h create mode 100644 src/utils/surahjob.cpp create mode 100644 src/utils/surahjob.h create mode 100644 src/utils/taskdownloader.cpp create mode 100644 src/utils/taskdownloader.h create mode 100644 src/utils/translationjob.cpp create mode 100644 src/utils/translationjob.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d3e97866..788935e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,7 +114,30 @@ set(PROJECT_SOURCES resources.qrc qurancompanion.rc) -qt_add_executable(quran-companion MANUAL_FINALIZATION ${PROJECT_SOURCES}) +qt_add_executable( + quran-companion + MANUAL_FINALIZATION + ${PROJECT_SOURCES} + src/utils/downloadjob.h + src/types/downloadtask.h + src/utils/surahjob.h + src/utils/surahjob.cpp + src/types/recitationtask.h + src/types/recitationtask.cpp + src/utils/taskdownloader.h + src/utils/taskdownloader.cpp + src/types/tafsirtask.h + src/types/tafsirtask.cpp + src/types/translationtask.h + src/types/translationtask.cpp + src/types/qcftask.h + src/types/qcftask.cpp + src/utils/contentjob.h + src/utils/contentjob.cpp + src/utils/qcfjob.h + src/utils/qcfjob.cpp + src/utils/jobmanager.h + src/utils/jobmanager.cpp) target_link_libraries( quran-companion PRIVATE Qt6::Widgets Qt6::Sql Qt6::Multimedia Qt6::Network diff --git a/src/types/downloadtask.cpp b/src/types/downloadtask.cpp new file mode 100644 index 00000000..5603570b --- /dev/null +++ b/src/types/downloadtask.cpp @@ -0,0 +1,31 @@ +#include "downloadtask.h" + +DownloadTask::DownloadTask(const QUrl& url, const QFileInfo& dest) + : m_url(url) + , m_dest(dest) +{ +} + +QUrl +DownloadTask::url() const +{ + return m_url; +} + +QFileInfo +DownloadTask::dest() const +{ + return m_dest; +} + +QPointer +DownloadTask::reply() const +{ + return m_reply; +} + +void +DownloadTask::setReply(QPointer newReply) +{ + m_reply = newReply; +} diff --git a/src/types/downloadtask.h b/src/types/downloadtask.h new file mode 100644 index 00000000..af12c3c3 --- /dev/null +++ b/src/types/downloadtask.h @@ -0,0 +1,17 @@ +#ifndef DOWNLOADTASK_H +#define DOWNLOADTASK_H + +#include +#include +#include +#include + +class DownloadTask +{ +public: + virtual QUrl url() const = 0; + virtual QFileInfo destination() const = 0; + virtual ~DownloadTask(){}; +}; + +#endif // DOWNLOADTASK_H diff --git a/src/types/qcftask.cpp b/src/types/qcftask.cpp new file mode 100644 index 00000000..78f53e81 --- /dev/null +++ b/src/types/qcftask.cpp @@ -0,0 +1,38 @@ +#include "qcftask.h" + +QcfTask::QcfTask(int page) + : m_page(page) +{ +} + +QcfTask::QcfTask(const QcfTask &other) +{ + m_page = other.m_page; +} + +QcfTask::QcfTask(const QcfTask &&other) +{ + m_page = other.m_page; +} + +QcfTask &QcfTask::operator=(const QcfTask other) +{ + m_page = other.m_page; + return *this; +} + +QUrl +QcfTask::url() const +{ + return QUrl::fromEncoded( + ("https://github.com/0xzer0x/quran-companion/raw/main/extras/QCFV2/QCF2" + + QString::number(m_page).rightJustified(3, '0') + ".ttf") + .toLatin1()); +} + +QFileInfo +QcfTask::destination() const +{ + return QFileInfo(m_downloadsDir->absoluteFilePath( + "QCFV2/QCF2" + QString::number(m_page).rightJustified(3, '0') + ".ttf")); +} diff --git a/src/types/qcftask.h b/src/types/qcftask.h new file mode 100644 index 00000000..029ee218 --- /dev/null +++ b/src/types/qcftask.h @@ -0,0 +1,24 @@ +#ifndef QCFTASK_H +#define QCFTASK_H + +#include "../utils/dirmanager.h" +#include "downloadtask.h" + +class QcfTask : public DownloadTask +{ +public: + QcfTask(int page); + QcfTask(const QcfTask& other); + QcfTask(const QcfTask&& other); + + QcfTask& operator=(const QcfTask other); + + QUrl url() const override; + QFileInfo destination() const override; + +private: + QSharedPointer m_downloadsDir = DirManager::downloadsDir; + int m_page; +}; + +#endif // QCFTASK_H diff --git a/src/types/recitationtask.cpp b/src/types/recitationtask.cpp new file mode 100644 index 00000000..934925e9 --- /dev/null +++ b/src/types/recitationtask.cpp @@ -0,0 +1,64 @@ +#include "recitationtask.h" + +RecitationTask::RecitationTask() + : m_reciter(-1) + , m_surah(-1) + , m_verse(-1) +{ +} + +RecitationTask::RecitationTask(int reciter, int surah, int verse) + : m_reciter(reciter) + , m_surah(surah) + , m_verse(verse) +{ +} + +RecitationTask::RecitationTask(const RecitationTask& other) +{ + m_reciter = other.m_reciter; + m_surah = other.m_surah; + m_verse = other.m_verse; +} + +RecitationTask::RecitationTask(const RecitationTask&& other) +{ + m_reciter = other.m_reciter; + m_surah = other.m_surah; + m_verse = other.m_verse; +} + +RecitationTask& +RecitationTask::operator=(RecitationTask other) +{ + m_reciter = other.m_reciter; + m_surah = other.m_surah; + m_verse = other.m_verse; + return *this; +} + +QUrl +RecitationTask::url() const +{ + const Reciter& r = *m_reciters.at(m_reciter); + QString url = r.baseUrl(); + if (r.useId()) + url.append(QString::number(m_dbMgr->getVerseId(m_surah, m_verse)) + ".mp3"); + else + url.append(QString::number(m_surah).rightJustified(3, '0') + + QString::number(m_verse).rightJustified(3, '0') + ".mp3"); + + return QUrl::fromEncoded(url.toLatin1()); +} + +QFileInfo +RecitationTask::destination() const +{ + static const QString path = "recitations/%0/%1.mp3"; + return QFileInfo(m_downloadsDir->absoluteFilePath( + path.arg(m_reciters.at(m_reciter)->baseDirName(), + QString::number(m_surah).rightJustified(3, '0') + + QString::number(m_verse).rightJustified(3, '0')))); +} + +RecitationTask::~RecitationTask() {} diff --git a/src/types/recitationtask.h b/src/types/recitationtask.h new file mode 100644 index 00000000..9b7f6986 --- /dev/null +++ b/src/types/recitationtask.h @@ -0,0 +1,32 @@ +#ifndef RECITATIONTASK_H +#define RECITATIONTASK_H + +#include "../utils/dbmanager.h" +#include "../utils/dirmanager.h" +#include "downloadtask.h" +#include "reciter.h" + +class RecitationTask : public DownloadTask +{ +public: + RecitationTask(); + RecitationTask(int reciter, int surah, int verse); + RecitationTask(const RecitationTask& other); + RecitationTask(const RecitationTask&& other); + ~RecitationTask(); + + RecitationTask& operator=(RecitationTask other); + + QUrl url() const override; + QFileInfo destination() const override; + +private: + QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_downloadsDir = DirManager::downloadsDir; + const QList>& m_reciters = Reciter::reciters; + int m_reciter; + int m_surah; + int m_verse; +}; + +#endif // RECITATIONTASK_H diff --git a/src/types/tafsirtask.cpp b/src/types/tafsirtask.cpp new file mode 100644 index 00000000..9ce52fe3 --- /dev/null +++ b/src/types/tafsirtask.cpp @@ -0,0 +1,39 @@ +#include "tafsirtask.h" + +TafsirTask::TafsirTask(int idx) + : m_idx(idx) +{ +} + +TafsirTask::TafsirTask(const TafsirTask& other) +{ + m_idx = other.m_idx; +} + +TafsirTask::TafsirTask(const TafsirTask&& other) +{ + m_idx = other.m_idx; +} + +TafsirTask& +TafsirTask::operator=(const TafsirTask other) +{ + m_idx = other.m_idx; + return *this; +} + +QUrl +TafsirTask::url() const +{ + return QUrl::fromEncoded( + ("https://github.com/0xzer0x/quran-companion/raw/main/extras/tafasir/" + + m_tafasir.at(m_idx)->filename()) + .toLatin1()); +} + +QFileInfo +TafsirTask::destination() const +{ + return QFileInfo(m_downloadsDir->absoluteFilePath( + "tafasir/" + m_tafasir.at(m_idx)->filename())); +} diff --git a/src/types/tafsirtask.h b/src/types/tafsirtask.h new file mode 100644 index 00000000..aead0730 --- /dev/null +++ b/src/types/tafsirtask.h @@ -0,0 +1,26 @@ +#ifndef TAFSIRTASK_H +#define TAFSIRTASK_H + +#include "../utils/dirmanager.h" +#include "downloadtask.h" +#include "tafsir.h" + +class TafsirTask : public DownloadTask +{ +public: + TafsirTask(int idx); + TafsirTask(const TafsirTask& other); + TafsirTask(const TafsirTask&& other); + + TafsirTask& operator=(const TafsirTask other); + + QUrl url() const override; + QFileInfo destination() const override; + +private: + QSharedPointer m_downloadsDir = DirManager::downloadsDir; + const QList>& m_tafasir = Tafsir::tafasir; + int m_idx; +}; + +#endif // TAFSIRTASK_H diff --git a/src/types/translationtask.cpp b/src/types/translationtask.cpp new file mode 100644 index 00000000..1e405430 --- /dev/null +++ b/src/types/translationtask.cpp @@ -0,0 +1,39 @@ +#include "translationtask.h" + +TranslationTask::TranslationTask(int idx) + : m_idx(idx) +{ +} + +TranslationTask::TranslationTask(const TranslationTask& other) +{ + m_idx = other.m_idx; +} + +TranslationTask::TranslationTask(const TranslationTask&& other) +{ + m_idx = other.m_idx; +} + +TranslationTask& +TranslationTask::operator=(const TranslationTask other) +{ + m_idx = other.m_idx; + return *this; +} + +QUrl +TranslationTask::url() const +{ + return QUrl::fromEncoded(("https://github.com/0xzer0x/quran-companion/raw/" + "main/extras/translations/" + + m_translations.at(m_idx)->filename()) + .toLatin1()); +} + +QFileInfo +TranslationTask::destination() const +{ + return QFileInfo(m_downloadsDir->absoluteFilePath( + "translations/" + m_translations.at(m_idx)->filename())); +} diff --git a/src/types/translationtask.h b/src/types/translationtask.h new file mode 100644 index 00000000..fa35ab60 --- /dev/null +++ b/src/types/translationtask.h @@ -0,0 +1,27 @@ +#ifndef TRANSLATIONTASK_H +#define TRANSLATIONTASK_H + +#include "../utils/dirmanager.h" +#include "downloadtask.h" +#include "translation.h" + +class TranslationTask : public DownloadTask +{ +public: + TranslationTask(int idx); + TranslationTask(const TranslationTask& other); + TranslationTask(const TranslationTask&& other); + + TranslationTask& operator=(const TranslationTask other); + + QUrl url() const override; + QFileInfo destination() const override; + +private: + QSharedPointer m_downloadsDir = DirManager::downloadsDir; + const QList>& m_translations = + Translation::translations; + int m_idx; +}; + +#endif // TRANSLATIONTASK_H diff --git a/src/utils/contentjob.cpp b/src/utils/contentjob.cpp new file mode 100644 index 00000000..07caec4e --- /dev/null +++ b/src/utils/contentjob.cpp @@ -0,0 +1,77 @@ +#include "contentjob.h" +#include "../types/tafsirtask.h" +#include "../types/translationtask.h" + +ContentJob::ContentJob(Type type, int idx) + : m_idx(idx) + , m_type(type) + , m_isDownloading(false) + , m_taskDlr(new TaskDownloader(this)) +{ + if (type == DownloadJob::TafsirFile) + m_task = new TafsirTask(idx); + else if (type == DownloadJob::TranslationFile) + m_task = new TranslationTask(idx); + + connect(m_taskDlr, &TaskDownloader::completed, this, &DownloadJob::finished); + connect(m_taskDlr, &TaskDownloader::taskError, this, &DownloadJob::failed); + connect(m_taskDlr, + &TaskDownloader::downloadSpeedUpdated, + this, + &DownloadJob::downloadSpeedUpdated); +} + +void +ContentJob::start() +{ + if (m_isDownloading) + return; + m_isDownloading = true; + m_taskDlr->process(m_task, &m_netMgr); +} + +void +ContentJob::stop() +{ + if (!m_isDownloading) + return; + m_taskDlr->cancel(); + m_isDownloading = false; +} + +bool +ContentJob::isDownloading() +{ + return m_isDownloading; +} + +int +ContentJob::completed() +{ + return m_taskDlr->bytes(); +} + +int +ContentJob::total() +{ + return m_taskDlr->total(); +} + +DownloadJob::Type +ContentJob::type() +{ + return m_type; +} + +QString +ContentJob::name() +{ + return m_type == DownloadJob::TafsirFile + ? m_tafasir.at(m_idx)->displayName() + : m_translations.at(m_idx)->displayName(); +} + +ContentJob::~ContentJob() +{ + delete m_task; +} diff --git a/src/utils/contentjob.h b/src/utils/contentjob.h new file mode 100644 index 00000000..53ffb6d0 --- /dev/null +++ b/src/utils/contentjob.h @@ -0,0 +1,35 @@ +#ifndef CONTENTJOB_H +#define CONTENTJOB_H + +#include "../types/tafsir.h" +#include "../types/translation.h" +#include "downloadjob.h" +#include "taskdownloader.h" + +class ContentJob : public DownloadJob +{ +public: + ContentJob(Type type, int idx); + ~ContentJob(); + + void start() override; + void stop() override; + bool isDownloading() override; + int completed() override; + int total() override; + Type type() override; + QString name() override; + +private: + QList>& m_tafasir = Tafsir::tafasir; + QList>& m_translations = + Translation::translations; + QPointer m_taskDlr; + QNetworkAccessManager m_netMgr; + DownloadTask* m_task; + Type m_type; + bool m_isDownloading; + int m_idx; +}; + +#endif // CONTENTJOB_H diff --git a/src/utils/downloadjob.h b/src/utils/downloadjob.h new file mode 100644 index 00000000..b8481a8f --- /dev/null +++ b/src/utils/downloadjob.h @@ -0,0 +1,33 @@ +#ifndef DOWNLOADJOB_H +#define DOWNLOADJOB_H + +#include + +class DownloadJob : public QObject +{ + Q_OBJECT +public: + enum Type + { + TafsirFile, + TranslationFile, + Qcf, + Recitation + }; + virtual void start() = 0; + virtual void stop() = 0; + virtual bool isDownloading() = 0; + virtual int completed() = 0; + virtual int total() = 0; + virtual Type type() = 0; + virtual QString name() = 0; + +signals: + void aborted(); + void finished(); + void progressed(); + void failed(); + void downloadSpeedUpdated(int speed, QString unit); +}; + +#endif // DOWNLOADJOB_H diff --git a/src/utils/jobmanager.cpp b/src/utils/jobmanager.cpp new file mode 100644 index 00000000..478d5363 --- /dev/null +++ b/src/utils/jobmanager.cpp @@ -0,0 +1,111 @@ +#include "jobmanager.h" + +JobManager::JobManager(QObject* parent) + : QObject(parent) +{ +} + +void +JobManager::addJob(QPointer job) +{ + m_queue.enqueue(job); +} + +void +JobManager::start() +{ + if (m_isOn) + return; + m_isOn = true; + processJobs(); +} + +void +JobManager::stop() +{ + if (!m_isOn) + return; + m_isOn = false; + if (!m_active.isNull()) + m_active->stop(); + disconnectActive(); +} + +void +JobManager::processJobs() +{ + if (m_queue.isEmpty()) { + m_isOn = false; + return; + } + m_active = m_queue.dequeue(); + connectActive(); + m_active->start(); +} + +void +JobManager::connectActive() +{ + connect(m_active, &DownloadJob::failed, this, &JobManager::handleFailed); + connect(m_active, &DownloadJob::finished, this, &JobManager::handleCompleted); + connect( + m_active, &DownloadJob::progressed, this, &JobManager::handleProgressed); + connect(m_active, + &DownloadJob::downloadSpeedUpdated, + this, + &JobManager::downloadSpeedUpdated); +} + +void +JobManager::disconnectActive() +{ + disconnect(m_active, &DownloadJob::failed, this, &JobManager::handleFailed); + disconnect( + m_active, &DownloadJob::finished, this, &JobManager::handleCompleted); + disconnect( + m_active, &DownloadJob::progressed, this, &JobManager::handleProgressed); + disconnect(m_active, + &DownloadJob::downloadSpeedUpdated, + this, + &JobManager::downloadSpeedUpdated); +} + +void +JobManager::handleProgressed() +{ + emit jobProgressed(m_active); +} + +void +JobManager::handleFailed() +{ + emit jobFailed(m_active); + disconnectActive(); + processJobs(); +} + +void +JobManager::handleCompleted() +{ + emit jobCompleted(m_active); + disconnectActive(); + processJobs(); +} + +bool +JobManager::isOn() const +{ + return m_isOn; +} + +QPointer +JobManager::active() const +{ + return m_active; +} + +JobManager::~JobManager() +{ + if (!m_queue.isEmpty()) + qDeleteAll(m_queue); +} diff --git a/src/utils/jobmanager.h b/src/utils/jobmanager.h new file mode 100644 index 00000000..8a8ee725 --- /dev/null +++ b/src/utils/jobmanager.h @@ -0,0 +1,43 @@ +#ifndef JOBMANAGER_H +#define JOBMANAGER_H + +#include "downloadjob.h" +#include +#include +#include + +class JobManager : public QObject +{ + Q_OBJECT +public: + explicit JobManager(QObject* parent); + ~JobManager(); + + void start(); + void stop(); + void addJob(QPointer job); + + bool isOn() const; + QPointer active() const; + +signals: + void jobCompleted(QPointer job); + void jobFailed(QPointer job); + void jobProgressed(QPointer job); + void downloadSpeedUpdated(int speed, QString unit); + +private slots: + void handleProgressed(); + void handleFailed(); + void handleCompleted(); + void processJobs(); + +private: + void connectActive(); + void disconnectActive(); + QQueue> m_queue; + QPointer m_active; + bool m_isOn = false; +}; + +#endif // JOBMANAGER_H diff --git a/src/utils/qcfjob.cpp b/src/utils/qcfjob.cpp new file mode 100644 index 00000000..7b1df97d --- /dev/null +++ b/src/utils/qcfjob.cpp @@ -0,0 +1,115 @@ +#include "qcfjob.h" + +QcfJob::QcfJob() + : m_completed(0) + , m_active(0) + , m_isDownloading(false) + , m_taskDlr(new TaskDownloader(this)) +{ + connect(m_taskDlr, &TaskDownloader::completed, this, &QcfJob::taskFinished); + connect(m_taskDlr, &TaskDownloader::taskError, this, &QcfJob::taskFailed); + connect(m_taskDlr, + &TaskDownloader::downloadSpeedUpdated, + this, + &DownloadJob::downloadSpeedUpdated); +} + +void +QcfJob::enqueueTasks() +{ + for (int i = 1; i <= 604; i++) + m_queue.enqueue(QcfTask(i)); +} + +void +QcfJob::processTasks() +{ + if (m_queue.isEmpty()) { + m_isDownloading = false; + return; + } + + m_active = m_queue.dequeue(); + while (m_active.destination().exists()) { + m_completed++; + if (m_completed == 604) + emit DownloadJob::finished(); + + if (m_queue.isEmpty()) + return; + + m_active = m_queue.dequeue(); + } + + m_taskDlr->process(&m_active, &m_netMgr); +} + +void +QcfJob::taskFinished() +{ + m_completed++; + emit DownloadJob::progressed(); + if (m_completed == 604) + emit DownloadJob::finished(); + processTasks(); +} + +void +QcfJob::taskFailed() +{ + m_isDownloading = false; + m_queue.clear(); + emit DownloadJob::failed(); +} + +void +QcfJob::start() +{ + if (m_isDownloading) + return; + if (m_queue.isEmpty()) + enqueueTasks(); + m_isDownloading = true; + processTasks(); +} + +void +QcfJob::stop() +{ + if (!m_isDownloading) + return; + m_taskDlr->cancel(); + m_isDownloading = false; + m_queue.clear(); + emit DownloadJob::aborted(); +} + +bool +QcfJob::isDownloading() +{ + return m_isDownloading; +} + +int +QcfJob::completed() +{ + return m_completed; +} + +int +QcfJob::total() +{ + return 604; +} + +DownloadJob::Type +QcfJob::type() +{ + return DownloadJob::Qcf; +} + +QString +QcfJob::name() +{ + return tr("SettingsDialog", "QCFV2"); +} diff --git a/src/utils/qcfjob.h b/src/utils/qcfjob.h new file mode 100644 index 00000000..79dc1d59 --- /dev/null +++ b/src/utils/qcfjob.h @@ -0,0 +1,39 @@ +#ifndef QCFJOB_H +#define QCFJOB_H + +#include "../types/qcftask.h" +#include "downloadjob.h" +#include "taskdownloader.h" + +#include + +class QcfJob : public DownloadJob +{ +public: + QcfJob(); + + void start() override; + void stop() override; + bool isDownloading() override; + int completed() override; + int total() override; + Type type() override; + QString name() override; + + void enqueueTasks(); + +private slots: + void processTasks(); + void taskFinished(); + void taskFailed(); + +private: + QPointer m_taskDlr; + QQueue m_queue; + QNetworkAccessManager m_netMgr; + QcfTask m_active; + bool m_isDownloading; + int m_completed; +}; + +#endif // QCFJOB_H diff --git a/src/utils/surahjob.cpp b/src/utils/surahjob.cpp new file mode 100644 index 00000000..9c97bce3 --- /dev/null +++ b/src/utils/surahjob.cpp @@ -0,0 +1,130 @@ +#include "surahjob.h" + +SurahJob::SurahJob(int reciter, int surah) + : m_reciter(reciter) + , m_surah(surah) + , m_surahCount(m_dbMgr->getSurahVerseCount(surah)) + , m_taskDlr(new TaskDownloader(this)) +{ + connect(m_taskDlr, &TaskDownloader::completed, this, &SurahJob::taskFinished); + connect(m_taskDlr, &TaskDownloader::taskError, this, &SurahJob::taskFailed); + connect(m_taskDlr, + &TaskDownloader::downloadSpeedUpdated, + this, + &DownloadJob::downloadSpeedUpdated); +} + +void +SurahJob::enqueueTasks() +{ + for (int i = 1; i <= m_surahCount; i++) + m_queue.enqueue(RecitationTask(m_reciter, m_surah, i)); +} + +void +SurahJob::processTasks() +{ + if (m_queue.isEmpty()) { + m_isDownloading = false; + return; + } + + m_active = m_queue.dequeue(); + while (m_active.destination().exists()) { + m_completed++; + if (m_completed == m_surahCount) + emit DownloadJob::finished(); + + if (m_queue.isEmpty()) + return; + + m_active = m_queue.dequeue(); + } + + m_taskDlr->process(&m_active, &m_netMgr); +} + +void +SurahJob::taskFinished() +{ + m_completed++; + emit DownloadJob::progressed(); + if (m_completed == m_surahCount) + emit DownloadJob::finished(); + processTasks(); +} + +void +SurahJob::start() +{ + if (m_isDownloading) + return; + if (m_queue.isEmpty()) + enqueueTasks(); + m_isDownloading = true; + processTasks(); +} + +void +SurahJob::stop() +{ + if (!m_isDownloading) + return; + m_taskDlr->cancel(); + m_isDownloading = false; + m_queue.clear(); + emit DownloadJob::aborted(); +} + +void +SurahJob::taskFailed() +{ + m_isDownloading = false; + m_queue.clear(); + emit DownloadJob::failed(); +} + +bool +SurahJob::isDownloading() +{ + return m_isDownloading; +} + +int +SurahJob::completed() +{ + return m_completed; +} + +int +SurahJob::total() +{ + return m_surahCount; +} + +DownloadJob::Type +SurahJob::type() +{ + return DownloadJob::Recitation; +} + +QString +SurahJob::name() +{ + return m_reciters.at(m_reciter)->displayName() + " - " + + m_dbMgr->surahNameList().at(m_surah - 1); +} + +int +SurahJob::reciter() const +{ + return m_reciter; +} + +int +SurahJob::surah() const +{ + return m_surah; +} + +SurahJob::~SurahJob() {} diff --git a/src/utils/surahjob.h b/src/utils/surahjob.h new file mode 100644 index 00000000..a0a39423 --- /dev/null +++ b/src/utils/surahjob.h @@ -0,0 +1,47 @@ +#ifndef SURAHJOB_H +#define SURAHJOB_H + +#include "../types/recitationtask.h" +#include "downloadjob.h" +#include "taskdownloader.h" +#include +#include + +class SurahJob : public DownloadJob +{ +public: + SurahJob(int reciter, int surah); + ~SurahJob(); + + void start() override; + void stop() override; + bool isDownloading() override; + int completed() override; + int total() override; + Type type() override; + QString name() override; + + void enqueueTasks(); + int reciter() const; + int surah() const; + +private slots: + void processTasks(); + void taskFinished(); + void taskFailed(); + +private: + QSharedPointer m_dbMgr = DBManager::current(); + QList>& m_reciters = Reciter::reciters; + QPointer m_taskDlr; + QQueue m_queue; + QNetworkAccessManager m_netMgr; + RecitationTask m_active; + bool m_isDownloading; + int m_reciter; + int m_surah; + int m_completed; + int m_surahCount; +}; + +#endif // SURAHJOB_H diff --git a/src/utils/taskdownloader.cpp b/src/utils/taskdownloader.cpp new file mode 100644 index 00000000..94453d0a --- /dev/null +++ b/src/utils/taskdownloader.cpp @@ -0,0 +1,116 @@ +#include "taskdownloader.h" +#include + +TaskDownloader::TaskDownloader(QObject* parent) + : QObject(parent) + , m_reply(nullptr) + , m_task(nullptr) + , m_bytes(0) + , m_total(0) +{ +} + +void +TaskDownloader::process(DownloadTask* task, QNetworkAccessManager* manager) +{ + if (m_reply) { + disconnect(m_reply, + &QNetworkReply::downloadProgress, + this, + &TaskDownloader::taskProgress); + disconnect( + m_reply, &QNetworkReply::finished, this, &TaskDownloader::finish); + } + + QNetworkRequest req(m_task->url()); + m_reply = manager->get(req); + m_reply->ignoreSslErrors(); + m_startTime = QTime::currentTime(); + + connect(m_reply, + &QNetworkReply::downloadProgress, + this, + &TaskDownloader::taskProgress); + connect(m_reply, &QNetworkReply::finished, this, &TaskDownloader::finish); +} + +void +TaskDownloader::cancel() +{ + m_reply->abort(); + m_reply->close(); +} + +void +TaskDownloader::taskProgress(qint64 bytes, qint64 total) +{ + m_bytes = bytes; + m_total = total; + int secs = m_startTime.secsTo(QTime::currentTime()); + if (secs < 1) + secs = 1; + + int speedPerSec = bytes / secs; + QString unit = qApp->translate("DownloadManager", "bytes"); + if (speedPerSec >= 1024) { + unit = qApp->translate("DownloadManager", "KB"); + speedPerSec /= 1024; + } + if (speedPerSec >= 1024) { + unit = qApp->translate("DownloadManager", "MB"); + speedPerSec /= 1024; + } + + emit downloadSpeedUpdated(speedPerSec, unit); +} + +void +TaskDownloader::finish() +{ + if (m_reply->error() != QNetworkReply::NoError) + return handleError(m_reply->error()); + saveFile(); + emit completed(); +} + +void +TaskDownloader::handleError(QNetworkReply::NetworkError err) +{ + switch (err) { + case QNetworkReply::OperationCanceledError: + qInfo() << m_reply->errorString(); + break; + default: + qInfo() << m_reply->errorString(); + emit taskError(); + } +} + +bool +TaskDownloader::saveFile() +{ + QFile localFile(m_task->destination().absoluteFilePath()); + if (!localFile.open(QIODevice::WriteOnly)) { + qWarning() << "Couldn't open file:" << m_task->destination(); + return false; + } + + const QByteArray fdata = m_reply->readAll(); + m_reply->close(); + + localFile.write(fdata); + localFile.close(); + return true; +} + +int +TaskDownloader::bytes() const +{ + return m_bytes; +} + +int +TaskDownloader::total() const +{ + return m_total; +} diff --git a/src/utils/taskdownloader.h b/src/utils/taskdownloader.h new file mode 100644 index 00000000..746cdc26 --- /dev/null +++ b/src/utils/taskdownloader.h @@ -0,0 +1,39 @@ +#ifndef TASKDOWNLOADER_H +#define TASKDOWNLOADER_H + +#include "../types/downloadtask.h" +#include +#include +#include + +class TaskDownloader : public QObject +{ + Q_OBJECT +public: + explicit TaskDownloader(QObject* parent); + void process(DownloadTask* task, QNetworkAccessManager* manager); + void cancel(); + + int bytes() const; + int total() const; + +signals: + void downloadSpeedUpdated(int speed, QString unit); + void taskError(); + void completed(); + +private slots: + void taskProgress(qint64 bytes, qint64 total); + void handleError(QNetworkReply::NetworkError err); + bool saveFile(); + void finish(); + +private: + QTime m_startTime; + DownloadTask* m_task; + QNetworkReply* m_reply; + int m_bytes; + int m_total; +}; + +#endif // TASKDOWNLOADER_H diff --git a/src/utils/translationjob.cpp b/src/utils/translationjob.cpp new file mode 100644 index 00000000..e231b680 --- /dev/null +++ b/src/utils/translationjob.cpp @@ -0,0 +1,59 @@ +#include "translationjob.h" + +TranslationJob::TranslationJob(int idx) + : m_idx(idx) + , m_task(TranslationTask(idx)) + , m_taskDlr(new TaskDownloader(this)) +{ + connect(m_taskDlr, &TaskDownloader::completed, this, &DownloadJob::finished); + connect(m_taskDlr, &TaskDownloader::taskError, this, &DownloadJob::failed); + connect(m_taskDlr, + &TaskDownloader::downloadSpeedUpdated, + this, + &DownloadJob::downloadSpeedUpdated); +} + +void +TranslationJob::start() +{ + if (m_isDownloading) + return; + m_isDownloading = true; + m_taskDlr->process(&m_task, &m_netMgr); +} + +void +TranslationJob::stop() +{ + if (!m_isDownloading) + return; + m_taskDlr->cancel(); + m_isDownloading = false; +} + +bool +TranslationJob::isDownloading() +{ +} + +int +TranslationJob::completed() +{ +} + +int +TranslationJob::total() +{ +} + +DownloadJob::Type +TranslationJob::type() +{ +} + +QString +TranslationJob::name() +{ +} + +TranslationJob::~TranslationJob() {} diff --git a/src/utils/translationjob.h b/src/utils/translationjob.h new file mode 100644 index 00000000..befe0147 --- /dev/null +++ b/src/utils/translationjob.h @@ -0,0 +1,33 @@ +#ifndef TRANSLATIONJOB_H +#define TRANSLATIONJOB_H + +#include "../types/translation.h" +#include "../types/translationtask.h" +#include "downloadjob.h" +#include "taskdownloader.h" + +class TranslationJob : public DownloadJob +{ +public: + TranslationJob(int idx); + ~TranslationJob(); + + void start() override; + void stop() override; + bool isDownloading() override; + int completed() override; + int total() override; + Type type() override; + QString name() override; + +private: + QList>& m_translations = + Translation::translations; + QPointer m_taskDlr; + QNetworkAccessManager m_netMgr; + TranslationTask m_task; + bool m_isDownloading; + int m_idx; +}; + +#endif // TRANSLATIONJOB_H From ac5b6126acb105b7ec6ecf4a298e47cf54e947d8 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sat, 24 Feb 2024 21:04:14 +0200 Subject: [PATCH 19/51] refactor: organize code into directories and finish refactoring downloader module of the app --- CMakeLists.txt | 111 +++--- src/core/mainwindow.cpp | 103 ++---- src/core/mainwindow.h | 52 ++- src/core/playercontrols.cpp | 4 +- src/core/playercontrols.h | 4 +- src/core/quranreader.cpp | 8 +- src/core/quranreader.h | 8 +- src/{widgets => dialogs}/aboutdialog.cpp | 0 src/{widgets => dialogs}/aboutdialog.h | 2 +- src/{widgets => dialogs}/aboutdialog.ui | 0 src/{core => dialogs}/bookmarksdialog.cpp | 4 +- src/{core => dialogs}/bookmarksdialog.h | 6 +- src/{core => dialogs}/bookmarksdialog.ui | 0 src/{core => dialogs}/contentdialog.cpp | 6 +- src/{core => dialogs}/contentdialog.h | 4 +- src/{core => dialogs}/contentdialog.ui | 0 src/{core => dialogs}/copydialog.cpp | 0 src/{core => dialogs}/copydialog.h | 4 +- src/{core => dialogs}/copydialog.ui | 0 src/{core => dialogs}/downloaderdialog.cpp | 148 ++++---- src/{core => dialogs}/downloaderdialog.h | 25 +- src/{core => dialogs}/downloaderdialog.ui | 0 src/{core => dialogs}/khatmahdialog.cpp | 9 +- src/{core => dialogs}/khatmahdialog.h | 6 +- src/{core => dialogs}/khatmahdialog.ui | 0 src/{core => dialogs}/searchdialog.cpp | 6 +- src/{core => dialogs}/searchdialog.h | 6 +- src/{core => dialogs}/searchdialog.ui | 0 src/{core => dialogs}/settingsdialog.cpp | 6 +- src/{core => dialogs}/settingsdialog.h | 6 +- src/{core => dialogs}/settingsdialog.ui | 0 src/{widgets => dialogs}/versedialog.cpp | 0 src/{widgets => dialogs}/versedialog.h | 6 +- src/{widgets => dialogs}/versedialog.ui | 0 src/{utils => downloader}/contentjob.cpp | 11 +- src/{utils => downloader}/contentjob.h | 10 +- src/{utils => downloader}/jobmanager.cpp | 24 ++ src/{utils => downloader}/jobmanager.h | 5 +- src/{utils => downloader}/qcfjob.cpp | 7 +- src/{utils => downloader}/qcfjob.h | 5 +- src/{types => downloader}/qcftask.cpp | 0 src/{types => downloader}/qcftask.h | 4 +- src/{types => downloader}/recitationtask.cpp | 0 src/{types => downloader}/recitationtask.h | 8 +- src/{utils => downloader}/surahjob.cpp | 4 +- src/{utils => downloader}/surahjob.h | 5 +- src/{types => downloader}/tafsirtask.cpp | 0 src/{types => downloader}/tafsirtask.h | 6 +- src/{utils => downloader}/taskdownloader.cpp | 23 +- src/{utils => downloader}/taskdownloader.h | 4 +- src/{utils => downloader}/translationjob.cpp | 0 src/{utils => downloader}/translationjob.h | 0 src/{types => downloader}/translationtask.cpp | 0 src/{types => downloader}/translationtask.h | 6 +- src/{utils => interfaces}/downloadjob.h | 0 src/{types => interfaces}/downloadtask.h | 0 src/types/downloadtask.cpp | 31 -- src/types/reciter.cpp | 2 +- src/types/tafsir.cpp | 2 +- src/types/translation.cpp | 2 +- src/types/verse.h | 4 +- src/utils/dbmanager.h | 8 +- src/utils/downloadmanager.cpp | 315 ------------------ src/utils/downloadmanager.h | 281 ---------------- src/utils/settings.cpp | 2 +- src/utils/shortcuthandler.cpp | 2 +- src/utils/verseplayer.h | 4 +- src/utils/versionchecker.cpp | 72 ++++ src/utils/versionchecker.h | 36 ++ src/widgets/betaqaviewer.h | 2 +- src/widgets/downloadprogressbar.cpp | 16 +- src/widgets/downloadprogressbar.h | 8 +- src/widgets/notificationpopup.cpp | 29 +- src/widgets/notificationpopup.h | 13 +- src/widgets/quranpagebrowser.cpp | 4 +- src/widgets/quranpagebrowser.h | 2 +- 76 files changed, 484 insertions(+), 1007 deletions(-) rename src/{widgets => dialogs}/aboutdialog.cpp (100%) rename src/{widgets => dialogs}/aboutdialog.h (91%) rename src/{widgets => dialogs}/aboutdialog.ui (100%) rename src/{core => dialogs}/bookmarksdialog.cpp (99%) rename src/{core => dialogs}/bookmarksdialog.h (98%) rename src/{core => dialogs}/bookmarksdialog.ui (100%) rename src/{core => dialogs}/contentdialog.cpp (98%) rename src/{core => dialogs}/contentdialog.h (98%) rename src/{core => dialogs}/contentdialog.ui (100%) rename src/{core => dialogs}/copydialog.cpp (100%) rename src/{core => dialogs}/copydialog.h (93%) rename src/{core => dialogs}/copydialog.ui (100%) rename src/{core => dialogs}/downloaderdialog.cpp (72%) rename src/{core => dialogs}/downloaderdialog.h (90%) rename src/{core => dialogs}/downloaderdialog.ui (100%) rename src/{core => dialogs}/khatmahdialog.cpp (96%) rename src/{core => dialogs}/khatmahdialog.h (96%) rename src/{core => dialogs}/khatmahdialog.ui (100%) rename src/{core => dialogs}/searchdialog.cpp (98%) rename src/{core => dialogs}/searchdialog.h (97%) rename src/{core => dialogs}/searchdialog.ui (100%) rename src/{core => dialogs}/settingsdialog.cpp (98%) rename src/{core => dialogs}/settingsdialog.h (98%) rename src/{core => dialogs}/settingsdialog.ui (100%) rename src/{widgets => dialogs}/versedialog.cpp (100%) rename src/{widgets => dialogs}/versedialog.h (93%) rename src/{widgets => dialogs}/versedialog.ui (100%) rename src/{utils => downloader}/contentjob.cpp (80%) rename src/{utils => downloader}/contentjob.h (83%) rename src/{utils => downloader}/jobmanager.cpp (73%) rename src/{utils => downloader}/jobmanager.h (86%) rename src/{utils => downloader}/qcfjob.cpp (91%) rename src/{utils => downloader}/qcfjob.h (88%) rename src/{types => downloader}/qcftask.cpp (100%) rename src/{types => downloader}/qcftask.h (85%) rename src/{types => downloader}/recitationtask.cpp (100%) rename src/{types => downloader}/recitationtask.h (85%) rename src/{utils => downloader}/surahjob.cpp (96%) rename src/{utils => downloader}/surahjob.h (92%) rename src/{types => downloader}/tafsirtask.cpp (100%) rename src/{types => downloader}/tafsirtask.h (84%) rename src/{utils => downloader}/taskdownloader.cpp (86%) rename src/{utils => downloader}/taskdownloader.h (87%) rename src/{utils => downloader}/translationjob.cpp (100%) rename src/{utils => downloader}/translationjob.h (100%) rename src/{types => downloader}/translationtask.cpp (100%) rename src/{types => downloader}/translationtask.h (85%) rename src/{utils => interfaces}/downloadjob.h (100%) rename src/{types => interfaces}/downloadtask.h (100%) delete mode 100644 src/types/downloadtask.cpp delete mode 100644 src/utils/downloadmanager.cpp delete mode 100644 src/utils/downloadmanager.h create mode 100644 src/utils/versionchecker.cpp create mode 100644 src/utils/versionchecker.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 788935e8..2b929068 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,8 @@ endif() add_subdirectory(third_party/QtAwesome) +include_directories(src) + set(PROJECT_SOURCES src/main.cpp src/types/verse.h @@ -47,27 +49,39 @@ set(PROJECT_SOURCES src/core/playercontrols.h src/core/playercontrols.cpp src/core/playercontrols.ui - src/core/searchdialog.h - src/core/searchdialog.cpp - src/core/searchdialog.ui - src/core/settingsdialog.cpp - src/core/settingsdialog.h - src/core/settingsdialog.ui - src/core/downloaderdialog.cpp - src/core/downloaderdialog.h - src/core/downloaderdialog.ui - src/core/bookmarksdialog.h - src/core/bookmarksdialog.cpp - src/core/bookmarksdialog.ui - src/core/contentdialog.h - src/core/contentdialog.cpp - src/core/contentdialog.ui - src/core/khatmahdialog.h - src/core/khatmahdialog.cpp - src/core/khatmahdialog.ui - src/core/copydialog.h - src/core/copydialog.cpp - src/core/copydialog.ui + src/dialogs/searchdialog.h + src/dialogs/searchdialog.cpp + src/dialogs/searchdialog.ui + src/dialogs/settingsdialog.cpp + src/dialogs/settingsdialog.h + src/dialogs/settingsdialog.ui + src/dialogs/downloaderdialog.cpp + src/dialogs/downloaderdialog.h + src/dialogs/downloaderdialog.ui + src/dialogs/bookmarksdialog.h + src/dialogs/bookmarksdialog.cpp + src/dialogs/bookmarksdialog.ui + src/dialogs/contentdialog.h + src/dialogs/contentdialog.cpp + src/dialogs/contentdialog.ui + src/dialogs/khatmahdialog.h + src/dialogs/khatmahdialog.cpp + src/dialogs/khatmahdialog.ui + src/dialogs/copydialog.h + src/dialogs/copydialog.cpp + src/dialogs/copydialog.ui + src/dialogs/aboutdialog.h + src/dialogs/aboutdialog.h + src/dialogs/aboutdialog.cpp + src/dialogs/aboutdialog.ui + src/dialogs/versedialog.h + src/dialogs/versedialog.cpp + src/dialogs/versedialog.ui + src/dialogs/aboutdialog.cpp + src/dialogs/aboutdialog.ui + src/dialogs/versedialog.h + src/dialogs/versedialog.cpp + src/dialogs/versedialog.ui src/utils/settings.h src/utils/settings.cpp src/utils/shortcuthandler.h @@ -76,8 +90,6 @@ set(PROJECT_SOURCES src/utils/dbmanager.cpp src/utils/verseplayer.h src/utils/verseplayer.cpp - src/utils/downloadmanager.h - src/utils/downloadmanager.cpp src/utils/systemtray.h src/utils/systemtray.cpp src/utils/logger.h @@ -88,6 +100,28 @@ set(PROJECT_SOURCES src/utils/stylemanager.cpp src/utils/fontmanager.h src/utils/fontmanager.cpp + src/interfaces/downloadjob.h + src/interfaces/downloadtask.h + src/downloader/surahjob.h + src/downloader/surahjob.cpp + src/downloader/recitationtask.h + src/downloader/recitationtask.cpp + src/downloader/taskdownloader.h + src/downloader/taskdownloader.cpp + src/downloader/tafsirtask.h + src/downloader/tafsirtask.cpp + src/downloader/translationtask.h + src/downloader/translationtask.cpp + src/downloader/qcftask.h + src/downloader/qcftask.cpp + src/downloader/contentjob.h + src/downloader/contentjob.cpp + src/downloader/qcfjob.h + src/downloader/qcfjob.cpp + src/downloader/jobmanager.h + src/downloader/jobmanager.cpp + src/utils/versionchecker.h + src/utils/versionchecker.cpp src/widgets/quranpagebrowser.h src/widgets/quranpagebrowser.cpp src/widgets/clickablelabel.cpp @@ -102,42 +136,13 @@ set(PROJECT_SOURCES src/widgets/inputfield.cpp src/widgets/shortcutdelegate.h src/widgets/shortcutdelegate.cpp - src/widgets/aboutdialog.h - src/widgets/aboutdialog.cpp - src/widgets/aboutdialog.ui src/widgets/betaqaviewer.h src/widgets/betaqaviewer.cpp src/widgets/betaqaviewer.ui - src/widgets/versedialog.h - src/widgets/versedialog.cpp - src/widgets/versedialog.ui resources.qrc qurancompanion.rc) -qt_add_executable( - quran-companion - MANUAL_FINALIZATION - ${PROJECT_SOURCES} - src/utils/downloadjob.h - src/types/downloadtask.h - src/utils/surahjob.h - src/utils/surahjob.cpp - src/types/recitationtask.h - src/types/recitationtask.cpp - src/utils/taskdownloader.h - src/utils/taskdownloader.cpp - src/types/tafsirtask.h - src/types/tafsirtask.cpp - src/types/translationtask.h - src/types/translationtask.cpp - src/types/qcftask.h - src/types/qcftask.cpp - src/utils/contentjob.h - src/utils/contentjob.cpp - src/utils/qcfjob.h - src/utils/qcfjob.cpp - src/utils/jobmanager.h - src/utils/jobmanager.cpp) +qt_add_executable(quran-companion MANUAL_FINALIZATION ${PROJECT_SOURCES}) target_link_libraries( quran-companion PRIVATE Qt6::Widgets Qt6::Sql Qt6::Multimedia Qt6::Network diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 7a9a34d6..1bc4857f 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -4,17 +4,16 @@ */ #include "mainwindow.h" -#include "../utils/stylemanager.h" -#include "../widgets/aboutdialog.h" -#include "khatmahdialog.h" +#include "dialogs/aboutdialog.h" +#include "dialogs/khatmahdialog.h" #include "ui_mainwindow.h" +#include "utils/stylemanager.h" #include using namespace fa; MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) , ui(new Ui::MainWindow) - , m_process(new QProcess(this)) , m_verseValidator(new QIntValidator(this)) { ui->setupUi(this); @@ -86,10 +85,11 @@ MainWindow::loadComponents() m_popup = new NotificationPopup(this); m_betaqaViewer = new BetaqaViewer(this); m_verseDlg = new VerseDialog(this); - m_downManPtr = new DownloadManager(this); m_cpyDlg = new CopyDialog(this); m_systemTray = new SystemTray(this); m_contentDlg = new ContentDialog(this); + m_jobMgr = new JobManager(this); + m_versionChecker = new VersionChecker(this); QHBoxLayout* controls = new QHBoxLayout(); QFrame* controlsFrame = new QFrame(this); @@ -118,59 +118,6 @@ MainWindow::loadComponents() ui->cmbPage->setCurrentIndex(m_currVerse->page() - 1); } -void -MainWindow::checkForUpdates() -{ -#if defined Q_OS_WIN - static QString updateTool = QApplication::applicationDirPath() + - QDir::separator() + "QCMaintenanceTool.exe"; - QFileInfo tool(updateTool); - if (tool.exists()) { - m_process->setWorkingDirectory(QApplication::applicationDirPath()); - m_process->start(updateTool, QStringList("ch")); - return; - } -#endif - - m_downManPtr->getLatestVersion(); -} - -void -MainWindow::updateProcessCallback() -{ - static QString updateTool = QApplication::applicationDirPath() + - QDir::separator() + "QCMaintenanceTool.exe"; - - QString output = m_process->readAll(); - QString displayText; - - if (output.contains("There are currently no updates available.")) { - displayText = tr("There are currently no updates available."); - - if (this->isVisible()) - QMessageBox::information(this, tr("Update info"), displayText); - else - m_systemTray->notify(tr("Update info"), displayText); - } - - else { - displayText = tr("Updates available, do you want to open the update tool?"); - if (this->isVisible()) { - QMessageBox::StandardButton btn = - QMessageBox::question(this, tr("Updates info"), displayText); - if (btn == QMessageBox::Yes) - m_process->startDetached(updateTool); - } - - else { - m_systemTray->notify( - tr("Update info"), - tr("Updates are available, use the maintainance tool to install " - "the latest updates.")); - } - } -} - void MainWindow::setupShortcuts() { @@ -271,10 +218,6 @@ MainWindow::setupConnections() &QAction::triggered, this, &MainWindow::actionAdvancedCopyTriggered); - connect( - ui->actionUpdates, &QAction::triggered, this, &MainWindow::checkForUpdates); - connect( - m_process, &QProcess::finished, this, &MainWindow::updateProcessCallback); connect(ui->actionBookmarks, &QAction::triggered, this, @@ -287,6 +230,10 @@ MainWindow::setupConnections() &QAction::triggered, this, &MainWindow::actionAboutTriggered); + connect(ui->actionUpdates, + &QAction::triggered, + this, + &MainWindow::actionUpdatesTriggered); // ########## page controls ########## // connect(ui->cmbPage, @@ -338,8 +285,8 @@ MainWindow::setupConnections() connect(m_systemTray, &SystemTray::hideWindow, this, &MainWindow::hide); connect(m_systemTray, &SystemTray::checkForUpdates, - this, - &MainWindow::checkForUpdates); + m_versionChecker, + &VersionChecker::checkUpdates); connect(m_systemTray, &SystemTray::openAbout, this, @@ -354,16 +301,16 @@ MainWindow::setupConnections() &QDockWidget::dockLocationChanged, m_popup, &NotificationPopup::setDockArea); - connect(m_downManPtr, - &DownloadManager::downloadCompleted, + connect(m_jobMgr, + &JobManager::jobCompleted, m_popup, &NotificationPopup::completedDownload); - connect(m_downManPtr, - &DownloadManager::downloadErrored, + connect(m_jobMgr, + &JobManager::jobFailed, m_popup, &NotificationPopup::downloadError); - connect(m_downManPtr, - &DownloadManager::latestVersionFound, + connect(m_versionChecker, + &VersionChecker::versionFound, m_popup, &NotificationPopup::checkUpdate); connect(m_cpyDlg, @@ -701,6 +648,12 @@ MainWindow::addCurrentToBookmarks() m_dbMgr->addBookmark(vInfo); } +void +MainWindow::actionUpdatesTriggered() +{ + m_versionChecker->checkUpdates(); +} + void MainWindow::missingQCF() { @@ -711,7 +664,7 @@ MainWindow::missingQCF() if (btn == QMessageBox::Yes) { actionDMTriggered(); - m_downloaderDlg->selectDownload(DownloadManager::QCF); + m_downloaderDlg->selectDownload(DownloadJob::Qcf); } } @@ -725,7 +678,7 @@ MainWindow::missingTafsir(int idx) if (btn == QMessageBox::Yes) { actionDMTriggered(); - m_downloaderDlg->selectDownload(DownloadManager::File, { 0, idx }); + m_downloaderDlg->selectDownload(DownloadJob::TafsirFile, { 0, idx }); } } @@ -739,7 +692,7 @@ MainWindow::missingTranslation(int idx) if (btn == QMessageBox::Yes) { actionDMTriggered(); - m_downloaderDlg->selectDownload(DownloadManager::File, { 1, idx }); + m_downloaderDlg->selectDownload(DownloadJob::TranslationFile, { 1, idx }); } } @@ -757,7 +710,7 @@ MainWindow::missingRecitationFileWarn(int reciterIdx, int surah) if (btn == QMessageBox::Yes) { actionDMTriggered(); - m_downloaderDlg->selectDownload(DownloadManager::Recitation, + m_downloaderDlg->selectDownload(DownloadJob::Recitation, { reciterIdx, surah }); } } @@ -772,7 +725,7 @@ void MainWindow::actionDMTriggered() { if (m_downloaderDlg == nullptr) - m_downloaderDlg = new DownloaderDialog(this, m_downManPtr); + m_downloaderDlg = new DownloaderDialog(this, m_jobMgr); m_downloaderDlg->show(); } diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index bb6e0cc3..2447cc2b 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -6,29 +6,27 @@ #ifndef MAINWINDOW_H #define MAINWINDOW_H -#include "../types/verse.h" -#include "../utils/dbmanager.h" -#include "../utils/shortcuthandler.h" -#include "../utils/systemtray.h" -#include "../utils/verseplayer.h" -#include "../widgets/betaqaviewer.h" -#include "../widgets/notificationpopup.h" -#include "../widgets/versedialog.h" -#include "bookmarksdialog.h" -#include "contentdialog.h" -#include "copydialog.h" -#include "downloaderdialog.h" -#include "khatmahdialog.h" +#include "dialogs/bookmarksdialog.h" +#include "dialogs/contentdialog.h" +#include "dialogs/copydialog.h" +#include "dialogs/downloaderdialog.h" +#include "dialogs/khatmahdialog.h" +#include "dialogs/searchdialog.h" +#include "dialogs/settingsdialog.h" +#include "dialogs/versedialog.h" #include "playercontrols.h" #include "quranreader.h" -#include "searchdialog.h" -#include "settingsdialog.h" +#include "types/verse.h" +#include "utils/dbmanager.h" +#include "utils/shortcuthandler.h" +#include "utils/systemtray.h" +#include "utils/verseplayer.h" +#include "utils/versionchecker.h" +#include "widgets/betaqaviewer.h" +#include "widgets/notificationpopup.h" #include -#include -#include #include #include -#include #include #include #include @@ -71,16 +69,6 @@ public slots: * MODIFIED */ void currentSurahChanged(); - /** - * @brief check if there are updates using the maintainence tool if available, - * otherwise check latest version on github - */ - void checkForUpdates(); - /** - * @brief callback function that handles the output of the maintainence tool - * update check - */ - void updateProcessCallback(); /** * @brief save the current position and window state of the application to the * settings file @@ -163,6 +151,10 @@ private slots: * @brief adds the current ::Verse to the bookmarks */ void addCurrentToBookmarks(); + /** + * @brief actionUpdatesTriggered + */ + void actionUpdatesTriggered(); /** * @brief open the SettingsDialog and connect settings change slots */ @@ -398,7 +390,7 @@ private slots: /** * @brief pointer to DownloadManager instance */ - QPointer m_downManPtr; + QPointer m_jobMgr; /** * @brief pointer to the surah card (betaqa) widget */ @@ -411,7 +403,7 @@ private slots: * @brief pointer to the QProcess instance of the maintainence tool that * checks for updates */ - QPointer m_process; + QPointer m_versionChecker; /** * @brief pointer to the validator for the editable verse combobox to ensure * the number entered is within the surah verse range diff --git a/src/core/playercontrols.cpp b/src/core/playercontrols.cpp index 71b5c99c..64888c10 100644 --- a/src/core/playercontrols.cpp +++ b/src/core/playercontrols.cpp @@ -1,7 +1,7 @@ #include "playercontrols.h" -#include "../utils/shortcuthandler.h" -#include "../utils/stylemanager.h" #include "ui_playercontrols.h" +#include "utils/shortcuthandler.h" +#include "utils/stylemanager.h" #include using namespace fa; diff --git a/src/core/playercontrols.h b/src/core/playercontrols.h index c28c044a..ca406631 100644 --- a/src/core/playercontrols.h +++ b/src/core/playercontrols.h @@ -1,9 +1,9 @@ #ifndef PLAYERCONTROLS_H #define PLAYERCONTROLS_H -#include "../types/verse.h" -#include "../utils/verseplayer.h" #include "quranreader.h" +#include "types/verse.h" +#include "utils/verseplayer.h" #include namespace Ui { diff --git a/src/core/quranreader.cpp b/src/core/quranreader.cpp index 6e2a2fcb..627d18e3 100644 --- a/src/core/quranreader.cpp +++ b/src/core/quranreader.cpp @@ -1,9 +1,9 @@ #include "quranreader.h" -#include "../utils/fontmanager.h" -#include "../utils/shortcuthandler.h" -#include "../utils/stylemanager.h" -#include "../widgets/clickablelabel.h" #include "ui_quranreader.h" +#include "utils/fontmanager.h" +#include "utils/shortcuthandler.h" +#include "utils/stylemanager.h" +#include "widgets/clickablelabel.h" #include using namespace fa; diff --git a/src/core/quranreader.h b/src/core/quranreader.h index 3f478dbb..6d885b51 100644 --- a/src/core/quranreader.h +++ b/src/core/quranreader.h @@ -1,10 +1,10 @@ #ifndef QURANREADER_H #define QURANREADER_H -#include "../types/verse.h" -#include "../utils/verseplayer.h" -#include "../widgets/quranpagebrowser.h" -#include "../widgets/verseframe.h" +#include "types/verse.h" +#include "utils/verseplayer.h" +#include "widgets/quranpagebrowser.h" +#include "widgets/verseframe.h" #include #include #include diff --git a/src/widgets/aboutdialog.cpp b/src/dialogs/aboutdialog.cpp similarity index 100% rename from src/widgets/aboutdialog.cpp rename to src/dialogs/aboutdialog.cpp diff --git a/src/widgets/aboutdialog.h b/src/dialogs/aboutdialog.h similarity index 91% rename from src/widgets/aboutdialog.h rename to src/dialogs/aboutdialog.h index 3af4363d..0fed0e77 100644 --- a/src/widgets/aboutdialog.h +++ b/src/dialogs/aboutdialog.h @@ -1,7 +1,7 @@ #ifndef ABOUTDIALOG_H #define ABOUTDIALOG_H -#include "../utils/settings.h" +#include "utils/settings.h" #include namespace Ui { diff --git a/src/widgets/aboutdialog.ui b/src/dialogs/aboutdialog.ui similarity index 100% rename from src/widgets/aboutdialog.ui rename to src/dialogs/aboutdialog.ui diff --git a/src/core/bookmarksdialog.cpp b/src/dialogs/bookmarksdialog.cpp similarity index 99% rename from src/core/bookmarksdialog.cpp rename to src/dialogs/bookmarksdialog.cpp index 40d02b84..5f429aea 100644 --- a/src/core/bookmarksdialog.cpp +++ b/src/dialogs/bookmarksdialog.cpp @@ -4,9 +4,9 @@ */ #include "bookmarksdialog.h" -#include "../utils/fontmanager.h" -#include "../utils/stylemanager.h" #include "ui_bookmarksdialog.h" +#include "utils/fontmanager.h" +#include "utils/stylemanager.h" #include BookmarksDialog::BookmarksDialog(QWidget* parent) diff --git a/src/core/bookmarksdialog.h b/src/dialogs/bookmarksdialog.h similarity index 98% rename from src/core/bookmarksdialog.h rename to src/dialogs/bookmarksdialog.h index 28596526..c353cf77 100644 --- a/src/core/bookmarksdialog.h +++ b/src/dialogs/bookmarksdialog.h @@ -6,9 +6,9 @@ #ifndef BOOKMARKSDIALOG_H #define BOOKMARKSDIALOG_H -#include "../types/verse.h" -#include "../utils/dbmanager.h" -#include "../utils/settings.h" +#include "types/verse.h" +#include "utils/dbmanager.h" +#include "utils/settings.h" #include #include #include diff --git a/src/core/bookmarksdialog.ui b/src/dialogs/bookmarksdialog.ui similarity index 100% rename from src/core/bookmarksdialog.ui rename to src/dialogs/bookmarksdialog.ui diff --git a/src/core/contentdialog.cpp b/src/dialogs/contentdialog.cpp similarity index 98% rename from src/core/contentdialog.cpp rename to src/dialogs/contentdialog.cpp index 24958b5e..369a7a37 100644 --- a/src/core/contentdialog.cpp +++ b/src/dialogs/contentdialog.cpp @@ -4,10 +4,10 @@ */ #include "contentdialog.h" -#include "../types/tafsir.h" -#include "../utils/fontmanager.h" -#include "../utils/stylemanager.h" +#include "types/tafsir.h" #include "ui_contentdialog.h" +#include "utils/fontmanager.h" +#include "utils/stylemanager.h" ContentDialog::ContentDialog(QWidget* parent) : QDialog(parent) diff --git a/src/core/contentdialog.h b/src/dialogs/contentdialog.h similarity index 98% rename from src/core/contentdialog.h rename to src/dialogs/contentdialog.h index 78816eb7..59bd47dd 100644 --- a/src/core/contentdialog.h +++ b/src/dialogs/contentdialog.h @@ -6,8 +6,8 @@ #ifndef CONTENTDIALOG_H #define CONTENTDIALOG_H -#include "../types/verse.h" -#include "../utils/dbmanager.h" +#include "types/verse.h" +#include "utils/dbmanager.h" #include #include #include diff --git a/src/core/contentdialog.ui b/src/dialogs/contentdialog.ui similarity index 100% rename from src/core/contentdialog.ui rename to src/dialogs/contentdialog.ui diff --git a/src/core/copydialog.cpp b/src/dialogs/copydialog.cpp similarity index 100% rename from src/core/copydialog.cpp rename to src/dialogs/copydialog.cpp diff --git a/src/core/copydialog.h b/src/dialogs/copydialog.h similarity index 93% rename from src/core/copydialog.h rename to src/dialogs/copydialog.h index d6997108..2c6cc25d 100644 --- a/src/core/copydialog.h +++ b/src/dialogs/copydialog.h @@ -1,8 +1,8 @@ #ifndef COPYDIALOG_H #define COPYDIALOG_H -#include "../types/verse.h" -#include "../utils/dbmanager.h" +#include "types/verse.h" +#include "utils/dbmanager.h" #include #include #include diff --git a/src/core/copydialog.ui b/src/dialogs/copydialog.ui similarity index 100% rename from src/core/copydialog.ui rename to src/dialogs/copydialog.ui diff --git a/src/core/downloaderdialog.cpp b/src/dialogs/downloaderdialog.cpp similarity index 72% rename from src/core/downloaderdialog.cpp rename to src/dialogs/downloaderdialog.cpp index 62c6f96c..3e41e2c8 100644 --- a/src/core/downloaderdialog.cpp +++ b/src/dialogs/downloaderdialog.cpp @@ -4,14 +4,17 @@ */ #include "downloaderdialog.h" -#include "../utils/stylemanager.h" +#include "downloader/contentjob.h" +#include "downloader/qcfjob.h" +#include "downloader/surahjob.h" #include "ui_downloaderdialog.h" +#include "utils/stylemanager.h" -DownloaderDialog::DownloaderDialog(QWidget* parent, DownloadManager* downloader) +DownloaderDialog::DownloaderDialog(QWidget* parent, JobManager* manager) : QDialog(parent) , ui(new Ui::DownloaderDialog) - , m_downloaderPtr{ downloader } - , m_surahDisplayNames{ m_dbMgr->surahNameList() } + , m_jobMgr(manager) + , m_surahDisplayNames(m_dbMgr->surahNameList()) { ui->setupUi(this); @@ -53,28 +56,24 @@ DownloaderDialog::setupConnections() this, &DownloaderDialog::clearQueue); - connect(m_downloaderPtr, - &DownloadManager::downloadCompleted, + connect(m_jobMgr, + &JobManager::jobCompleted, this, &DownloaderDialog::downloadCompleted); - - connect(m_downloaderPtr, - &DownloadManager::filesFound, + connect(m_jobMgr, + &JobManager::filesFound, this, &DownloaderDialog::downloadCompleted); - - connect(m_downloaderPtr, - &DownloadManager::downloadCanceled, + connect(m_jobMgr, + &JobManager::jobAborted, this, &DownloaderDialog::downloadAborted); - - connect(m_downloaderPtr, - &DownloadManager::downloadErrored, + connect(m_jobMgr, + &JobManager::jobFailed, this, &DownloaderDialog::topTaskDownloadError); - - connect(m_downloaderPtr, - &DownloadManager::downloadSpeedUpdated, + connect(m_jobMgr, + &JobManager::downloadSpeedUpdated, this, &DownloaderDialog::updateDownloadSpeed); } @@ -147,17 +146,17 @@ void DownloaderDialog::addToDownloading(int reciter, int surah) { // add surah to downloading tasks - QSet& downloading = m_downloadingTasks[reciter]; + QSet& downloading = m_downloadingSurahs[reciter]; downloading.insert(surah); } void DownloaderDialog::removeFromDownloading(int reciter, int surah) { - QSet& downloading = m_downloadingTasks[reciter]; + QSet& downloading = m_downloadingSurahs[reciter]; downloading.remove(surah); if (downloading.isEmpty()) - m_downloadingTasks.remove(reciter); + m_downloadingSurahs.remove(reciter); } void @@ -181,43 +180,42 @@ DownloaderDialog::addToQueue() enqueueSurah(parent, current + 1); // tafasir else if (i.data(Qt::UserRole).toString() == "tadb") { - QPair info(0, i.data(Qt::UserRole + 1).toInt()); - m_downloaderPtr->addToQueue(DownloadManager::File, info); - addTaskProgress(DownloadManager::File, info); + ContentJob* job = new ContentJob(DownloadJob::TafsirFile, + i.data(Qt::UserRole + 1).toInt()); + m_jobMgr->addJob(job); + addTaskProgress(job); } // translation else if (i.data(Qt::UserRole).toString() == "trdb") { - QPair info(1, i.data(Qt::UserRole + 1).toInt()); - m_downloaderPtr->addToQueue(DownloadManager::File, info); - addTaskProgress(DownloadManager::File, info); + ContentJob* job = new ContentJob(DownloadJob::TranslationFile, + i.data(Qt::UserRole + 1).toInt()); + m_jobMgr->addJob(job); + addTaskProgress(job); } // extras else if (i.data(Qt::UserRole).toString() == "qcf") { - m_downloaderPtr->addToQueue(DownloadManager::QCF); - addTaskProgress(DownloadManager::QCF); + QcfJob* job = new QcfJob(); + m_jobMgr->addJob(job); + addTaskProgress(job); } } setCurrentBar(); - m_downloaderPtr->startQueue(); + m_jobMgr->start(); } void -DownloaderDialog::addTaskProgress(DownloadType type, QPair info) +DownloaderDialog::addTaskProgress(QPointer job) { - int total = 0; + int total = job->total(); QString objName; - if (type == DownloadManager::Recitation) { - QString reciter = m_reciters.at(info.first)->displayName(); - QString surahName = m_surahDisplayNames.at(info.second - 1); + if (job->type() == DownloadJob::Recitation) { + SurahJob* sJob = qobject_cast(job); + QString reciter = m_reciters.at(sJob->reciter())->displayName(); + QString surahName = m_surahDisplayNames.at(sJob->surah() - 1); objName = reciter + tr(" // Surah: ") + surahName; - total = m_dbMgr->getSurahVerseCount(info.second); - } else if (type == DownloadManager::QCF) { - objName = qApp->translate("SettingsDialog", "QCF V2"); - total = 604; - } else if (type == DownloadManager::File) { - objName = info.first ? m_tr.at(info.second)->displayName() - : m_tafasir.at(info.second)->displayName(); + } else { + objName = job->name(); } QFrame* prgFrm = new QFrame(ui->scrollAreaWidgetContents); @@ -241,7 +239,8 @@ DownloaderDialog::addTaskProgress(DownloadType type, QPair info) downInfo->addWidget(downSpeed); prgFrm->layout()->addItem(downInfo); - DownloadProgressBar* dpb = new DownloadProgressBar(prgFrm, type, total); + DownloadProgressBar* dpb = + new DownloadProgressBar(prgFrm, job->type(), total); prgFrm->layout()->addWidget(dpb); m_frameLst.append(prgFrm); @@ -251,13 +250,15 @@ DownloaderDialog::addTaskProgress(DownloadType type, QPair info) void DownloaderDialog::enqueueSurah(int reciter, int surah) { - bool currentlyDownloading = m_downloadingTasks.value(reciter).contains(surah); + bool currentlyDownloading = + m_downloadingSurahs.value(reciter).contains(surah); if (currentlyDownloading) return; + SurahJob* sj = new SurahJob(reciter, surah); addToDownloading(reciter, surah); - addTaskProgress(DownloadManager::Recitation, QPair(reciter, surah)); - m_downloaderPtr->addToQueue(reciter, surah); + addTaskProgress(sj); + m_jobMgr->addJob(sj); } void @@ -272,8 +273,8 @@ DownloaderDialog::setCurrentBar() m_currentLb->parent()->objectName()); m_currentBar = m_frameLst.at(0)->findChild(); - connect(m_downloaderPtr, - &DownloadManager::downloadProgressed, + connect(m_jobMgr, + &JobManager::jobProgressed, m_currentBar, &DownloadProgressBar::updateProgress); } @@ -285,18 +286,19 @@ DownloaderDialog::updateDownloadSpeed(int value, QString unit) } void -DownloaderDialog::selectDownload(DownloadType type, QPair info) +DownloaderDialog::selectDownload(DownloadJob::Type type, QPair info) { QItemSelectionModel* selector = ui->treeView->selectionModel(); QModelIndex parent; QModelIndex task; - if (type == DownloadManager::Recitation) { + if (type == DownloadJob::Recitation) { parent = m_treeModel.index(info.first, 0); task = m_treeModel.index(info.second - 1, 0, parent); - } else if (type == DownloadManager::QCF) { + } else if (type == DownloadJob::Qcf) { parent = m_treeModel.index(m_treeModel.rowCount() - 1, 0); task = m_treeModel.index(0, 0, parent); - } else if (type == DownloadManager::File) { + } else if (type == DownloadJob::TafsirFile || + type == DownloadJob::TranslationFile) { parent = m_treeModel.index(m_treeModel.rowCount() - 2 - !info.first, 0); // remove default db indices from current index as defaults are not // downloadable @@ -319,8 +321,8 @@ DownloaderDialog::selectDownload(DownloadType type, QPair info) void DownloaderDialog::clearQueue() { - m_downloadingTasks.clear(); - m_downloaderPtr->stopQueue(); + m_downloadingSurahs.clear(); + m_jobMgr->stop(); if (!m_finishedFrames.isEmpty()) { qDeleteAll(m_finishedFrames); m_finishedFrames.clear(); @@ -330,14 +332,14 @@ DownloaderDialog::clearQueue() void DownloaderDialog::btnStopClicked() { - m_downloadingTasks.clear(); - m_downloaderPtr->stopQueue(); + m_downloadingSurahs.clear(); + m_jobMgr->stop(); } void DownloaderDialog::downloadAborted() { - m_downloadingTasks.clear(); + m_downloadingSurahs.clear(); if (!m_frameLst.isEmpty()) { qDeleteAll(m_frameLst); m_frameLst.clear(); @@ -345,40 +347,46 @@ DownloaderDialog::downloadAborted() } void -DownloaderDialog::downloadCompleted(DownloadType type, - const QList& metainfo) +DownloaderDialog::downloadCompleted(QPointer finished) { m_currentBar->setStyling(DownloadProgressBar::completed); m_currentLb->setText(m_currentLb->parent()->objectName()); m_currDownSpeedLb->setText(tr("Download Completed")); - disconnect(m_downloaderPtr, - &DownloadManager::downloadProgressed, + disconnect(m_jobMgr, + &JobManager::jobProgressed, m_currentBar, &DownloadProgressBar::updateProgress); - if (type == DownloadManager::Recitation) - removeFromDownloading(metainfo[0], metainfo[1]); - if (m_currentBar->maximum() == 1) + if (finished->type() == DownloadJob::Recitation) { + SurahJob* sj = qobject_cast(finished); + removeFromDownloading(sj->reciter(), sj->surah()); + } + if (finished->type() == DownloadJob::TafsirFile || + finished->type() == DownloadJob::TranslationFile) { + m_currentBar->setValue(1); + m_currentBar->setMaximum(1); m_currentBar->setFormat("1 / 1"); + } m_finishedFrames.append(m_frameLst.front()); m_frameLst.pop_front(); setCurrentBar(); } void -DownloaderDialog::topTaskDownloadError(DownloadType type, - const QList& metainfo) +DownloaderDialog::topTaskDownloadError(QPointer failed) { m_currentBar->setStyling(DownloadProgressBar::aborted); m_currentLb->setText(m_currentLb->parent()->objectName()); m_currDownSpeedLb->setText(tr("Download Failed")); - disconnect(m_downloaderPtr, - &DownloadManager::downloadProgressed, + disconnect(m_jobMgr, + &JobManager::jobProgressed, m_currentBar, &DownloadProgressBar::updateProgress); - if (type == DownloadManager::Recitation) - removeFromDownloading(metainfo[0], metainfo[1]); + if (failed->type() == DownloadJob::Recitation) { + SurahJob* sj = qobject_cast(failed); + removeFromDownloading(sj->reciter(), sj->surah()); + } m_finishedFrames.append(m_frameLst.front()); m_frameLst.pop_front(); setCurrentBar(); diff --git a/src/core/downloaderdialog.h b/src/dialogs/downloaderdialog.h similarity index 90% rename from src/core/downloaderdialog.h rename to src/dialogs/downloaderdialog.h index f4a0396b..8b501de7 100644 --- a/src/core/downloaderdialog.h +++ b/src/dialogs/downloaderdialog.h @@ -6,9 +6,10 @@ #ifndef DOWNLOADERDIALOG_H #define DOWNLOADERDIALOG_H -#include "../utils/dbmanager.h" -#include "../utils/downloadmanager.h" -#include "../widgets/downloadprogressbar.h" +#include "downloader/jobmanager.h" +#include "types/reciter.h" +#include "utils/dbmanager.h" +#include "widgets/downloadprogressbar.h" #include #include #include @@ -41,7 +42,7 @@ class DownloaderDialog : public QDialog * @param dbMan - pointer to DBManager instance */ explicit DownloaderDialog(QWidget* parent = nullptr, - DownloadManager* downloader = nullptr); + JobManager* manager = nullptr); ~DownloaderDialog(); public slots: @@ -69,14 +70,14 @@ public slots: * @param type - DownloadType of the download group * @param metainfo - QList of download group information */ - void downloadCompleted(DownloadType type, const QList& metainfo); + void downloadCompleted(QPointer finished); /** * @brief Callback function to update UI elements when the current active * download group fails * @param type - DownloadType of the download group * @param metainfo - QList of download group information */ - void topTaskDownloadError(DownloadType type, const QList& metainfo); + void topTaskDownloadError(QPointer failed); /** * @brief slot to update the displayed download speed in the currently active * download. @@ -89,7 +90,7 @@ public slots: * @param type - DownloadType of the download group to select * @param info - metainfo for the download task */ - void selectDownload(DownloadType type, + void selectDownload(DownloadJob::Type type, QPair info = QPair(0, 1)); /** * @brief Stops downloading tasks and clears the downloads scrollarea @@ -139,8 +140,7 @@ private slots: * @param info - download metainfo QPair used by DownloadType::Recitation and * DownloadType::File */ - void addTaskProgress(DownloadType type, - QPair info = QPair(-1, -1)); + void addTaskProgress(QPointer job); /** * @brief enqueue a surah to download * @param reciter - ::Globals::recitersList index for the reciter whose @@ -169,10 +169,7 @@ private slots: * verses are downloaded. */ QPointer m_currentBar; - /** - * @brief Pointer to DownloadManager instance. - */ - QPointer m_downloaderPtr; + QPointer m_jobMgr; /** * @brief Pointer to QLabel which contains the state and information for the * currently active download. @@ -206,7 +203,7 @@ private slots: * checked against this QHash in order to determine whether its a duplicate or * not. */ - QHash> m_downloadingTasks; + QHash> m_downloadingSurahs; }; #endif // DOWNLOADERDIALOG_H diff --git a/src/core/downloaderdialog.ui b/src/dialogs/downloaderdialog.ui similarity index 100% rename from src/core/downloaderdialog.ui rename to src/dialogs/downloaderdialog.ui diff --git a/src/core/khatmahdialog.cpp b/src/dialogs/khatmahdialog.cpp similarity index 96% rename from src/core/khatmahdialog.cpp rename to src/dialogs/khatmahdialog.cpp index ae3f6acb..6ba69f01 100644 --- a/src/core/khatmahdialog.cpp +++ b/src/dialogs/khatmahdialog.cpp @@ -1,7 +1,7 @@ #include "khatmahdialog.h" -#include "../utils/settings.h" -#include "../utils/stylemanager.h" #include "ui_khatmahdialog.h" +#include "utils/settings.h" +#include "utils/stylemanager.h" #include KhatmahDialog::KhatmahDialog(QWidget* parent) @@ -19,10 +19,11 @@ KhatmahDialog::KhatmahDialog(QWidget* parent) &KhatmahDialog::startNewKhatmah); } -QPointer KhatmahDialog::loadKhatmah(const int id) +QPointer +KhatmahDialog::loadKhatmah(const int id) { QList vInfo(3); - m_dbMgr->loadVerse(id, vInfo); + m_dbMgr->loadVerse(id, vInfo); QFrame* frame = new QFrame(ui->scrlDialogContent); // property used for styling frame->setProperty("khatmah", true); diff --git a/src/core/khatmahdialog.h b/src/dialogs/khatmahdialog.h similarity index 96% rename from src/core/khatmahdialog.h rename to src/dialogs/khatmahdialog.h index 26555261..03b28b69 100644 --- a/src/core/khatmahdialog.h +++ b/src/dialogs/khatmahdialog.h @@ -1,9 +1,9 @@ #ifndef KHATMAHDIALOG_H #define KHATMAHDIALOG_H -#include "../types/verse.h" -#include "../utils/dbmanager.h" -#include "../widgets/inputfield.h" +#include "types/verse.h" +#include "utils/dbmanager.h" +#include "widgets/inputfield.h" #include #include #include diff --git a/src/core/khatmahdialog.ui b/src/dialogs/khatmahdialog.ui similarity index 100% rename from src/core/khatmahdialog.ui rename to src/dialogs/khatmahdialog.ui diff --git a/src/core/searchdialog.cpp b/src/dialogs/searchdialog.cpp similarity index 98% rename from src/core/searchdialog.cpp rename to src/dialogs/searchdialog.cpp index 215a26f6..f53d15b2 100644 --- a/src/core/searchdialog.cpp +++ b/src/dialogs/searchdialog.cpp @@ -4,10 +4,10 @@ */ #include "searchdialog.h" -#include "../utils/fontmanager.h" -#include "../utils/stylemanager.h" -#include "../widgets/clickablelabel.h" #include "ui_searchdialog.h" +#include "utils/fontmanager.h" +#include "utils/stylemanager.h" +#include "widgets/clickablelabel.h" SearchDialog::SearchDialog(QWidget* parent) : QDialog(parent) diff --git a/src/core/searchdialog.h b/src/dialogs/searchdialog.h similarity index 97% rename from src/core/searchdialog.h rename to src/dialogs/searchdialog.h index 6363a3ac..6061fffa 100644 --- a/src/core/searchdialog.h +++ b/src/dialogs/searchdialog.h @@ -6,9 +6,9 @@ #ifndef SEARCHDIALOG_H #define SEARCHDIALOG_H -#include "../types/verse.h" -#include "../utils/dbmanager.h" -#include "../widgets/verseframe.h" +#include "types/verse.h" +#include "utils/dbmanager.h" +#include "widgets/verseframe.h" #include #include #include diff --git a/src/core/searchdialog.ui b/src/dialogs/searchdialog.ui similarity index 100% rename from src/core/searchdialog.ui rename to src/dialogs/searchdialog.ui diff --git a/src/core/settingsdialog.cpp b/src/dialogs/settingsdialog.cpp similarity index 98% rename from src/core/settingsdialog.cpp rename to src/dialogs/settingsdialog.cpp index f862a5bf..e57e3443 100644 --- a/src/core/settingsdialog.cpp +++ b/src/dialogs/settingsdialog.cpp @@ -4,10 +4,10 @@ */ #include "settingsdialog.h" -#include "../utils/fontmanager.h" -#include "../utils/stylemanager.h" -#include "../widgets/shortcutdelegate.h" #include "ui_settingsdialog.h" +#include "utils/fontmanager.h" +#include "utils/stylemanager.h" +#include "widgets/shortcutdelegate.h" SettingsDialog::SettingsDialog(QWidget* parent, VersePlayer* vPlayerPtr) : QDialog(parent) diff --git a/src/core/settingsdialog.h b/src/dialogs/settingsdialog.h similarity index 98% rename from src/core/settingsdialog.h rename to src/dialogs/settingsdialog.h index ed94d660..92c5cb8d 100644 --- a/src/core/settingsdialog.h +++ b/src/dialogs/settingsdialog.h @@ -6,9 +6,9 @@ #ifndef SETTINGSDIALOG_H #define SETTINGSDIALOG_H -#include "../utils/settings.h" -#include "../utils/shortcuthandler.h" -#include "../utils/verseplayer.h" +#include "utils/settings.h" +#include "utils/shortcuthandler.h" +#include "utils/verseplayer.h" #include #include #include diff --git a/src/core/settingsdialog.ui b/src/dialogs/settingsdialog.ui similarity index 100% rename from src/core/settingsdialog.ui rename to src/dialogs/settingsdialog.ui diff --git a/src/widgets/versedialog.cpp b/src/dialogs/versedialog.cpp similarity index 100% rename from src/widgets/versedialog.cpp rename to src/dialogs/versedialog.cpp diff --git a/src/widgets/versedialog.h b/src/dialogs/versedialog.h similarity index 93% rename from src/widgets/versedialog.h rename to src/dialogs/versedialog.h index a882cd73..46c7d1ab 100644 --- a/src/widgets/versedialog.h +++ b/src/dialogs/versedialog.h @@ -1,9 +1,9 @@ #ifndef VERSEDIALOG_H #define VERSEDIALOG_H -#include "../types/verse.h" -#include "../utils/dbmanager.h" -#include "../utils/dirmanager.h" +#include "types/verse.h" +#include "utils/dbmanager.h" +#include "utils/dirmanager.h" #include #include diff --git a/src/widgets/versedialog.ui b/src/dialogs/versedialog.ui similarity index 100% rename from src/widgets/versedialog.ui rename to src/dialogs/versedialog.ui diff --git a/src/utils/contentjob.cpp b/src/downloader/contentjob.cpp similarity index 80% rename from src/utils/contentjob.cpp rename to src/downloader/contentjob.cpp index 07caec4e..24a29e81 100644 --- a/src/utils/contentjob.cpp +++ b/src/downloader/contentjob.cpp @@ -1,6 +1,6 @@ #include "contentjob.h" -#include "../types/tafsirtask.h" -#include "../types/translationtask.h" +#include "downloader/tafsirtask.h" +#include "downloader/translationtask.h" ContentJob::ContentJob(Type type, int idx) : m_idx(idx) @@ -13,8 +13,11 @@ ContentJob::ContentJob(Type type, int idx) else if (type == DownloadJob::TranslationFile) m_task = new TranslationTask(idx); + connect(m_taskDlr, &TaskDownloader::fileFound, this, &ContentJob::fileFound); connect(m_taskDlr, &TaskDownloader::completed, this, &DownloadJob::finished); connect(m_taskDlr, &TaskDownloader::taskError, this, &DownloadJob::failed); + connect( + m_taskDlr, &TaskDownloader::progressed, this, &DownloadJob::progressed); connect(m_taskDlr, &TaskDownloader::downloadSpeedUpdated, this, @@ -48,13 +51,13 @@ ContentJob::isDownloading() int ContentJob::completed() { - return m_taskDlr->bytes(); + return m_taskDlr->bytes() / 1024; } int ContentJob::total() { - return m_taskDlr->total(); + return m_taskDlr->total() / 1024; } DownloadJob::Type diff --git a/src/utils/contentjob.h b/src/downloader/contentjob.h similarity index 83% rename from src/utils/contentjob.h rename to src/downloader/contentjob.h index 53ffb6d0..8d2a0afc 100644 --- a/src/utils/contentjob.h +++ b/src/downloader/contentjob.h @@ -1,13 +1,14 @@ #ifndef CONTENTJOB_H #define CONTENTJOB_H -#include "../types/tafsir.h" -#include "../types/translation.h" -#include "downloadjob.h" +#include "interfaces/downloadjob.h" #include "taskdownloader.h" +#include "types/tafsir.h" +#include "types/translation.h" class ContentJob : public DownloadJob { + Q_OBJECT public: ContentJob(Type type, int idx); ~ContentJob(); @@ -20,6 +21,9 @@ class ContentJob : public DownloadJob Type type() override; QString name() override; +signals: + void fileFound(); + private: QList>& m_tafasir = Tafsir::tafasir; QList>& m_translations = diff --git a/src/utils/jobmanager.cpp b/src/downloader/jobmanager.cpp similarity index 73% rename from src/utils/jobmanager.cpp rename to src/downloader/jobmanager.cpp index 478d5363..669e6b48 100644 --- a/src/utils/jobmanager.cpp +++ b/src/downloader/jobmanager.cpp @@ -1,4 +1,5 @@ #include "jobmanager.h" +#include "contentjob.h" JobManager::JobManager(QObject* parent) : QObject(parent) @@ -29,6 +30,7 @@ JobManager::stop() if (!m_active.isNull()) m_active->stop(); disconnectActive(); + emit jobAborted(); } void @@ -46,6 +48,13 @@ JobManager::processJobs() void JobManager::connectActive() { + if (m_active->type() == DownloadJob::TafsirFile || + m_active->type() == DownloadJob::TranslationFile) + connect(qobject_cast(m_active), + &ContentJob::fileFound, + this, + &JobManager::handleFilesFound); + connect(m_active, &DownloadJob::failed, this, &JobManager::handleFailed); connect(m_active, &DownloadJob::finished, this, &JobManager::handleCompleted); connect( @@ -59,6 +68,13 @@ JobManager::connectActive() void JobManager::disconnectActive() { + if (m_active->type() == DownloadJob::TafsirFile || + m_active->type() == DownloadJob::TranslationFile) + disconnect(qobject_cast(m_active), + &ContentJob::fileFound, + this, + &JobManager::handleFilesFound); + disconnect(m_active, &DownloadJob::failed, this, &JobManager::handleFailed); disconnect( m_active, &DownloadJob::finished, this, &JobManager::handleCompleted); @@ -92,6 +108,14 @@ JobManager::handleCompleted() processJobs(); } +void +JobManager::handleFilesFound() +{ + emit filesFound(m_active); + disconnectActive(); + processJobs(); +} + bool JobManager::isOn() const { diff --git a/src/utils/jobmanager.h b/src/downloader/jobmanager.h similarity index 86% rename from src/utils/jobmanager.h rename to src/downloader/jobmanager.h index 8a8ee725..010268fe 100644 --- a/src/utils/jobmanager.h +++ b/src/downloader/jobmanager.h @@ -1,7 +1,7 @@ #ifndef JOBMANAGER_H #define JOBMANAGER_H -#include "downloadjob.h" +#include "interfaces/downloadjob.h" #include #include #include @@ -21,15 +21,18 @@ class JobManager : public QObject QPointer active() const; signals: + void jobAborted(); void jobCompleted(QPointer job); void jobFailed(QPointer job); void jobProgressed(QPointer job); void downloadSpeedUpdated(int speed, QString unit); + void filesFound(QPointer job); private slots: void handleProgressed(); void handleFailed(); void handleCompleted(); + void handleFilesFound(); void processJobs(); private: diff --git a/src/utils/qcfjob.cpp b/src/downloader/qcfjob.cpp similarity index 91% rename from src/utils/qcfjob.cpp rename to src/downloader/qcfjob.cpp index 7b1df97d..e402a916 100644 --- a/src/utils/qcfjob.cpp +++ b/src/downloader/qcfjob.cpp @@ -1,4 +1,5 @@ #include "qcfjob.h" +#include QcfJob::QcfJob() : m_completed(0) @@ -32,8 +33,10 @@ QcfJob::processTasks() m_active = m_queue.dequeue(); while (m_active.destination().exists()) { m_completed++; - if (m_completed == 604) + if (m_completed == 604) { + emit DownloadJob::progressed(); emit DownloadJob::finished(); + } if (m_queue.isEmpty()) return; @@ -111,5 +114,5 @@ QcfJob::type() QString QcfJob::name() { - return tr("SettingsDialog", "QCFV2"); + return qApp->translate("SettingsDialog", "QCF V2"); } diff --git a/src/utils/qcfjob.h b/src/downloader/qcfjob.h similarity index 88% rename from src/utils/qcfjob.h rename to src/downloader/qcfjob.h index 79dc1d59..3d936ecc 100644 --- a/src/utils/qcfjob.h +++ b/src/downloader/qcfjob.h @@ -1,14 +1,15 @@ #ifndef QCFJOB_H #define QCFJOB_H -#include "../types/qcftask.h" -#include "downloadjob.h" +#include "downloader/qcftask.h" +#include "interfaces/downloadjob.h" #include "taskdownloader.h" #include class QcfJob : public DownloadJob { + Q_OBJECT public: QcfJob(); diff --git a/src/types/qcftask.cpp b/src/downloader/qcftask.cpp similarity index 100% rename from src/types/qcftask.cpp rename to src/downloader/qcftask.cpp diff --git a/src/types/qcftask.h b/src/downloader/qcftask.h similarity index 85% rename from src/types/qcftask.h rename to src/downloader/qcftask.h index 029ee218..26861c9b 100644 --- a/src/types/qcftask.h +++ b/src/downloader/qcftask.h @@ -1,8 +1,8 @@ #ifndef QCFTASK_H #define QCFTASK_H -#include "../utils/dirmanager.h" -#include "downloadtask.h" +#include "interfaces/downloadtask.h" +#include "utils/dirmanager.h" class QcfTask : public DownloadTask { diff --git a/src/types/recitationtask.cpp b/src/downloader/recitationtask.cpp similarity index 100% rename from src/types/recitationtask.cpp rename to src/downloader/recitationtask.cpp diff --git a/src/types/recitationtask.h b/src/downloader/recitationtask.h similarity index 85% rename from src/types/recitationtask.h rename to src/downloader/recitationtask.h index 9b7f6986..88006a7f 100644 --- a/src/types/recitationtask.h +++ b/src/downloader/recitationtask.h @@ -1,10 +1,10 @@ #ifndef RECITATIONTASK_H #define RECITATIONTASK_H -#include "../utils/dbmanager.h" -#include "../utils/dirmanager.h" -#include "downloadtask.h" -#include "reciter.h" +#include "interfaces/downloadtask.h" +#include "types/reciter.h" +#include "utils/dbmanager.h" +#include "utils/dirmanager.h" class RecitationTask : public DownloadTask { diff --git a/src/utils/surahjob.cpp b/src/downloader/surahjob.cpp similarity index 96% rename from src/utils/surahjob.cpp rename to src/downloader/surahjob.cpp index 9c97bce3..b7a55744 100644 --- a/src/utils/surahjob.cpp +++ b/src/downloader/surahjob.cpp @@ -32,8 +32,10 @@ SurahJob::processTasks() m_active = m_queue.dequeue(); while (m_active.destination().exists()) { m_completed++; - if (m_completed == m_surahCount) + if (m_completed == m_surahCount) { + emit DownloadJob::progressed(); emit DownloadJob::finished(); + } if (m_queue.isEmpty()) return; diff --git a/src/utils/surahjob.h b/src/downloader/surahjob.h similarity index 92% rename from src/utils/surahjob.h rename to src/downloader/surahjob.h index a0a39423..cc5d0b6d 100644 --- a/src/utils/surahjob.h +++ b/src/downloader/surahjob.h @@ -1,14 +1,15 @@ #ifndef SURAHJOB_H #define SURAHJOB_H -#include "../types/recitationtask.h" -#include "downloadjob.h" +#include "interfaces/downloadjob.h" +#include "recitationtask.h" #include "taskdownloader.h" #include #include class SurahJob : public DownloadJob { + Q_OBJECT public: SurahJob(int reciter, int surah); ~SurahJob(); diff --git a/src/types/tafsirtask.cpp b/src/downloader/tafsirtask.cpp similarity index 100% rename from src/types/tafsirtask.cpp rename to src/downloader/tafsirtask.cpp diff --git a/src/types/tafsirtask.h b/src/downloader/tafsirtask.h similarity index 84% rename from src/types/tafsirtask.h rename to src/downloader/tafsirtask.h index aead0730..90b1e8dc 100644 --- a/src/types/tafsirtask.h +++ b/src/downloader/tafsirtask.h @@ -1,9 +1,9 @@ #ifndef TAFSIRTASK_H #define TAFSIRTASK_H -#include "../utils/dirmanager.h" -#include "downloadtask.h" -#include "tafsir.h" +#include "interfaces/downloadtask.h" +#include "types/tafsir.h" +#include "utils/dirmanager.h" class TafsirTask : public DownloadTask { diff --git a/src/utils/taskdownloader.cpp b/src/downloader/taskdownloader.cpp similarity index 86% rename from src/utils/taskdownloader.cpp rename to src/downloader/taskdownloader.cpp index 94453d0a..b4cb032e 100644 --- a/src/utils/taskdownloader.cpp +++ b/src/downloader/taskdownloader.cpp @@ -6,20 +6,30 @@ TaskDownloader::TaskDownloader(QObject* parent) , m_reply(nullptr) , m_task(nullptr) , m_bytes(0) - , m_total(0) + , m_total(1) { } void TaskDownloader::process(DownloadTask* task, QNetworkAccessManager* manager) { + m_task = task; if (m_reply) { + disconnect( + m_reply, &QNetworkReply::finished, this, &TaskDownloader::finish); + disconnect(m_reply, + &QNetworkReply::downloadProgress, + this, + &TaskDownloader::progressed); disconnect(m_reply, &QNetworkReply::downloadProgress, this, &TaskDownloader::taskProgress); - disconnect( - m_reply, &QNetworkReply::finished, this, &TaskDownloader::finish); + } + + if (m_task->destination().exists()) { + emit fileFound(); + return; } QNetworkRequest req(m_task->url()); @@ -27,11 +37,15 @@ TaskDownloader::process(DownloadTask* task, QNetworkAccessManager* manager) m_reply->ignoreSslErrors(); m_startTime = QTime::currentTime(); + connect(m_reply, &QNetworkReply::finished, this, &TaskDownloader::finish); + connect(m_reply, + &QNetworkReply::downloadProgress, + this, + &TaskDownloader::progressed); connect(m_reply, &QNetworkReply::downloadProgress, this, &TaskDownloader::taskProgress); - connect(m_reply, &QNetworkReply::finished, this, &TaskDownloader::finish); } void @@ -69,6 +83,7 @@ TaskDownloader::finish() { if (m_reply->error() != QNetworkReply::NoError) return handleError(m_reply->error()); + m_bytes = m_total; saveFile(); emit completed(); } diff --git a/src/utils/taskdownloader.h b/src/downloader/taskdownloader.h similarity index 87% rename from src/utils/taskdownloader.h rename to src/downloader/taskdownloader.h index 746cdc26..8a9525fc 100644 --- a/src/utils/taskdownloader.h +++ b/src/downloader/taskdownloader.h @@ -1,7 +1,7 @@ #ifndef TASKDOWNLOADER_H #define TASKDOWNLOADER_H -#include "../types/downloadtask.h" +#include "interfaces/downloadtask.h" #include #include #include @@ -19,8 +19,10 @@ class TaskDownloader : public QObject signals: void downloadSpeedUpdated(int speed, QString unit); + void progressed(qint64 bytes, qint64 total); void taskError(); void completed(); + void fileFound(); private slots: void taskProgress(qint64 bytes, qint64 total); diff --git a/src/utils/translationjob.cpp b/src/downloader/translationjob.cpp similarity index 100% rename from src/utils/translationjob.cpp rename to src/downloader/translationjob.cpp diff --git a/src/utils/translationjob.h b/src/downloader/translationjob.h similarity index 100% rename from src/utils/translationjob.h rename to src/downloader/translationjob.h diff --git a/src/types/translationtask.cpp b/src/downloader/translationtask.cpp similarity index 100% rename from src/types/translationtask.cpp rename to src/downloader/translationtask.cpp diff --git a/src/types/translationtask.h b/src/downloader/translationtask.h similarity index 85% rename from src/types/translationtask.h rename to src/downloader/translationtask.h index fa35ab60..e6244960 100644 --- a/src/types/translationtask.h +++ b/src/downloader/translationtask.h @@ -1,9 +1,9 @@ #ifndef TRANSLATIONTASK_H #define TRANSLATIONTASK_H -#include "../utils/dirmanager.h" -#include "downloadtask.h" -#include "translation.h" +#include "interfaces/downloadtask.h" +#include "types/translation.h" +#include "utils/dirmanager.h" class TranslationTask : public DownloadTask { diff --git a/src/utils/downloadjob.h b/src/interfaces/downloadjob.h similarity index 100% rename from src/utils/downloadjob.h rename to src/interfaces/downloadjob.h diff --git a/src/types/downloadtask.h b/src/interfaces/downloadtask.h similarity index 100% rename from src/types/downloadtask.h rename to src/interfaces/downloadtask.h diff --git a/src/types/downloadtask.cpp b/src/types/downloadtask.cpp deleted file mode 100644 index 5603570b..00000000 --- a/src/types/downloadtask.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "downloadtask.h" - -DownloadTask::DownloadTask(const QUrl& url, const QFileInfo& dest) - : m_url(url) - , m_dest(dest) -{ -} - -QUrl -DownloadTask::url() const -{ - return m_url; -} - -QFileInfo -DownloadTask::dest() const -{ - return m_dest; -} - -QPointer -DownloadTask::reply() const -{ - return m_reply; -} - -void -DownloadTask::setReply(QPointer newReply) -{ - m_reply = newReply; -} diff --git a/src/types/reciter.cpp b/src/types/reciter.cpp index 799f6adc..36a07894 100644 --- a/src/types/reciter.cpp +++ b/src/types/reciter.cpp @@ -1,5 +1,5 @@ #include "reciter.h" -#include "../utils/dirmanager.h" +#include "utils/dirmanager.h" #include #include #include diff --git a/src/types/tafsir.cpp b/src/types/tafsir.cpp index f8c10a02..64315730 100644 --- a/src/types/tafsir.cpp +++ b/src/types/tafsir.cpp @@ -1,6 +1,6 @@ #include "tafsir.h" -#include "../utils/dirmanager.h" #include "content.h" +#include "utils/dirmanager.h" #include #include #include diff --git a/src/types/translation.cpp b/src/types/translation.cpp index 00315ecc..e14b151d 100644 --- a/src/types/translation.cpp +++ b/src/types/translation.cpp @@ -1,5 +1,5 @@ #include "translation.h" -#include "../utils/dirmanager.h" +#include "utils/dirmanager.h" #include #include #include diff --git a/src/types/verse.h b/src/types/verse.h index 7757e9cf..f405f78a 100644 --- a/src/types/verse.h +++ b/src/types/verse.h @@ -1,8 +1,8 @@ #ifndef VERSE_H #define VERSE_H -#include "../utils/dbmanager.h" -#include "../utils/settings.h" +#include "utils/dbmanager.h" +#include "utils/settings.h" #include /** diff --git a/src/utils/dbmanager.h b/src/utils/dbmanager.h index 17b53cad..062799b2 100644 --- a/src/utils/dbmanager.h +++ b/src/utils/dbmanager.h @@ -6,10 +6,10 @@ #ifndef DBMANAGER_H #define DBMANAGER_H -#include "../types/tafsir.h" -#include "../types/translation.h" -#include "../utils/dirmanager.h" -#include "../utils/settings.h" +#include "types/tafsir.h" +#include "types/translation.h" +#include "utils/dirmanager.h" +#include "utils/settings.h" #include #include #include diff --git a/src/utils/downloadmanager.cpp b/src/utils/downloadmanager.cpp deleted file mode 100644 index b005e52f..00000000 --- a/src/utils/downloadmanager.cpp +++ /dev/null @@ -1,315 +0,0 @@ -/** - * @file downloadmanager.cpp - * @brief Implementation file for DownloadManager - */ - -#include "downloadmanager.h" - -DownloadManager::DownloadManager(QObject* parent) - : QObject(parent) - , m_netMan{ new QNetworkAccessManager(this) } -{ - connect(m_netMan, - &QNetworkAccessManager::finished, - this, - &DownloadManager::finishupTask); - - m_versionReq.setUrl(QUrl::fromEncoded( - "https://raw.githubusercontent.com/0xzer0x/quran-companion/main/VERSION")); - m_versionReq.setTransferTimeout(1500); - m_versionReq.setAttribute(QNetworkRequest::User, 1); -} - -void -DownloadManager::getLatestVersion() -{ - m_versionReply = m_netMan->get(m_versionReq); - m_versionReply->ignoreSslErrors(); -} - -void -DownloadManager::addToQueue(DownloadType type, QPair info) -{ - m_downloadQueue.enqueue(QPair>(type, info)); -} - -void -DownloadManager::addToQueue(int reciter, int surah) -{ - m_downloadQueue.enqueue(QPair>( - Recitation, QPair(reciter, surah))); -} - -void -DownloadManager::startQueue() -{ - if (!m_isDownloading) { - processTaskQueue(); - emit downloadStarted(); - } -} - -void -DownloadManager::stopQueue() -{ - if (m_isDownloading) { - cancelCurrentTask(); - m_isDownloading = false; - m_downloadQueue.clear(); - m_taskQueue.clear(); - } -} - -void -DownloadManager::cancelCurrentTask() -{ - if (m_activeTask.reply == nullptr) - return; - - m_activeTask.reply->abort(); - m_activeTask.reply->close(); - emit downloadCanceled(); -} - -void -DownloadManager::enqeueQCF() -{ - static const QString base = - "https://github.com/0xzer0x/quran-companion/raw/main/extras/"; - QString path; - DownloadTask t; - for (int i = 1; i <= 604; i++) { - path = QString("QCFV2/QCF2%0.ttf") - .arg(QString::number(i).rightJustified(3, '0')); - t.metainfo = { -1, -1, i }; - t.metainfo.squeeze(); - t.downloadPath.setFile(m_downloadsDir->absoluteFilePath(path)); - t.link = QUrl::fromEncoded((base + path).toLatin1()); - m_taskQueue.enqueue(t); - } -} - -void -DownloadManager::enqeueTask(QPair info) -{ - static const QString base = - "https://github.com/0xzer0x/quran-companion/raw/main/extras/"; - QString path; - if (info.first) - path = "translations/" + m_translations.at(info.second)->filename(); - else - path = "tafasir/" + m_tafasir.at(info.second)->filename(); - DownloadTask t; - t.metainfo = { info.first, info.second, 0 }; - t.metainfo.squeeze(); - t.link = QUrl::fromEncoded((base + path).toLatin1()); - t.downloadPath.setFile(m_downloadsDir->absoluteFilePath(path)); - m_taskQueue.enqueue(t); -} - -void -DownloadManager::enqeueTask(int reciterIdx, int surah, int verse) -{ - static const QString path = "recitations/%0/%1.mp3"; - DownloadTask t; - t.metainfo = { reciterIdx, surah, verse }; - t.metainfo.squeeze(); - t.link = downloadUrl(reciterIdx, surah, verse); - t.downloadPath.setFile(m_downloadsDir->absoluteFilePath( - path.arg(m_recitersList.at(reciterIdx)->baseDirName(), - QString::number(surah).rightJustified(3, '0') + - QString::number(verse).rightJustified(3, '0')))); - - m_taskQueue.enqueue(t); -} - -void -DownloadManager::processDownloadQueue() -{ - if (m_downloadQueue.empty()) { - m_isDownloading = false; - return; - } - - m_isDownloading = true; - QPair> current = m_downloadQueue.dequeue(); - QPair& info = current.second; - m_activeType = current.first; - if (current.first == QCF) { - m_activeTotal = 604; - enqeueQCF(); - } else if (current.first == Recitation) { - m_activeTotal = m_dbMgr->getSurahVerseCount(info.second); - for (int v = 1; v <= m_activeTotal; v++) - enqeueTask(info.first, info.second, v); - } else if (current.first == File) { - m_activeTotal = 1; - enqeueTask(info); - } -} - -void -DownloadManager::processTaskQueue() -{ - if (m_taskQueue.empty()) { - processDownloadQueue(); - if (!m_isDownloading) - return; - } - - m_activeTask = m_taskQueue.dequeue(); - - while (m_activeTask.downloadPath.exists()) { - if (m_activeTask.metainfo[2] == m_activeTotal || m_activeType == File) { - emit downloadProgressed(m_activeTotal, m_activeTotal); - emit filesFound(m_activeType, m_activeTask.metainfo); - } - - if (m_taskQueue.empty()) { - processDownloadQueue(); - if (!m_isDownloading) - return; - } - - m_activeTask = m_taskQueue.dequeue(); - } - - QNetworkRequest req(m_activeTask.link); - m_activeTask.reply = m_netMan->get(req); - m_activeTask.reply->ignoreSslErrors(); - m_downloadStart = QTime::currentTime(); - - connect(m_activeTask.reply, - &QNetworkReply::downloadProgress, - this, - &DownloadManager::downloadProgress); -} - -void -DownloadManager::downloadProgress(qint64 bytes, qint64 total) -{ - if (m_activeType == File) { - if (!m_activeTask.metainfo[2]) - m_activeTotal = total / 1024; - m_activeTask.metainfo[2] = bytes / 1024; - emit downloadProgressed(m_activeTask.metainfo[2], m_activeTotal); - } - - int secs = m_downloadStart.secsTo(QTime::currentTime()); - if (secs < 1) - secs = 1; - - int speedPerSec = bytes / secs; - QString unit = tr("bytes"); - if (speedPerSec >= 1024) { - unit = tr("KB"); - speedPerSec /= 1024; - } - - if (speedPerSec >= 1024) { - unit = tr("MB"); - speedPerSec /= 1024; - } - - emit downloadSpeedUpdated(speedPerSec, unit); -} - -void -DownloadManager::finishupTask(QNetworkReply* replyData) -{ - if (replyData->request().attribute(QNetworkRequest::User).toInt() == 1) - return handleVersionReply(); - - if (m_activeTask.reply->error() != QNetworkReply::NoError) - return handleConError(m_activeTask.reply->error()); - - saveFile(replyData); - - emit downloadProgressed(m_activeTask.metainfo[2], m_activeTotal); - if (m_activeTask.metainfo[2] == m_activeTotal) { - emit downloadCompleted(m_activeType, m_activeTask.metainfo); - } - - disconnect(m_activeTask.reply, - &QNetworkReply::downloadProgress, - this, - &DownloadManager::downloadProgress); - - processTaskQueue(); -} - -bool -DownloadManager::saveFile(QNetworkReply* data) -{ - QFile localFile(m_activeTask.downloadPath.absoluteFilePath()); - - if (!localFile.open(QIODevice::WriteOnly)) { - qWarning() << "Couldn't open file:" << m_activeTask.downloadPath; - return false; - } - - const QByteArray fdata = data->readAll(); - m_activeTask.reply->close(); - - localFile.write(fdata); - localFile.close(); - - return true; -} - -QUrl -DownloadManager::downloadUrl(const int reciterIdx, - const int surah, - const int verse) const -{ - const Reciter& r = *m_recitersList.at(reciterIdx); - QString url = r.baseUrl(); - if (r.useId()) - url.append(QString::number(m_dbMgr->getVerseId(surah, verse)) + ".mp3"); - else - url.append(QString::number(surah).rightJustified(3, '0') + - QString::number(verse).rightJustified(3, '0') + ".mp3"); - - return QUrl::fromEncoded(url.toLatin1()); -} - -void -DownloadManager::handleConError(QNetworkReply::NetworkError err) -{ - switch (err) { - case QNetworkReply::OperationCanceledError: - qInfo() << m_activeTask.reply->errorString(); - break; - - default: - qInfo() << m_activeTask.reply->errorString(); - emit downloadErrored(m_activeType, m_activeTask.metainfo); - } -} - -void -DownloadManager::handleVersionReply() -{ - int status = - m_versionReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (status == 200) - emit latestVersionFound(m_versionReply->readAll().trimmed()); -} - -QNetworkAccessManager* -DownloadManager::netMan() const -{ - return m_netMan; -} - -DownloadManager::DownloadTask -DownloadManager::currentTask() const -{ - return m_activeTask; -} - -bool -DownloadManager::isDownloading() const -{ - return m_isDownloading; -} diff --git a/src/utils/downloadmanager.h b/src/utils/downloadmanager.h deleted file mode 100644 index 67ecf292..00000000 --- a/src/utils/downloadmanager.h +++ /dev/null @@ -1,281 +0,0 @@ -/** - * @file downloadmanager.h - * @brief Header file for DownloadManager - */ - -#ifndef DOWNLOADMANAGER_H -#define DOWNLOADMANAGER_H - -#include "../types/reciter.h" -#include "dbmanager.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/** - * @brief DownloadManager class is responsible for downloading files and - * preforming web requests - */ -class DownloadManager : public QObject -{ - Q_OBJECT -public: - enum DownloadType - { - QCF, - Recitation, - File - }; - /** - * @brief DownloadTask struct represents a single verse file download task - * @details downloads are separated into 3 different types - * DownloadType::Recitation - MP3 recitation for a single verse - reciter - * combination - * DownloadType::QCF - QCF v2 font files - * DownloadType::File - single file download, used for downloading tafsir and - * translation DB files - */ - struct DownloadTask - { - /** - * @brief metainfo vector for storing information about the download task - * @details data in the metainfo QList depends on the DownloadType - * DownloadType::Recitation - { reciter, surah, verse } - * DownloadType::QCF - { -1, -1, page } - * DownloadType::File - { k, idx, bytes } where - * k: kind of file (0 for tafsir, 1 for translation) - * idx: index of the file in its corresponding global QList - * bytes: is the current number of bytes downloaded (updated automatically) - */ - QList metainfo; - /** - * @brief download link for the verse - */ - QUrl link; - /** - * @brief reply data of the web request - */ - QNetworkReply* reply = nullptr; - /** - * @brief QFileInfo representing the path to save the file to - */ - QFileInfo downloadPath; - }; - - /** - * @brief Class constructor - * @param parent - pointer to parent widget - */ - explicit DownloadManager(QObject* parent = nullptr); - /** - * @brief gets the latest release of the application from the github repo - */ - void getLatestVersion(); - /** - * @brief getter for m_isDownloading - * @return boolean - */ - bool isDownloading() const; - /** - * @brief getter for m_currentTask - * @return DownloadTask - */ - DownloadTask currentTask() const; - /** - * @brief getter for m_netMan - * @return QNetworkAccessManager* - */ - QNetworkAccessManager* netMan() const; - -public slots: - /** - * @brief starts the download queue to process download tasks - */ - void startQueue(); - /** - * @brief stops the download process - */ - void stopQueue(); - /** - * @brief cancels the currently active download task - */ - void cancelCurrentTask(); - /** - * @brief process download queue front task. sets the networkReply for the - * current task - */ - void processTaskQueue(); - /** - * @brief process the download group queue by adding appropriate download - * tasks according to the DownloadType and metainfo - */ - void processDownloadQueue(); - /** - * @brief enqueues an entry in the download group queue - * @param type - DownloadType of group - * @param info - additional info of the download required - */ - void addToQueue(DownloadType type, QPair info = { -1, -1 }); - /** - * @brief overload to enqueue a Recitation entry in the download group queue - * @param reciter - reciter index in Globals::recitersList - * @param surah - surah number to download - */ - void addToQueue(int reciter, int surah); - /** - * @brief calculate download speed and emit signal for UI component to update - * its value - * @param bytes - bytes downloaded so far - * @param total - total bytes - */ - void downloadProgress(qint64 bytes, qint64 total); - /** - * @brief handle finished tasks and emit the correct signal - * @param replyData - received data from request - */ - void finishupTask(QNetworkReply* replyData); - /** - * @brief save downloaded verse file locally - * @param data - downloaded binary data - * @return boolean value to indicate successful write operation - */ - bool saveFile(QNetworkReply* data); - -signals: - /** - * @fn void latestVersionFound(QString) - * @brief Emitted when the application version is fetched from github - * @param appVer - application version string - */ - void latestVersionFound(QString appVer); - /** - * @fn void downloadStarted() - * @brief Emitted when the download queue begins processing - */ - void downloadStarted(); - /** - * @fn void downloadCanceled() - */ - void downloadCanceled(); - /** - * @fn void downloadProgressed(int, int) - * @brief Emitted when a download task from the queue completed - */ - void downloadProgressed(int downloaded, int total); - /** - * @fn void downloadSpeedUpdated(int, QString) - * @brief Emitted to signal UI change for the displayed download speed - */ - void downloadSpeedUpdated(int valuePerSec, QString unit); - /** - * @fn void downloadCompleted(int, int) - * @brief Emitted when the currently active download group is completed - */ - void downloadCompleted(DownloadType type, const QList& metainfo); - /** - * @fn void downloadErrored(int, int) - */ - void downloadErrored(DownloadType type, const QList& metainfo); - /** - * @fn void filesFound(int, int) - * @brief Emitted when the current surah verses are found in recitations - * directory - */ - void filesFound(DownloadType type, const QList& metainfo); - -private: - QSharedPointer m_downloadsDir = DirManager::downloadsDir; - QSharedPointer m_dbMgr = DBManager::current(); - const QList>& m_recitersList = Reciter::reciters; - const QList>& m_tafasir = Tafsir::tafasir; - const QList>& m_translations = - Translation::translations; - /** - * @brief generate download url for specified verse using the reciter download - * url - * @param reciterIdx - ::Globals::recitersList index for the reciter - * @param surah - surah number - * @param verse - verse number - * @return - */ - QUrl downloadUrl(const int reciterIdx, - const int surah, - const int verse) const; - /** - * @brief enqueues QCF font file tasks - */ - void enqeueQCF(); - /** - * @brief enqueues a File task based on the given info - * @param info - */ - void enqeueTask(QPair info); - /** - * @brief create a DownloadTask and add it to the download Queue - * @param reciterIdx - ::Globals::recitersList index for the reciter - * @param surah - surah number - * @param verse - verse number - */ - void enqeueTask(int reciterIdx, int surah, int verse); - - /** - * @brief emit signal according to the download error that occured - * @param err - network error received - */ - void handleConError(QNetworkReply::NetworkError err); - /** - * @brief emit signal with the latest application version - */ - void handleVersionReply(); - /** - * @brief boolean value representing the download state - */ - bool m_isDownloading = false; - /** - * @brief the total count of files in the current group download / total bytes - * if downloading a single file - */ - int m_activeTotal; - QNetworkRequest m_versionReq; - /** - * @brief QNetworkReply for version info request - */ - QPointer m_versionReply; - /** - * @brief QNetwrokAccessManager instance responsible for sending download - * requests - */ - QPointer m_netMan; - /** - * @brief the currently active DownloadTask - */ - DownloadTask m_activeTask; - /** - * @brief currently active DownloadType - */ - DownloadType m_activeType = Recitation; - /** - * @brief download group queue, used for quickly adding downloads that will be - * expanding into separate DownloadTask (s) when its processed - */ - QQueue>> m_downloadQueue; - /** - * @brief individual DownloadTask queue - */ - QQueue m_taskQueue; - /** - * @brief QTime object to get the download start time, used in calculating - * download speed - */ - QTime m_downloadStart; -}; - -#endif // DOWNLOADMANAGER_H diff --git a/src/utils/settings.cpp b/src/utils/settings.cpp index ce8a177e..43e2289c 100644 --- a/src/utils/settings.cpp +++ b/src/utils/settings.cpp @@ -1,5 +1,5 @@ #include "settings.h" -#include "../utils/dirmanager.h" +#include "utils/dirmanager.h" #include #include #include diff --git a/src/utils/shortcuthandler.cpp b/src/utils/shortcuthandler.cpp index d07bf8a7..28189800 100644 --- a/src/utils/shortcuthandler.cpp +++ b/src/utils/shortcuthandler.cpp @@ -4,7 +4,7 @@ */ #include "shortcuthandler.h" -#include "../utils/settings.h" +#include "utils/settings.h" #include #include #include diff --git a/src/utils/verseplayer.h b/src/utils/verseplayer.h index 4e1fe1b0..3b2f9fe4 100644 --- a/src/utils/verseplayer.h +++ b/src/utils/verseplayer.h @@ -6,9 +6,9 @@ #ifndef VERSEPLAYER_H #define VERSEPLAYER_H -#include "../types/reciter.h" -#include "../types/verse.h" #include "dbmanager.h" +#include "types/reciter.h" +#include "types/verse.h" #include #include #include diff --git a/src/utils/versionchecker.cpp b/src/utils/versionchecker.cpp new file mode 100644 index 00000000..41608b75 --- /dev/null +++ b/src/utils/versionchecker.cpp @@ -0,0 +1,72 @@ +#include "versionchecker.h" +#include +#include +#include + +VersionChecker::VersionChecker(QObject* parent) + : QObject(parent) + , m_updateTool(QApplication::applicationDirPath() + QDir::separator() + + "QCMaintenanceTool.exe") +{ + m_versionReq.setUrl(QUrl::fromEncoded( + "https://raw.githubusercontent.com/0xzer0x/quran-companion/main/VERSION")); + m_versionReq.setTransferTimeout(1500); + + connect( + &m_runner, &QProcess::finished, this, &VersionChecker::handleToolOutput); + connect(&m_netMgr, + &QNetworkAccessManager::finished, + this, + &VersionChecker::handleReply); +} + +void +VersionChecker::checkUpdates() +{ + if (toolExists()) { + m_runner.setWorkingDirectory(QApplication::applicationDirPath()); + m_runner.start(m_updateTool, QStringList("ch")); + return; + } + getLatestVersion(); +} + +void +VersionChecker::handleToolOutput() +{ + QString output = m_runner.readAll(); + QString displayText; + + if (output.contains("There are currently no updates available.")) { + displayText = tr("There are currently no updates available."); + QMessageBox::information(nullptr, tr("Update info"), displayText); + } else { + displayText = tr("Updates available, do you want to open the update tool?"); + QMessageBox::StandardButton btn = + QMessageBox::question(nullptr, tr("Updates info"), displayText); + if (btn == QMessageBox::Yes) + m_runner.startDetached(m_updateTool); + } +} + +void +VersionChecker::handleReply(QPointer reply) +{ + int status = + reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (status == 200) + emit versionFound(reply->readAll().trimmed()); +} + +void +VersionChecker::getLatestVersion() +{ + QNetworkReply* reply = m_netMgr.get(m_versionReq); + reply->ignoreSslErrors(); +} + +bool +VersionChecker::toolExists() +{ + return QFileInfo::exists(m_updateTool); +} diff --git a/src/utils/versionchecker.h b/src/utils/versionchecker.h new file mode 100644 index 00000000..10750b2f --- /dev/null +++ b/src/utils/versionchecker.h @@ -0,0 +1,36 @@ +#ifndef VERSIONCHECKER_H +#define VERSIONCHECKER_H + +#include +#include +#include +#include +#include +#include + +class VersionChecker : public QObject +{ + Q_OBJECT +public: + explicit VersionChecker(QObject* parent = nullptr); + +public slots: + void checkUpdates(); + +signals: + void versionFound(QString version); + +private slots: + void handleToolOutput(); + void handleReply(QPointer reply); + +private: + void getLatestVersion(); + bool toolExists(); + QNetworkAccessManager m_netMgr; + QString m_updateTool; + QProcess m_runner; + QNetworkRequest m_versionReq; +}; + +#endif // VERSIONCHECKER_H diff --git a/src/widgets/betaqaviewer.h b/src/widgets/betaqaviewer.h index f8b72499..885cdd29 100644 --- a/src/widgets/betaqaviewer.h +++ b/src/widgets/betaqaviewer.h @@ -1,7 +1,7 @@ #ifndef BETAQAVIEWER_H #define BETAQAVIEWER_H -#include "../utils/dbmanager.h" +#include "utils/dbmanager.h" #include #include #include diff --git a/src/widgets/downloadprogressbar.cpp b/src/widgets/downloadprogressbar.cpp index 70b78643..142f670b 100644 --- a/src/widgets/downloadprogressbar.cpp +++ b/src/widgets/downloadprogressbar.cpp @@ -4,29 +4,29 @@ */ #include "downloadprogressbar.h" +#include +#include #include -DownloadProgressBar::DownloadProgressBar(QWidget* parent, - DownloadType type, - int max) +DownloadProgressBar::DownloadProgressBar(QWidget* parent, Type type, int max) : QProgressBar(parent) { setStyling(downloading); setMaximum(max); setValue(0); - if (type == DownloadManager::File) + if (type == DownloadJob::TafsirFile || type == DownloadJob::TranslationFile) setFormat("%v / %m " + qApp->translate("DownloadManager", "KB")); else setFormat("%v / %m"); } void -DownloadProgressBar::updateProgress(qint64 downloaded, qint64 total) +DownloadProgressBar::updateProgress(QPointer job) { - if (maximum() != total) - setMaximum(total); + if (maximum() != job->total()) + setMaximum(job->total()); - setValue(downloaded); + setValue(job->completed()); } void diff --git a/src/widgets/downloadprogressbar.h b/src/widgets/downloadprogressbar.h index 26b7abf3..33a316e1 100644 --- a/src/widgets/downloadprogressbar.h +++ b/src/widgets/downloadprogressbar.h @@ -6,9 +6,9 @@ #ifndef DOWNLOADPROGRESSBAR_H #define DOWNLOADPROGRESSBAR_H -#include "../utils/downloadmanager.h" +#include "interfaces/downloadjob.h" #include -typedef DownloadManager::DownloadType DownloadType; +typedef DownloadJob::Type Type; /** * @brief DownloadProgressBar class is a modified QProgressBar to change its @@ -23,7 +23,7 @@ class DownloadProgressBar : public QProgressBar * @param max - maximum value for the progress bar (defaults to longest surah * in the Quran) */ - DownloadProgressBar(QWidget* parent, DownloadType type, int max); + DownloadProgressBar(QWidget* parent, Type type, int max); /** * @brief The State enum represents the different states of the progressbar UI * component @@ -36,7 +36,7 @@ class DownloadProgressBar : public QProgressBar }; public slots: - void updateProgress(qint64 downloaded, qint64 total); + void updateProgress(QPointer job); void setStyling(State); }; diff --git a/src/widgets/notificationpopup.cpp b/src/widgets/notificationpopup.cpp index 91c49409..dfaddef8 100644 --- a/src/widgets/notificationpopup.cpp +++ b/src/widgets/notificationpopup.cpp @@ -4,7 +4,7 @@ */ #include "notificationpopup.h" -#include "../utils/stylemanager.h" +#include "utils/stylemanager.h" #include using namespace fa; @@ -82,37 +82,18 @@ NotificationPopup::notify(QString message, NotificationPopup::Action icon) } void -NotificationPopup::completedDownload(DownloadType type, - const QList& metainfo) +NotificationPopup::completedDownload(QPointer job) { setStyleSheet(""); - QString msg = tr("Download Completed") + ": "; - if (type == DownloadManager::Recitation) - msg += m_recitersList.at(metainfo[0])->displayName() + " - " + - m_dbMgr->surahNameList().at(metainfo[1] - 1); - else if (type == DownloadManager::QCF) - msg += tr("QCF V2"); - else if (type == DownloadManager::File) - msg += metainfo[0] ? m_translations.at(metainfo[1])->displayName() - : m_tafasir.at(metainfo[1])->displayName(); - + QString msg = tr("Download Completed") + ": " + job->name(); this->notify(msg, success); } void -NotificationPopup::downloadError(DownloadType type, const QList& metainfo) +NotificationPopup::downloadError(QPointer job) { setStyleSheet("QFrame#Popup { background-color: #a50500 }"); - QString msg = tr("Download Failed") + ": "; - if (type == DownloadManager::Recitation) - msg += m_recitersList.at(metainfo[0])->displayName() + " - " + - m_dbMgr->surahNameList().at(metainfo[1] - 1); - else if (type == DownloadManager::QCF) - msg += tr("QCF V2"); - else if (type == DownloadManager::File) - msg += metainfo[0] ? m_translations.at(metainfo[1])->displayName() - : m_tafasir.at(metainfo[1])->displayName(); - + QString msg = tr("Download Failed") + ": " + job->name(); this->notify(msg, fail); } diff --git a/src/widgets/notificationpopup.h b/src/widgets/notificationpopup.h index 15dc4970..98d21a3c 100644 --- a/src/widgets/notificationpopup.h +++ b/src/widgets/notificationpopup.h @@ -6,8 +6,9 @@ #ifndef NOTIFICATIONPOPUP_H #define NOTIFICATIONPOPUP_H -#include "../utils/dbmanager.h" -#include "../utils/downloadmanager.h" +#include "interfaces/downloadjob.h" +#include "types/reciter.h" +#include "utils/dbmanager.h" #include #include #include @@ -18,7 +19,6 @@ #include #include #include -typedef DownloadManager::DownloadType DownloadType; /** * @brief NotificationPopup class represents an in-app popup for notifying the @@ -79,13 +79,13 @@ public slots: * @param reciterIdx - ::Globals::recitersList index for the reciter * @param surah - the surah that was downloaded */ - void completedDownload(DownloadType type, const QList& metainfo); + void completedDownload(QPointer job); /** * @brief slot to show a notification on download error * @param reciterIdx - ::Globals::recitersList index for the reciter * @param surah - the surah that was downloaded */ - void downloadError(DownloadType type, const QList& metainfo); + void downloadError(QPointer job); /** * @brief slot to show a notification on bookmark addition */ @@ -109,7 +109,8 @@ public slots: QSharedPointer m_dbMgr = DBManager::current(); QList>& m_recitersList = Reciter::reciters; QList>& m_tafasir = Tafsir::tafasir; - QList>& m_translations = Translation::translations; + QList>& m_translations = + Translation::translations; /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/widgets/quranpagebrowser.cpp b/src/widgets/quranpagebrowser.cpp index 0b55eb8c..33c9ae49 100644 --- a/src/widgets/quranpagebrowser.cpp +++ b/src/widgets/quranpagebrowser.cpp @@ -4,8 +4,8 @@ */ #include "quranpagebrowser.h" -#include "../utils/fontmanager.h" -#include "../utils/stylemanager.h" +#include "utils/fontmanager.h" +#include "utils/stylemanager.h" #include #include #include diff --git a/src/widgets/quranpagebrowser.h b/src/widgets/quranpagebrowser.h index c24f4874..e3c05bfb 100644 --- a/src/widgets/quranpagebrowser.h +++ b/src/widgets/quranpagebrowser.h @@ -6,7 +6,7 @@ #ifndef QURANPAGEBROWSER_H #define QURANPAGEBROWSER_H -#include "../utils/dbmanager.h" +#include "utils/dbmanager.h" #include #include #include From 8cce55478f829adb4d7d3a426216650c8dbd6ac5 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sat, 24 Feb 2024 23:48:37 +0200 Subject: [PATCH 20/51] ci: flatpak metainfo file update --- ...ithub._0xzer0x.qurancompanion.metainfo.xml | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/dist/xdg/io.github._0xzer0x.qurancompanion.metainfo.xml b/dist/xdg/io.github._0xzer0x.qurancompanion.metainfo.xml index 0c70fd45..3e0e2dc5 100644 --- a/dist/xdg/io.github._0xzer0x.qurancompanion.metainfo.xml +++ b/dist/xdg/io.github._0xzer0x.qurancompanion.metainfo.xml @@ -6,6 +6,9 @@ MIT LGPL-3.0-or-later + + Youssef Fathy +

Quran Companion is a cross-platform Quran reader and player with recitation download capabilities, verse highlighting, resizable quran font, and a variety of tafsir books and translations

@@ -16,22 +19,28 @@ https://github.com/0xzer0x/quran-companion/issues - https://raw.githubusercontent.com/0xzer0x/quran-companion/main/screenshots/light.png + The main application window in light theme. + https://raw.githubusercontent.com/0xzer0x/quran-companion/main/screenshots/light.png - https://raw.githubusercontent.com/0xzer0x/quran-companion/main/screenshots/dark.png + The main application window in dark theme. + https://raw.githubusercontent.com/0xzer0x/quran-companion/main/screenshots/dark.png - https://raw.githubusercontent.com/0xzer0x/quran-companion/main/screenshots/sepia.png + The main application window in sepia theme. + https://raw.githubusercontent.com/0xzer0x/quran-companion/main/screenshots/sepia.png - https://raw.githubusercontent.com/0xzer0x/quran-companion/main/screenshots/ar_light.png + The main application window in light theme with Arabic UI. + https://raw.githubusercontent.com/0xzer0x/quran-companion/main/screenshots/ar_light.png - https://raw.githubusercontent.com/0xzer0x/quran-companion/main/screenshots/ar_dark.png + The main application window in dark theme with Arabic UI. + https://raw.githubusercontent.com/0xzer0x/quran-companion/main/screenshots/ar_dark.png - https://raw.githubusercontent.com/0xzer0x/quran-companion/main/screenshots/ar_two-page.png + The main application window in dark theme with Arabic UI and two page mode. + https://raw.githubusercontent.com/0xzer0x/quran-companion/main/screenshots/ar_two-page.png From 5a4827ee66b4fd7c88464f65ebb35a5f551e9651 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sun, 25 Feb 2024 10:28:35 +0200 Subject: [PATCH 21/51] fix: more refactoring issues --- src/dialogs/downloaderdialog.cpp | 3 +-- src/downloader/qcfjob.cpp | 6 ++++++ src/downloader/qcfjob.h | 1 + src/downloader/surahjob.cpp | 6 +++++- src/downloader/taskdownloader.cpp | 5 +++++ src/downloader/taskdownloader.h | 2 ++ src/interfaces/downloadjob.h | 1 + 7 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/dialogs/downloaderdialog.cpp b/src/dialogs/downloaderdialog.cpp index 3e41e2c8..a2c805b8 100644 --- a/src/dialogs/downloaderdialog.cpp +++ b/src/dialogs/downloaderdialog.cpp @@ -361,8 +361,7 @@ DownloaderDialog::downloadCompleted(QPointer finished) SurahJob* sj = qobject_cast(finished); removeFromDownloading(sj->reciter(), sj->surah()); } - if (finished->type() == DownloadJob::TafsirFile || - finished->type() == DownloadJob::TranslationFile) { + if (m_currentBar->maximum() == 0) { m_currentBar->setValue(1); m_currentBar->setMaximum(1); m_currentBar->setFormat("1 / 1"); diff --git a/src/downloader/qcfjob.cpp b/src/downloader/qcfjob.cpp index e402a916..7414c396 100644 --- a/src/downloader/qcfjob.cpp +++ b/src/downloader/qcfjob.cpp @@ -7,6 +7,7 @@ QcfJob::QcfJob() , m_isDownloading(false) , m_taskDlr(new TaskDownloader(this)) { + connect(m_taskDlr, &TaskDownloader::fileFound, this, &QcfJob::taskFinished); connect(m_taskDlr, &TaskDownloader::completed, this, &QcfJob::taskFinished); connect(m_taskDlr, &TaskDownloader::taskError, this, &QcfJob::taskFailed); connect(m_taskDlr, @@ -116,3 +117,8 @@ QcfJob::name() { return qApp->translate("SettingsDialog", "QCF V2"); } + +QcfJob::~QcfJob() +{ + delete m_taskDlr; +} diff --git a/src/downloader/qcfjob.h b/src/downloader/qcfjob.h index 3d936ecc..63334664 100644 --- a/src/downloader/qcfjob.h +++ b/src/downloader/qcfjob.h @@ -12,6 +12,7 @@ class QcfJob : public DownloadJob Q_OBJECT public: QcfJob(); + ~QcfJob(); void start() override; void stop() override; diff --git a/src/downloader/surahjob.cpp b/src/downloader/surahjob.cpp index b7a55744..3eec1ff7 100644 --- a/src/downloader/surahjob.cpp +++ b/src/downloader/surahjob.cpp @@ -6,6 +6,7 @@ SurahJob::SurahJob(int reciter, int surah) , m_surahCount(m_dbMgr->getSurahVerseCount(surah)) , m_taskDlr(new TaskDownloader(this)) { + connect(m_taskDlr, &TaskDownloader::fileFound, this, &SurahJob::taskFinished); connect(m_taskDlr, &TaskDownloader::completed, this, &SurahJob::taskFinished); connect(m_taskDlr, &TaskDownloader::taskError, this, &SurahJob::taskFailed); connect(m_taskDlr, @@ -129,4 +130,7 @@ SurahJob::surah() const return m_surah; } -SurahJob::~SurahJob() {} +SurahJob::~SurahJob() +{ + delete m_taskDlr; +} diff --git a/src/downloader/taskdownloader.cpp b/src/downloader/taskdownloader.cpp index b4cb032e..6b309ae3 100644 --- a/src/downloader/taskdownloader.cpp +++ b/src/downloader/taskdownloader.cpp @@ -129,3 +129,8 @@ TaskDownloader::total() const { return m_total; } + +TaskDownloader::~TaskDownloader() +{ + delete m_task; +} diff --git a/src/downloader/taskdownloader.h b/src/downloader/taskdownloader.h index 8a9525fc..37252dec 100644 --- a/src/downloader/taskdownloader.h +++ b/src/downloader/taskdownloader.h @@ -11,6 +11,8 @@ class TaskDownloader : public QObject Q_OBJECT public: explicit TaskDownloader(QObject* parent); + ~TaskDownloader(); + void process(DownloadTask* task, QNetworkAccessManager* manager); void cancel(); diff --git a/src/interfaces/downloadjob.h b/src/interfaces/downloadjob.h index b8481a8f..e095513b 100644 --- a/src/interfaces/downloadjob.h +++ b/src/interfaces/downloadjob.h @@ -21,6 +21,7 @@ class DownloadJob : public QObject virtual int total() = 0; virtual Type type() = 0; virtual QString name() = 0; + virtual ~DownloadJob(){}; signals: void aborted(); From 64f89d1ba74437edad9c2835862f9c89fbd76d13 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sun, 25 Feb 2024 12:19:02 +0200 Subject: [PATCH 22/51] refactor: use QSharedPointers for shared objects --- src/dialogs/downloaderdialog.cpp | 30 +++++++++------- src/dialogs/downloaderdialog.h | 6 ++-- src/downloader/contentjob.cpp | 23 +++++++------ src/downloader/contentjob.h | 2 +- src/downloader/jobmanager.cpp | 53 +++++++++++++++-------------- src/downloader/jobmanager.h | 23 +++++++------ src/downloader/qcfjob.cpp | 18 ++++------ src/downloader/qcfjob.h | 2 +- src/downloader/surahjob.cpp | 22 ++++++------ src/downloader/surahjob.h | 2 +- src/downloader/taskdownloader.cpp | 10 +----- src/downloader/taskdownloader.h | 1 - src/widgets/downloadprogressbar.cpp | 3 +- src/widgets/downloadprogressbar.h | 2 +- src/widgets/notificationpopup.cpp | 6 ++-- src/widgets/notificationpopup.h | 4 +-- 16 files changed, 101 insertions(+), 106 deletions(-) diff --git a/src/dialogs/downloaderdialog.cpp b/src/dialogs/downloaderdialog.cpp index a2c805b8..a66f7ce3 100644 --- a/src/dialogs/downloaderdialog.cpp +++ b/src/dialogs/downloaderdialog.cpp @@ -180,21 +180,21 @@ DownloaderDialog::addToQueue() enqueueSurah(parent, current + 1); // tafasir else if (i.data(Qt::UserRole).toString() == "tadb") { - ContentJob* job = new ContentJob(DownloadJob::TafsirFile, - i.data(Qt::UserRole + 1).toInt()); + QSharedPointer job = QSharedPointer::create( + DownloadJob::TafsirFile, i.data(Qt::UserRole + 1).toInt()); m_jobMgr->addJob(job); addTaskProgress(job); } // translation else if (i.data(Qt::UserRole).toString() == "trdb") { - ContentJob* job = new ContentJob(DownloadJob::TranslationFile, - i.data(Qt::UserRole + 1).toInt()); + QSharedPointer job = QSharedPointer::create( + DownloadJob::TranslationFile, i.data(Qt::UserRole + 1).toInt()); m_jobMgr->addJob(job); addTaskProgress(job); } // extras else if (i.data(Qt::UserRole).toString() == "qcf") { - QcfJob* job = new QcfJob(); + QSharedPointer job = QSharedPointer::create(); m_jobMgr->addJob(job); addTaskProgress(job); } @@ -205,12 +205,12 @@ DownloaderDialog::addToQueue() } void -DownloaderDialog::addTaskProgress(QPointer job) +DownloaderDialog::addTaskProgress(QSharedPointer job) { int total = job->total(); QString objName; if (job->type() == DownloadJob::Recitation) { - SurahJob* sJob = qobject_cast(job); + SurahJob* sJob = qobject_cast(job.data()); QString reciter = m_reciters.at(sJob->reciter())->displayName(); QString surahName = m_surahDisplayNames.at(sJob->surah() - 1); objName = reciter + tr(" // Surah: ") + surahName; @@ -255,7 +255,8 @@ DownloaderDialog::enqueueSurah(int reciter, int surah) if (currentlyDownloading) return; - SurahJob* sj = new SurahJob(reciter, surah); + QSharedPointer sj = + QSharedPointer::create(reciter, surah); addToDownloading(reciter, surah); addTaskProgress(sj); m_jobMgr->addJob(sj); @@ -347,7 +348,7 @@ DownloaderDialog::downloadAborted() } void -DownloaderDialog::downloadCompleted(QPointer finished) +DownloaderDialog::downloadCompleted(QSharedPointer finished) { m_currentBar->setStyling(DownloadProgressBar::completed); m_currentLb->setText(m_currentLb->parent()->objectName()); @@ -358,10 +359,11 @@ DownloaderDialog::downloadCompleted(QPointer finished) &DownloadProgressBar::updateProgress); if (finished->type() == DownloadJob::Recitation) { - SurahJob* sj = qobject_cast(finished); + QSharedPointer sj = finished.dynamicCast(); removeFromDownloading(sj->reciter(), sj->surah()); } - if (m_currentBar->maximum() == 0) { + if (finished->type() == DownloadJob::TafsirFile || + finished->type() == DownloadJob::TranslationFile) { m_currentBar->setValue(1); m_currentBar->setMaximum(1); m_currentBar->setFormat("1 / 1"); @@ -369,10 +371,11 @@ DownloaderDialog::downloadCompleted(QPointer finished) m_finishedFrames.append(m_frameLst.front()); m_frameLst.pop_front(); setCurrentBar(); + m_jobMgr->processJobs(); } void -DownloaderDialog::topTaskDownloadError(QPointer failed) +DownloaderDialog::topTaskDownloadError(QSharedPointer failed) { m_currentBar->setStyling(DownloadProgressBar::aborted); m_currentLb->setText(m_currentLb->parent()->objectName()); @@ -383,12 +386,13 @@ DownloaderDialog::topTaskDownloadError(QPointer failed) &DownloadProgressBar::updateProgress); if (failed->type() == DownloadJob::Recitation) { - SurahJob* sj = qobject_cast(failed); + QSharedPointer sj = failed.dynamicCast(); removeFromDownloading(sj->reciter(), sj->surah()); } m_finishedFrames.append(m_frameLst.front()); m_frameLst.pop_front(); setCurrentBar(); + m_jobMgr->processJobs(); } void diff --git a/src/dialogs/downloaderdialog.h b/src/dialogs/downloaderdialog.h index 8b501de7..39dec4e0 100644 --- a/src/dialogs/downloaderdialog.h +++ b/src/dialogs/downloaderdialog.h @@ -70,14 +70,14 @@ public slots: * @param type - DownloadType of the download group * @param metainfo - QList of download group information */ - void downloadCompleted(QPointer finished); + void downloadCompleted(QSharedPointer finished); /** * @brief Callback function to update UI elements when the current active * download group fails * @param type - DownloadType of the download group * @param metainfo - QList of download group information */ - void topTaskDownloadError(QPointer failed); + void topTaskDownloadError(QSharedPointer failed); /** * @brief slot to update the displayed download speed in the currently active * download. @@ -140,7 +140,7 @@ private slots: * @param info - download metainfo QPair used by DownloadType::Recitation and * DownloadType::File */ - void addTaskProgress(QPointer job); + void addTaskProgress(QSharedPointer job); /** * @brief enqueue a surah to download * @param reciter - ::Globals::recitersList index for the reciter whose diff --git a/src/downloader/contentjob.cpp b/src/downloader/contentjob.cpp index 24a29e81..4dfe6564 100644 --- a/src/downloader/contentjob.cpp +++ b/src/downloader/contentjob.cpp @@ -6,19 +6,18 @@ ContentJob::ContentJob(Type type, int idx) : m_idx(idx) , m_type(type) , m_isDownloading(false) - , m_taskDlr(new TaskDownloader(this)) + , m_taskDlr(this) { if (type == DownloadJob::TafsirFile) m_task = new TafsirTask(idx); else if (type == DownloadJob::TranslationFile) m_task = new TranslationTask(idx); - connect(m_taskDlr, &TaskDownloader::fileFound, this, &ContentJob::fileFound); - connect(m_taskDlr, &TaskDownloader::completed, this, &DownloadJob::finished); - connect(m_taskDlr, &TaskDownloader::taskError, this, &DownloadJob::failed); + connect(&m_taskDlr, &TaskDownloader::completed, this, &DownloadJob::finished); + connect(&m_taskDlr, &TaskDownloader::taskError, this, &DownloadJob::failed); connect( - m_taskDlr, &TaskDownloader::progressed, this, &DownloadJob::progressed); - connect(m_taskDlr, + &m_taskDlr, &TaskDownloader::progressed, this, &DownloadJob::progressed); + connect(&m_taskDlr, &TaskDownloader::downloadSpeedUpdated, this, &DownloadJob::downloadSpeedUpdated); @@ -30,7 +29,11 @@ ContentJob::start() if (m_isDownloading) return; m_isDownloading = true; - m_taskDlr->process(m_task, &m_netMgr); + if (m_task->destination().exists()) { + emit fileFound(); + return; + } + m_taskDlr.process(m_task, &m_netMgr); } void @@ -38,7 +41,7 @@ ContentJob::stop() { if (!m_isDownloading) return; - m_taskDlr->cancel(); + m_taskDlr.cancel(); m_isDownloading = false; } @@ -51,13 +54,13 @@ ContentJob::isDownloading() int ContentJob::completed() { - return m_taskDlr->bytes() / 1024; + return m_taskDlr.bytes() / 1024; } int ContentJob::total() { - return m_taskDlr->total() / 1024; + return m_taskDlr.total() / 1024; } DownloadJob::Type diff --git a/src/downloader/contentjob.h b/src/downloader/contentjob.h index 8d2a0afc..416bb740 100644 --- a/src/downloader/contentjob.h +++ b/src/downloader/contentjob.h @@ -28,7 +28,7 @@ class ContentJob : public DownloadJob QList>& m_tafasir = Tafsir::tafasir; QList>& m_translations = Translation::translations; - QPointer m_taskDlr; + TaskDownloader m_taskDlr; QNetworkAccessManager m_netMgr; DownloadTask* m_task; Type m_type; diff --git a/src/downloader/jobmanager.cpp b/src/downloader/jobmanager.cpp index 669e6b48..4810c707 100644 --- a/src/downloader/jobmanager.cpp +++ b/src/downloader/jobmanager.cpp @@ -7,7 +7,7 @@ JobManager::JobManager(QObject* parent) } void -JobManager::addJob(QPointer job) +JobManager::addJob(QSharedPointer job) { m_queue.enqueue(job); } @@ -29,6 +29,7 @@ JobManager::stop() m_isOn = false; if (!m_active.isNull()) m_active->stop(); + m_queue.clear(); disconnectActive(); emit jobAborted(); } @@ -48,18 +49,27 @@ JobManager::processJobs() void JobManager::connectActive() { + if (!m_active.isNull()) + disconnectActive(); + if (m_active->type() == DownloadJob::TafsirFile || m_active->type() == DownloadJob::TranslationFile) - connect(qobject_cast(m_active), + connect(qobject_cast(m_active.data()), &ContentJob::fileFound, this, &JobManager::handleFilesFound); - connect(m_active, &DownloadJob::failed, this, &JobManager::handleFailed); - connect(m_active, &DownloadJob::finished, this, &JobManager::handleCompleted); connect( - m_active, &DownloadJob::progressed, this, &JobManager::handleProgressed); - connect(m_active, + m_active.data(), &DownloadJob::failed, this, &JobManager::handleFailed); + connect(m_active.data(), + &DownloadJob::finished, + this, + &JobManager::handleCompleted); + connect(m_active.data(), + &DownloadJob::progressed, + this, + &JobManager::handleProgressed); + connect(m_active.data(), &DownloadJob::downloadSpeedUpdated, this, &JobManager::downloadSpeedUpdated); @@ -70,17 +80,22 @@ JobManager::disconnectActive() { if (m_active->type() == DownloadJob::TafsirFile || m_active->type() == DownloadJob::TranslationFile) - disconnect(qobject_cast(m_active), + disconnect(qobject_cast(m_active.data()), &ContentJob::fileFound, this, &JobManager::handleFilesFound); - disconnect(m_active, &DownloadJob::failed, this, &JobManager::handleFailed); disconnect( - m_active, &DownloadJob::finished, this, &JobManager::handleCompleted); - disconnect( - m_active, &DownloadJob::progressed, this, &JobManager::handleProgressed); - disconnect(m_active, + m_active.data(), &DownloadJob::failed, this, &JobManager::handleFailed); + disconnect(m_active.data(), + &DownloadJob::finished, + this, + &JobManager::handleCompleted); + disconnect(m_active.data(), + &DownloadJob::progressed, + this, + &JobManager::handleProgressed); + disconnect(m_active.data(), &DownloadJob::downloadSpeedUpdated, this, &JobManager::downloadSpeedUpdated); @@ -96,24 +111,18 @@ void JobManager::handleFailed() { emit jobFailed(m_active); - disconnectActive(); - processJobs(); } void JobManager::handleCompleted() { emit jobCompleted(m_active); - disconnectActive(); - processJobs(); } void JobManager::handleFilesFound() { emit filesFound(m_active); - disconnectActive(); - processJobs(); } bool @@ -122,14 +131,8 @@ JobManager::isOn() const return m_isOn; } -QPointer +QSharedPointer JobManager::active() const { return m_active; } - -JobManager::~JobManager() -{ - if (!m_queue.isEmpty()) - qDeleteAll(m_queue); -} diff --git a/src/downloader/jobmanager.h b/src/downloader/jobmanager.h index 010268fe..81f1cfc3 100644 --- a/src/downloader/jobmanager.h +++ b/src/downloader/jobmanager.h @@ -3,43 +3,44 @@ #include "interfaces/downloadjob.h" #include -#include #include +#include class JobManager : public QObject { Q_OBJECT public: explicit JobManager(QObject* parent); - ~JobManager(); void start(); void stop(); - void addJob(QPointer job); + void addJob(QSharedPointer job); bool isOn() const; - QPointer active() const; + QSharedPointer active() const; + +public slots: + void processJobs(); signals: void jobAborted(); - void jobCompleted(QPointer job); - void jobFailed(QPointer job); - void jobProgressed(QPointer job); + void jobCompleted(QSharedPointer job); + void jobFailed(QSharedPointer job); + void jobProgressed(QSharedPointer job); void downloadSpeedUpdated(int speed, QString unit); - void filesFound(QPointer job); + void filesFound(QSharedPointer job); private slots: void handleProgressed(); void handleFailed(); void handleCompleted(); void handleFilesFound(); - void processJobs(); private: void connectActive(); void disconnectActive(); - QQueue> m_queue; - QPointer m_active; + QQueue> m_queue; + QSharedPointer m_active; bool m_isOn = false; }; diff --git a/src/downloader/qcfjob.cpp b/src/downloader/qcfjob.cpp index 7414c396..d8282c82 100644 --- a/src/downloader/qcfjob.cpp +++ b/src/downloader/qcfjob.cpp @@ -5,12 +5,11 @@ QcfJob::QcfJob() : m_completed(0) , m_active(0) , m_isDownloading(false) - , m_taskDlr(new TaskDownloader(this)) + , m_taskDlr(this) { - connect(m_taskDlr, &TaskDownloader::fileFound, this, &QcfJob::taskFinished); - connect(m_taskDlr, &TaskDownloader::completed, this, &QcfJob::taskFinished); - connect(m_taskDlr, &TaskDownloader::taskError, this, &QcfJob::taskFailed); - connect(m_taskDlr, + connect(&m_taskDlr, &TaskDownloader::completed, this, &QcfJob::taskFinished); + connect(&m_taskDlr, &TaskDownloader::taskError, this, &QcfJob::taskFailed); + connect(&m_taskDlr, &TaskDownloader::downloadSpeedUpdated, this, &DownloadJob::downloadSpeedUpdated); @@ -45,7 +44,7 @@ QcfJob::processTasks() m_active = m_queue.dequeue(); } - m_taskDlr->process(&m_active, &m_netMgr); + m_taskDlr.process(&m_active, &m_netMgr); } void @@ -82,7 +81,7 @@ QcfJob::stop() { if (!m_isDownloading) return; - m_taskDlr->cancel(); + m_taskDlr.cancel(); m_isDownloading = false; m_queue.clear(); emit DownloadJob::aborted(); @@ -118,7 +117,4 @@ QcfJob::name() return qApp->translate("SettingsDialog", "QCF V2"); } -QcfJob::~QcfJob() -{ - delete m_taskDlr; -} +QcfJob::~QcfJob() {} diff --git a/src/downloader/qcfjob.h b/src/downloader/qcfjob.h index 63334664..ac3113f0 100644 --- a/src/downloader/qcfjob.h +++ b/src/downloader/qcfjob.h @@ -30,7 +30,7 @@ private slots: void taskFailed(); private: - QPointer m_taskDlr; + TaskDownloader m_taskDlr; QQueue m_queue; QNetworkAccessManager m_netMgr; QcfTask m_active; diff --git a/src/downloader/surahjob.cpp b/src/downloader/surahjob.cpp index 3eec1ff7..bcfc647c 100644 --- a/src/downloader/surahjob.cpp +++ b/src/downloader/surahjob.cpp @@ -3,13 +3,15 @@ SurahJob::SurahJob(int reciter, int surah) : m_reciter(reciter) , m_surah(surah) + , m_completed(0) , m_surahCount(m_dbMgr->getSurahVerseCount(surah)) - , m_taskDlr(new TaskDownloader(this)) + , m_isDownloading(false) + , m_taskDlr(this) { - connect(m_taskDlr, &TaskDownloader::fileFound, this, &SurahJob::taskFinished); - connect(m_taskDlr, &TaskDownloader::completed, this, &SurahJob::taskFinished); - connect(m_taskDlr, &TaskDownloader::taskError, this, &SurahJob::taskFailed); - connect(m_taskDlr, + connect( + &m_taskDlr, &TaskDownloader::completed, this, &SurahJob::taskFinished); + connect(&m_taskDlr, &TaskDownloader::taskError, this, &SurahJob::taskFailed); + connect(&m_taskDlr, &TaskDownloader::downloadSpeedUpdated, this, &DownloadJob::downloadSpeedUpdated); @@ -36,6 +38,7 @@ SurahJob::processTasks() if (m_completed == m_surahCount) { emit DownloadJob::progressed(); emit DownloadJob::finished(); + m_isDownloading = false; } if (m_queue.isEmpty()) @@ -44,7 +47,7 @@ SurahJob::processTasks() m_active = m_queue.dequeue(); } - m_taskDlr->process(&m_active, &m_netMgr); + m_taskDlr.process(&m_active, &m_netMgr); } void @@ -73,7 +76,7 @@ SurahJob::stop() { if (!m_isDownloading) return; - m_taskDlr->cancel(); + m_taskDlr.cancel(); m_isDownloading = false; m_queue.clear(); emit DownloadJob::aborted(); @@ -130,7 +133,4 @@ SurahJob::surah() const return m_surah; } -SurahJob::~SurahJob() -{ - delete m_taskDlr; -} +SurahJob::~SurahJob() {} diff --git a/src/downloader/surahjob.h b/src/downloader/surahjob.h index cc5d0b6d..941d8aa9 100644 --- a/src/downloader/surahjob.h +++ b/src/downloader/surahjob.h @@ -34,7 +34,7 @@ private slots: private: QSharedPointer m_dbMgr = DBManager::current(); QList>& m_reciters = Reciter::reciters; - QPointer m_taskDlr; + TaskDownloader m_taskDlr; QQueue m_queue; QNetworkAccessManager m_netMgr; RecitationTask m_active; diff --git a/src/downloader/taskdownloader.cpp b/src/downloader/taskdownloader.cpp index 6b309ae3..1fe1356f 100644 --- a/src/downloader/taskdownloader.cpp +++ b/src/downloader/taskdownloader.cpp @@ -27,11 +27,6 @@ TaskDownloader::process(DownloadTask* task, QNetworkAccessManager* manager) &TaskDownloader::taskProgress); } - if (m_task->destination().exists()) { - emit fileFound(); - return; - } - QNetworkRequest req(m_task->url()); m_reply = manager->get(req); m_reply->ignoreSslErrors(); @@ -130,7 +125,4 @@ TaskDownloader::total() const return m_total; } -TaskDownloader::~TaskDownloader() -{ - delete m_task; -} +TaskDownloader::~TaskDownloader() {} diff --git a/src/downloader/taskdownloader.h b/src/downloader/taskdownloader.h index 37252dec..ebee80ec 100644 --- a/src/downloader/taskdownloader.h +++ b/src/downloader/taskdownloader.h @@ -24,7 +24,6 @@ class TaskDownloader : public QObject void progressed(qint64 bytes, qint64 total); void taskError(); void completed(); - void fileFound(); private slots: void taskProgress(qint64 bytes, qint64 total); diff --git a/src/widgets/downloadprogressbar.cpp b/src/widgets/downloadprogressbar.cpp index 142f670b..17546a71 100644 --- a/src/widgets/downloadprogressbar.cpp +++ b/src/widgets/downloadprogressbar.cpp @@ -20,8 +20,7 @@ DownloadProgressBar::DownloadProgressBar(QWidget* parent, Type type, int max) setFormat("%v / %m"); } -void -DownloadProgressBar::updateProgress(QPointer job) +void DownloadProgressBar::updateProgress(QSharedPointer job) { if (maximum() != job->total()) setMaximum(job->total()); diff --git a/src/widgets/downloadprogressbar.h b/src/widgets/downloadprogressbar.h index 33a316e1..57272e0a 100644 --- a/src/widgets/downloadprogressbar.h +++ b/src/widgets/downloadprogressbar.h @@ -36,7 +36,7 @@ class DownloadProgressBar : public QProgressBar }; public slots: - void updateProgress(QPointer job); + void updateProgress(QSharedPointer job); void setStyling(State); }; diff --git a/src/widgets/notificationpopup.cpp b/src/widgets/notificationpopup.cpp index dfaddef8..d599773c 100644 --- a/src/widgets/notificationpopup.cpp +++ b/src/widgets/notificationpopup.cpp @@ -81,16 +81,14 @@ NotificationPopup::notify(QString message, NotificationPopup::Action icon) m_notificationPeriod.start(); } -void -NotificationPopup::completedDownload(QPointer job) +void NotificationPopup::completedDownload(QSharedPointer job) { setStyleSheet(""); QString msg = tr("Download Completed") + ": " + job->name(); this->notify(msg, success); } -void -NotificationPopup::downloadError(QPointer job) +void NotificationPopup::downloadError(QSharedPointer job) { setStyleSheet("QFrame#Popup { background-color: #a50500 }"); QString msg = tr("Download Failed") + ": " + job->name(); diff --git a/src/widgets/notificationpopup.h b/src/widgets/notificationpopup.h index 98d21a3c..4c64870a 100644 --- a/src/widgets/notificationpopup.h +++ b/src/widgets/notificationpopup.h @@ -79,13 +79,13 @@ public slots: * @param reciterIdx - ::Globals::recitersList index for the reciter * @param surah - the surah that was downloaded */ - void completedDownload(QPointer job); + void completedDownload(QSharedPointer job); /** * @brief slot to show a notification on download error * @param reciterIdx - ::Globals::recitersList index for the reciter * @param surah - the surah that was downloaded */ - void downloadError(QPointer job); + void downloadError(QSharedPointer job); /** * @brief slot to show a notification on bookmark addition */ From ef7883a6ddd398b05a8724d4ac52ce26660be079 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sun, 25 Feb 2024 12:19:17 +0200 Subject: [PATCH 23/51] fix: fix ibn-juzayy tafsir db name --- resources/files.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/files.xml b/resources/files.xml index 2b8b1acc..262af89b 100644 --- a/resources/files.xml +++ b/resources/files.xml @@ -12,7 +12,7 @@ - + From a4b73f329b1d3776dc6574b74d72382321e648d4 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sun, 25 Feb 2024 12:26:15 +0200 Subject: [PATCH 24/51] fix: use alternative link for alafasy recitation download --- resources/reciters.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/reciters.xml b/resources/reciters.xml index 67ac54a2..287a394e 100644 --- a/resources/reciters.xml +++ b/resources/reciters.xml @@ -29,7 +29,7 @@ + url="https://verses.quran.com/Alafasy/mp3/" useid="0" /> Date: Sun, 25 Feb 2024 21:53:30 +0200 Subject: [PATCH 25/51] refactor: break down dbmanager class into separate classes --- CMakeLists.txt | 19 +- src/core/mainwindow.cpp | 54 ++--- src/core/mainwindow.h | 38 +-- src/core/playercontrols.cpp | 4 +- src/core/playercontrols.h | 5 +- src/core/quranreader.cpp | 42 ++-- src/core/quranreader.h | 17 +- src/database/betaqatdb.cpp | 49 ++++ src/database/betaqatdb.h | 31 +++ src/database/bookmarksdb.cpp | 306 ++++++++++++++++++++++++ src/database/bookmarksdb.h | 123 ++++++++++ src/database/glyphsdb.cpp | 98 ++++++++ src/database/glyphsdb.h | 52 ++++ src/database/qurandb.cpp | 380 ++++++++++++++++++++++++++++++ src/database/qurandb.h | 160 +++++++++++++ src/database/tafsirdb.cpp | 79 +++++++ src/database/tafsirdb.h | 60 +++++ src/database/translationdb.cpp | 78 ++++++ src/database/translationdb.h | 66 ++++++ src/dialogs/aboutdialog.h | 2 +- src/dialogs/bookmarksdialog.cpp | 21 +- src/dialogs/bookmarksdialog.h | 11 +- src/dialogs/contentdialog.cpp | 47 ++-- src/dialogs/contentdialog.h | 15 +- src/dialogs/copydialog.cpp | 10 +- src/dialogs/copydialog.h | 5 +- src/dialogs/downloaderdialog.cpp | 10 +- src/dialogs/downloaderdialog.h | 13 +- src/dialogs/khatmahdialog.cpp | 37 +-- src/dialogs/khatmahdialog.h | 9 +- src/dialogs/searchdialog.cpp | 24 +- src/dialogs/searchdialog.h | 9 +- src/dialogs/settingsdialog.cpp | 6 +- src/dialogs/settingsdialog.h | 8 +- src/dialogs/versedialog.cpp | 8 +- src/dialogs/versedialog.h | 9 +- src/downloader/contentjob.cpp | 4 +- src/downloader/contentjob.h | 6 +- src/downloader/jobmanager.h | 2 +- src/downloader/qcfjob.h | 4 +- src/downloader/qcftask.h | 4 +- src/downloader/recitationtask.cpp | 2 +- src/downloader/recitationtask.h | 10 +- src/downloader/surahjob.cpp | 4 +- src/downloader/surahjob.h | 4 +- src/downloader/tafsirtask.h | 6 +- src/downloader/taskdownloader.h | 2 +- src/downloader/translationtask.h | 6 +- src/interfaces/dbconnection.h | 28 +++ src/main.cpp | 20 +- src/types/reciter.cpp | 2 +- src/types/tafsir.cpp | 2 +- src/types/translation.cpp | 2 +- src/types/verse.cpp | 6 +- src/types/verse.h | 6 +- src/utils/dbmanager.cpp | 4 +- src/utils/dbmanager.h | 2 +- src/utils/fontmanager.cpp | 6 +- src/utils/settings.cpp | 2 +- src/utils/settings.h | 6 +- src/utils/shortcuthandler.cpp | 2 +- src/utils/systemtray.cpp | 7 +- src/utils/systemtray.h | 2 - src/utils/verseplayer.h | 6 +- src/widgets/betaqaviewer.cpp | 2 +- src/widgets/betaqaviewer.h | 4 +- src/widgets/downloadprogressbar.h | 2 +- src/widgets/notificationpopup.cpp | 8 +- src/widgets/notificationpopup.h | 8 +- src/widgets/quranpagebrowser.cpp | 14 +- src/widgets/quranpagebrowser.h | 7 +- 71 files changed, 1838 insertions(+), 269 deletions(-) create mode 100644 src/database/betaqatdb.cpp create mode 100644 src/database/betaqatdb.h create mode 100644 src/database/bookmarksdb.cpp create mode 100644 src/database/bookmarksdb.h create mode 100644 src/database/glyphsdb.cpp create mode 100644 src/database/glyphsdb.h create mode 100644 src/database/qurandb.cpp create mode 100644 src/database/qurandb.h create mode 100644 src/database/tafsirdb.cpp create mode 100644 src/database/tafsirdb.h create mode 100644 src/database/translationdb.cpp create mode 100644 src/database/translationdb.h create mode 100644 src/interfaces/dbconnection.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b929068..453d10bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,8 +86,6 @@ set(PROJECT_SOURCES src/utils/settings.cpp src/utils/shortcuthandler.h src/utils/shortcuthandler.cpp - src/utils/dbmanager.h - src/utils/dbmanager.cpp src/utils/verseplayer.h src/utils/verseplayer.cpp src/utils/systemtray.h @@ -100,8 +98,11 @@ set(PROJECT_SOURCES src/utils/stylemanager.cpp src/utils/fontmanager.h src/utils/fontmanager.cpp + src/utils/versionchecker.h + src/utils/versionchecker.cpp src/interfaces/downloadjob.h src/interfaces/downloadtask.h + src/interfaces/dbconnection.h src/downloader/surahjob.h src/downloader/surahjob.cpp src/downloader/recitationtask.h @@ -120,8 +121,18 @@ set(PROJECT_SOURCES src/downloader/qcfjob.cpp src/downloader/jobmanager.h src/downloader/jobmanager.cpp - src/utils/versionchecker.h - src/utils/versionchecker.cpp + src/database/qurandb.h + src/database/qurandb.cpp + src/database/glyphsdb.h + src/database/glyphsdb.cpp + src/database/betaqatdb.h + src/database/betaqatdb.cpp + src/database/tafsirdb.h + src/database/tafsirdb.cpp + src/database/translationdb.h + src/database/translationdb.cpp + src/database/bookmarksdb.h + src/database/bookmarksdb.cpp src/widgets/quranpagebrowser.h src/widgets/quranpagebrowser.cpp src/widgets/clickablelabel.cpp diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 1bc4857f..57a27712 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -4,11 +4,11 @@ */ #include "mainwindow.h" -#include "dialogs/aboutdialog.h" -#include "dialogs/khatmahdialog.h" #include "ui_mainwindow.h" -#include "utils/stylemanager.h" #include +#include +#include +#include using namespace fa; MainWindow::MainWindow(QWidget* parent) @@ -66,10 +66,10 @@ MainWindow::loadVerse() { int id = m_settings->value("Reader/Khatmah").toInt(); QList vInfo = m_currVerse->toList(); - m_dbMgr->setActiveKhatmah(id); - if (!m_dbMgr->loadVerse(id, vInfo)) { + m_bookmarkDb->setActiveKhatmah(id); + if (!m_bookmarkDb->loadVerse(id, vInfo)) { QString name = id ? tr("Khatmah ") + QString::number(id) : tr("Default"); - m_dbMgr->addKhatmah(vInfo, name, id); + m_bookmarkDb->addKhatmah(vInfo, name, id); } m_currVerse->update(vInfo); @@ -113,7 +113,7 @@ MainWindow::loadComponents() // sets without emitting signal setCmbVerseIdx(m_currVerse->number() - 1); - setCmbJuzIdx(m_dbMgr->getJuzOfPage(m_currVerse->page()) - 1); + setCmbJuzIdx(m_quranDb->getJuzOfPage(m_currVerse->page()) - 1); ui->cmbPage->setCurrentIndex(m_currVerse->page() - 1); } @@ -317,12 +317,12 @@ MainWindow::setupConnections() &CopyDialog::verseCopied, m_popup, &NotificationPopup::copiedToClipboard); - connect(m_dbMgr.data(), - &DBManager::bookmarkAdded, + connect(m_bookmarkDb.data(), + &BookmarksDb::bookmarkAdded, m_popup, &NotificationPopup::bookmarkAdded); - connect(m_dbMgr.data(), - &DBManager::bookmarkRemoved, + connect(m_bookmarkDb.data(), + &BookmarksDb::bookmarkRemoved, m_popup, &NotificationPopup::bookmarkRemoved); @@ -353,8 +353,8 @@ MainWindow::setupConnections() &QuranReader::addSideContent); connect(m_settingsDlg, &SettingsDialog::translationChanged, - m_dbMgr.data(), - &DBManager::updateLoadedTranslation); + m_translationDb.data(), + &TranslationDb::updateLoadedTranslation); connect(m_settingsDlg, &SettingsDialog::sideFontChanged, m_reader, @@ -430,7 +430,7 @@ MainWindow::setupSurahsDock() { for (int i = 1; i < 115; i++) { QString item = QString::number(i).rightJustified(3, '0') + ' ' + - m_dbMgr->surahNameList().at(i - 1); + m_quranDb->surahNames().at(i - 1); m_surahList.append(item); } @@ -495,7 +495,7 @@ MainWindow::currentVerseChanged() { setCmbVerseIdx(m_currVerse->number() - 1); setCmbPageIdx(m_currVerse->page() - 1); - setCmbJuzIdx(m_dbMgr->getJuzOfPage(m_currVerse->page()) - 1); + setCmbJuzIdx(m_quranDb->getJuzOfPage(m_currVerse->page()) - 1); } void @@ -539,8 +539,8 @@ MainWindow::setVerseComboBoxRange(bool forceUpdate) m_verseValidator->setTop(m_currVerse->surahCount()); // updates values in the combobox with the current surah verses - ui->cmbVerse->clear(); m_internalVerseChange = true; + ui->cmbVerse->clear(); for (int i = 1; i <= m_currVerse->surahCount(); i++) ui->cmbVerse->addItem(QString::number(i), i); m_internalVerseChange = false; @@ -556,7 +556,7 @@ MainWindow::searchSurahTextChanged(const QString& arg1) m_surahListModel.setStringList(m_surahList); syncSelectedSurah(); } else { - QList suggestions = m_dbMgr->searchSurahNames(arg1); + QList suggestions = m_quranDb->searchSurahNames(arg1); QStringList res; foreach (int sura, suggestions) res.append(m_surahList.at(sura - 1)); @@ -595,7 +595,7 @@ MainWindow::cmbVerseChanged(int newVerseIdx) } int verse = newVerseIdx + 1; - int page = m_dbMgr->getVersePage(m_currVerse->surah(), verse); + int page = m_quranDb->getVersePage(m_currVerse->surah(), verse); if (page != m_currVerse->page()) m_reader->gotoPage(page, false); @@ -605,7 +605,7 @@ MainWindow::cmbVerseChanged(int newVerseIdx) m_reader->highlightCurrentVerse(); setCmbPageIdx(page - 1); - setCmbJuzIdx(m_dbMgr->getJuzOfPage(page) - 1); + setCmbJuzIdx(m_quranDb->getJuzOfPage(page) - 1); // open newly set verse recitation file m_player->loadActiveVerse(); @@ -618,7 +618,7 @@ MainWindow::cmbJuzChanged(int newJuzIdx) qDebug() << "Internal jozz change"; return; } - int page = m_dbMgr->getJuzStartPage(newJuzIdx + 1); + int page = m_quranDb->getJuzStartPage(newJuzIdx + 1); m_reader->gotoPage(page); } @@ -633,9 +633,9 @@ void MainWindow::updateTrayTooltip(QMediaPlayer::PlaybackState state) { if (state == QMediaPlayer::PlayingState) { - m_systemTray->setTooltip(tr("Now playing: ") + m_player->reciterName() + - " - " + tr("Surah ") + - m_dbMgr->getSurahName(m_currVerse->surah())); + m_systemTray->setTooltip( + tr("Now playing: ") + m_player->reciterName() + " - " + tr("Surah ") + + m_quranDb->surahNames().at(m_currVerse->surah() - 1)); } else m_systemTray->setTooltip(tr("Quran Companion")); } @@ -644,8 +644,8 @@ void MainWindow::addCurrentToBookmarks() { QList vInfo = m_currVerse->toList(); - if (!m_dbMgr->isBookmarked(vInfo)) - m_dbMgr->addBookmark(vInfo); + if (!m_bookmarkDb->isBookmarked(vInfo)) + m_bookmarkDb->addBookmark(vInfo); } void @@ -755,7 +755,7 @@ MainWindow::actionKhatmahTriggered() &QuranReader::navigateToVerse); } - m_dbMgr->saveActiveKhatmah(m_currVerse->toList()); + m_bookmarkDb->saveActiveKhatmah(m_currVerse->toList()); m_khatmahDlg->show(); } @@ -895,7 +895,7 @@ MainWindow::saveReaderState() m_settings->setValue("Reciter", m_playerControls->currentReciter()); m_settings->sync(); - m_dbMgr->saveActiveKhatmah(m_currVerse->toList()); + m_bookmarkDb->saveActiveKhatmah(m_currVerse->toList()); } void diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 2447cc2b..299b1360 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -6,24 +6,8 @@ #ifndef MAINWINDOW_H #define MAINWINDOW_H -#include "dialogs/bookmarksdialog.h" -#include "dialogs/contentdialog.h" -#include "dialogs/copydialog.h" -#include "dialogs/downloaderdialog.h" -#include "dialogs/khatmahdialog.h" -#include "dialogs/searchdialog.h" -#include "dialogs/settingsdialog.h" -#include "dialogs/versedialog.h" #include "playercontrols.h" #include "quranreader.h" -#include "types/verse.h" -#include "utils/dbmanager.h" -#include "utils/shortcuthandler.h" -#include "utils/systemtray.h" -#include "utils/verseplayer.h" -#include "utils/versionchecker.h" -#include "widgets/betaqaviewer.h" -#include "widgets/notificationpopup.h" #include #include #include @@ -32,6 +16,24 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include QT_BEGIN_NAMESPACE namespace Ui { @@ -248,7 +250,9 @@ private slots: private: Ui::MainWindow* ui; QSharedPointer m_currVerse = Verse::current(); - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_quranDb = QuranDb::current(); + QSharedPointer m_bookmarkDb = BookmarksDb::current(); + QSharedPointer m_translationDb = TranslationDb::current(); QSharedPointer m_shortcutHandler = ShortcutHandler::current(); QSharedPointer m_settings = Settings::settings; diff --git a/src/core/playercontrols.cpp b/src/core/playercontrols.cpp index 64888c10..467fae12 100644 --- a/src/core/playercontrols.cpp +++ b/src/core/playercontrols.cpp @@ -1,8 +1,8 @@ #include "playercontrols.h" #include "ui_playercontrols.h" -#include "utils/shortcuthandler.h" -#include "utils/stylemanager.h" #include +#include +#include using namespace fa; PlayerControls::PlayerControls(QWidget* parent, diff --git a/src/core/playercontrols.h b/src/core/playercontrols.h index ca406631..0c584feb 100644 --- a/src/core/playercontrols.h +++ b/src/core/playercontrols.h @@ -2,9 +2,9 @@ #define PLAYERCONTROLS_H #include "quranreader.h" -#include "types/verse.h" -#include "utils/verseplayer.h" #include +#include +#include namespace Ui { class PlayerControls; @@ -79,7 +79,6 @@ private slots: private: Ui::PlayerControls* ui; QSharedPointer m_currVerse = Verse::current(); - QSharedPointer m_dbMgr = DBManager::current(); QSharedPointer const m_settings = Settings::settings; const QList>& m_reciters = Reciter::reciters; /** diff --git a/src/core/quranreader.cpp b/src/core/quranreader.cpp index 627d18e3..6316dbba 100644 --- a/src/core/quranreader.cpp +++ b/src/core/quranreader.cpp @@ -1,10 +1,10 @@ #include "quranreader.h" #include "ui_quranreader.h" -#include "utils/fontmanager.h" -#include "utils/shortcuthandler.h" -#include "utils/stylemanager.h" -#include "widgets/clickablelabel.h" #include +#include +#include +#include +#include using namespace fa; QuranReader::QuranReader(QWidget* parent, VersePlayer* player) @@ -162,7 +162,7 @@ QuranReader::updateVerseType() qvariant_cast(m_settings->value("Reader/VerseType")); m_versesFont.setFamily(FontManager::verseFontname(type, m_currVerse->page())); m_versesFont.setPointSize(m_settings->value("Reader/VerseFontSize").toInt()); - m_dbMgr->setVerseType(type); + m_quranDb->setVerseType(type); } void @@ -213,11 +213,11 @@ QuranReader::addSideContent() ClickableLabel* verselb; QLabel* contentLb; VerseFrame* verseContFrame; - QString prevLbContent, currLbContent; - if (m_dbMgr->getVerseType() == Settings::qcf) + QString prevLbContent, currLbContent, glyphs; + if (m_quranDb->verseType() == Settings::Qcf) m_versesFont.setFamily(FontManager::pageFontname(m_currVerse->page())); - m_dbMgr->setCurrentTranslation( + m_translationDb->setCurrentTranslation( m_settings->value("Reader/Translation").toInt()); for (int i = m_activeVList->size() - 1; i >= 0; i--) { const Verse* vInfo = &(m_activeVList->at(i)); @@ -225,16 +225,20 @@ QuranReader::addSideContent() verseContFrame = new VerseFrame(m_scrlVerseByVerse->widget()); verselb = new ClickableLabel(verseContFrame); contentLb = new QLabel(verseContFrame); + glyphs = m_quranDb->verseType() == Settings::Qcf + ? m_glyphsDb->getVerseGlyphs(vInfo->surah(), vInfo->number()) + : m_quranDb->verseText(vInfo->surah(), vInfo->number()); verseContFrame->setObjectName( QString("%0_%1").arg(vInfo->surah()).arg(vInfo->number())); verselb->setFont(m_versesFont); - verselb->setText(m_dbMgr->getVerseGlyphs(vInfo->surah(), vInfo->number())); + verselb->setText(glyphs); verselb->setAlignment(Qt::AlignCenter); verselb->setWordWrap(true); - currLbContent = m_dbMgr->getTranslation(vInfo->surah(), vInfo->number()); + currLbContent = + m_translationDb->getTranslation(vInfo->surah(), vInfo->number()); if (currLbContent == prevLbContent) { currLbContent = '-'; @@ -312,18 +316,18 @@ QuranReader::updatePageVerseInfoList() { if (m_activeQuranBrowser == m_quranBrowsers[0]) { m_vLists[0] = - Verse::fromList(m_dbMgr->getVerseInfoList(m_currVerse->page())); + Verse::fromList(m_quranDb->verseInfoList(m_currVerse->page())); if (m_readerMode == Settings::DoublePage) m_vLists[1] = - Verse::fromList(m_dbMgr->getVerseInfoList(m_currVerse->page() + 1)); + Verse::fromList(m_quranDb->verseInfoList(m_currVerse->page() + 1)); m_activeVList = &m_vLists[0]; } else { m_vLists[0] = - Verse::fromList(m_dbMgr->getVerseInfoList(m_currVerse->page() - 1)); + Verse::fromList(m_quranDb->verseInfoList(m_currVerse->page() - 1)); m_vLists[1] = - Verse::fromList(m_dbMgr->getVerseInfoList(m_currVerse->page())); + Verse::fromList(m_quranDb->verseInfoList(m_currVerse->page())); m_activeVList = &m_vLists[1]; } @@ -389,7 +393,6 @@ QuranReader::setVerseToStartOfPage() { // set the current verse to the verse at the top of the page m_currVerse->update(m_activeVList->at(0)); - m_player->loadActiveVerse(); } void @@ -408,7 +411,7 @@ QuranReader::verseAnchorClicked(const QUrl& hrefUrl) Verse v(m_vLists[browerIdx].at(idx)); QuranPageBrowser::Action chosenAction = - senderBrowser->lmbVerseMenu(m_dbMgr->isBookmarked(v.toList())); + senderBrowser->lmbVerseMenu(m_bookmarksDb->isBookmarked(v.toList())); switch (chosenAction) { case QuranPageBrowser::Play: @@ -434,11 +437,10 @@ QuranReader::verseAnchorClicked(const QUrl& hrefUrl) emit copyVerseText(v); break; case QuranPageBrowser::AddBookmark: - m_dbMgr->addBookmark(v.toList()); + m_bookmarksDb->addBookmark(v.toList()); break; case QuranPageBrowser::RemoveBookmark: - if (m_dbMgr->removeBookmark(v.toList())) - break; + m_bookmarksDb->removeBookmark(v.toList()); default: break; } @@ -553,7 +555,7 @@ void QuranReader::gotoSurah(int surahIdx) { // getting surah index - int page = m_dbMgr->getSurahStartPage(surahIdx); + int page = m_quranDb->surahStartPage(surahIdx); gotoPage(page, false); m_currVerse->setPage(page); diff --git a/src/core/quranreader.h b/src/core/quranreader.h index 6d885b51..b7fdbc9c 100644 --- a/src/core/quranreader.h +++ b/src/core/quranreader.h @@ -1,13 +1,16 @@ #ifndef QURANREADER_H #define QURANREADER_H -#include "types/verse.h" -#include "utils/verseplayer.h" -#include "widgets/quranpagebrowser.h" -#include "widgets/verseframe.h" #include #include #include +#include +#include +#include +#include +#include +#include +#include typedef Settings::ReaderMode ReaderMode; namespace Ui { @@ -139,7 +142,11 @@ private slots: private: Ui::QuranReader* ui; QSharedPointer m_currVerse = Verse::current(); - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_quranDb = QuranDb::current(); + QSharedPointer m_glyphsDb = GlyphsDb::current(); + QSharedPointer m_bookmarksDb = BookmarksDb::current(); + QSharedPointer m_tafsirDb = TafsirDb::current(); + QSharedPointer m_translationDb = TranslationDb::current(); QSharedPointer m_settings = Settings::settings; const QList>& m_recitersList = Reciter::reciters; const QList>& m_tafasir = Tafsir::tafasir; diff --git a/src/database/betaqatdb.cpp b/src/database/betaqatdb.cpp new file mode 100644 index 00000000..31cce9bc --- /dev/null +++ b/src/database/betaqatdb.cpp @@ -0,0 +1,49 @@ +#include "betaqatdb.h" +#include + +QSharedPointer +BetaqatDb::current() +{ + static QSharedPointer betaqatDb = + QSharedPointer::create(); + return betaqatDb; +} + +BetaqatDb::BetaqatDb() + : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "BetaqatCon")) +{ + BetaqatDb::open(); +} + +void +BetaqatDb::open() +{ + setDatabaseName(m_assetsDir->absoluteFilePath("betaqat.db")); + if (!QSqlDatabase::open()) + qFatal("Error opening betaqat db"); +} + +DbConnection::Type +BetaqatDb::type() +{ + return DbConnection::Betaqat; +} + +QString +BetaqatDb::getBetaqa(const int surah) +{ + QSqlQuery dbQuery(*this); + + if (m_languageCode == QLocale::Arabic) + dbQuery.prepare("SELECT text FROM content WHERE sura=:i"); + else + dbQuery.prepare("SELECT text_en FROM content WHERE sura=:i"); + + dbQuery.bindValue(0, surah); + if (!dbQuery.exec()) { + qCritical() << "Error occurred during getBetaqa SQL statment exec"; + } + + dbQuery.next(); + return dbQuery.value(0).toString(); +} diff --git a/src/database/betaqatdb.h b/src/database/betaqatdb.h new file mode 100644 index 00000000..19ec1063 --- /dev/null +++ b/src/database/betaqatdb.h @@ -0,0 +1,31 @@ +#ifndef BETAQATDB_H +#define BETAQATDB_H + +#include +#include +#include +#include +#include + +class BetaqatDb + : public DbConnection + , QSqlDatabase +{ +public: + static QSharedPointer current(); + BetaqatDb(); + void open(); + Type type(); + /** + * @brief get the surah card (betaqa) content + * @param surah - surah number + * @return QString of the surah card text + */ + QString getBetaqa(const int surah); + +private: + const QLocale::Language m_languageCode = Settings::language; + const QSharedPointer m_assetsDir = DirManager::assetsDir; +}; + +#endif // BETAQATDB_H diff --git a/src/database/bookmarksdb.cpp b/src/database/bookmarksdb.cpp new file mode 100644 index 00000000..bc6e296f --- /dev/null +++ b/src/database/bookmarksdb.cpp @@ -0,0 +1,306 @@ +#include "bookmarksdb.h" +#include +#include + +QSharedPointer +BookmarksDb::current() +{ + static QSharedPointer bookmarkDb = + QSharedPointer::create(); + return bookmarkDb; +} + +BookmarksDb::BookmarksDb() + : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "BookmarksCon")) +{ + BookmarksDb::open(); +} + +void +BookmarksDb::open() +{ + setDatabaseName(m_configDir->absoluteFilePath("bookmarks.db")); + if (!QSqlDatabase::open()) + qFatal("Error opening bookmarks db"); +} + +DbConnection::Type +BookmarksDb::type() +{ + return DbConnection::Bookmarks; +} + +bool +BookmarksDb::saveActiveKhatmah(QList vInfo) +{ + QSqlQuery dbQuery(*this); + QString q = QString::asprintf( + "UPDATE khatmah SET page=%i, surah=%i, number=%i WHERE id=%i", + vInfo[0], + vInfo[1], + vInfo[2], + m_activeKhatmah); + if (!dbQuery.exec(q)) { + qCritical() << "Couldn't save position in mushaf"; + return false; + } + if (!commit()) + return false; + + return true; +} + +QList +BookmarksDb::getAllKhatmah() +{ + QList res; + QSqlQuery dbQuery(*this); + if (!dbQuery.exec("SELECT id FROM khatmah")) + qCritical() << "Couldn't execute sql query: " << dbQuery.lastQuery(); + + while (dbQuery.next()) + res.append(dbQuery.value(0).toInt()); + + return res; +} + +QString +BookmarksDb::getKhatmahName(const int id) +{ + QSqlQuery dbQuery(*this); + if (!dbQuery.exec("SELECT name FROM khatmah WHERE id=" + QString::number(id))) + qCritical() << "Couldn't execute sql query: " << dbQuery.lastQuery(); + + dbQuery.next(); + return dbQuery.value(0).toString(); +} + +bool +BookmarksDb::loadVerse(const int khatmahId, QList& vInfo) +{ + QSqlQuery dbQuery(*this); + + QString q = QString::asprintf( + "SELECT page,surah,number FROM khatmah WHERE id=%i", khatmahId); + if (!dbQuery.exec(q)) { + qCritical() << "Couldn't execute getPosition SQL query!"; + return false; + } + if (!dbQuery.next()) + return false; + + vInfo[0] = dbQuery.value(0).toInt(); + vInfo[1] = dbQuery.value(1).toInt(); + vInfo[2] = dbQuery.value(2).toInt(); + return true; +} + +int +BookmarksDb::addKhatmah(QList vInfo, const QString name, const int id) +{ + QSqlQuery dbQuery(*this); + dbQuery.exec( + "CREATE TABLE IF NOT EXISTS khatmah(id INTEGER PRIMARY KEY " + "AUTOINCREMENT, name TEXT, page INTEGER, surah INTEGER, number INTEGER)"); + QString q; + if (id == -1) { + q = "INSERT INTO khatmah(name, page, surah, number) VALUES ('%0', %1, %2, " + "%3)"; + dbQuery.prepare(q.arg(name, + QString::number(vInfo[0]), + QString::number(vInfo[1]), + QString::number(vInfo[2]))); + } else { + q = "REPLACE INTO khatmah VALUES " + "(%0, " + "'%1', %2, %3, %4)"; + dbQuery.prepare(q.arg(QString::number(id), + name, + QString::number(vInfo[0]), + QString::number(vInfo[1]), + QString::number(vInfo[2]))); + } + + if (!dbQuery.exec()) { + qCritical() << "Couldn't create new khatmah entry!"; + qDebug() << lastError(); + return -1; + } + + if (id != -1) + return id; + + dbQuery.exec("SELECT id FROM khatmah ORDER BY id DESC limit 1"); + dbQuery.next(); + return dbQuery.value(0).toInt(); +} + +bool +BookmarksDb::editKhatmahName(const int khatmahId, QString newName) +{ + QSqlQuery dbQuery(*this); + QString q = "SELECT DISTINCT id FROM khatmah WHERE name='%0'"; + if (!dbQuery.exec(q.arg(newName))) { + qCritical() << "Couldn't execute sql query: " << dbQuery.lastQuery(); + qDebug() << lastError(); + return false; + } + if (dbQuery.next()) + return false; + + q = "UPDATE khatmah SET name='%0' WHERE id=%1"; + if (!dbQuery.exec(q.arg(newName, QString::number(khatmahId)))) { + qCritical() << "Couldn't rename khatmah entry!"; + qDebug() << lastError(); + return false; + } + + commit(); + return true; +} + +void +BookmarksDb::removeKhatmah(const int id) +{ + QSqlQuery dbQuery(*this); + if (!dbQuery.exec(QString::asprintf("DELETE FROM khatmah WHERE id=%i", id))) + qDebug() << "Couldn't execute query: " << dbQuery.lastQuery(); +} + +QList> +BookmarksDb::bookmarkedVerses(int surahIdx) +{ + QList> results; + QSqlQuery dbQuery(*this); + QString q = "SELECT page,surah,number FROM favorites"; + if (surahIdx != -1) + q.append(" WHERE surah=" + QString::number(surahIdx)); + + dbQuery.prepare(q.append(" ORDER BY surah, number")); + if (!dbQuery.exec()) + qCritical() << "Couldn't execute bookmarkedVerses SELECT query"; + + while (dbQuery.next()) { + results.append({ dbQuery.value(0).toInt(), + dbQuery.value(1).toInt(), + dbQuery.value(2).toInt() }); + } + + return results; +} + +bool +BookmarksDb::isBookmarked(QList vInfo) +{ + QSqlQuery dbQuery(*this); + + dbQuery.prepare( + "SELECT page FROM favorites WHERE page=:p AND surah=:s AND number=:n"); + dbQuery.bindValue(0, vInfo[0]); + dbQuery.bindValue(1, vInfo[1]); + dbQuery.bindValue(2, vInfo[2]); + + if (!dbQuery.exec()) { + qWarning() << "Couldn't check if verse is bookmarked"; + return false; + } + + dbQuery.next(); + + return dbQuery.isValid(); +} + +bool +BookmarksDb::addBookmark(QList vInfo) +{ + QSqlQuery dbQuery(*this); + dbQuery.exec("CREATE TABLE IF NOT EXISTS favorites(id INTEGER PRIMARY KEY " + "AUTOINCREMENT," + "page INTEGER, surah INTEGER, number INTEGER)"); + + dbQuery.prepare( + "INSERT INTO favorites(page, surah, number) VALUES (:p, :s, :n)"); + dbQuery.bindValue(0, vInfo[0]); + dbQuery.bindValue(1, vInfo[1]); + dbQuery.bindValue(2, vInfo[2]); + + if (!dbQuery.exec()) { + qWarning() << "Couldn't add verse to bookmarks db"; + return false; + } + + commit(); + emit bookmarkAdded(); + return true; +} + +bool +BookmarksDb::removeBookmark(QList vInfo) +{ + QSqlQuery dbQuery(*this); + dbQuery.prepare( + "DELETE FROM favorites WHERE page=:p AND surah=:s AND number=:n"); + dbQuery.bindValue(0, vInfo[0]); + dbQuery.bindValue(1, vInfo[1]); + dbQuery.bindValue(2, vInfo[2]); + + if (!dbQuery.exec()) { + qWarning() << "Couldn't remove verse from bookmarks"; + return false; + } + + emit bookmarkRemoved(); + return true; +} + +void +BookmarksDb::saveThoughts(QList vInfo, const QString& text) +{ + int id = m_quranDb->verseId(vInfo[1], vInfo[2]); + QSqlQuery dbQuery(*this); + dbQuery.exec("CREATE TABLE IF NOT EXISTS thoughts(id INTEGER PRIMARY KEY " + "UNIQUE," + "page INTEGER, surah INTEGER, number INTEGER, text TEXT)"); + + dbQuery.prepare("REPLACE INTO thoughts(id, page, surah, number, text) " + "VALUES(:i, :p, :s, :n, :t)"); + dbQuery.bindValue(0, id); + dbQuery.bindValue(1, vInfo[0]); + dbQuery.bindValue(2, vInfo[1]); + dbQuery.bindValue(3, vInfo[2]); + dbQuery.bindValue(4, text); + + if (!dbQuery.exec()) + qCritical() << "SQL statement execution error:" << dbQuery.lastError(); + + commit(); +} + +QString +BookmarksDb::getThoughts(QList vInfo) +{ + QSqlQuery dbQuery(*this); + dbQuery.prepare( + "SELECT text FROM thoughts WHERE page=:p AND surah=:s AND number=:n"); + dbQuery.bindValue(0, vInfo[0]); + dbQuery.bindValue(1, vInfo[1]); + dbQuery.bindValue(2, vInfo[2]); + + if (!dbQuery.exec()) + qCritical() << "SQL statement execution error:" << dbQuery.lastError(); + + dbQuery.next(); + return dbQuery.value(0).toString(); +} + +void +BookmarksDb::setActiveKhatmah(const int id) +{ + m_activeKhatmah = id; +} + +int +BookmarksDb::activeKhatmah() const +{ + return m_activeKhatmah; +} diff --git a/src/database/bookmarksdb.h b/src/database/bookmarksdb.h new file mode 100644 index 00000000..bda22913 --- /dev/null +++ b/src/database/bookmarksdb.h @@ -0,0 +1,123 @@ +#ifndef BOOKMARKSDB_H +#define BOOKMARKSDB_H + +#include +#include +#include +#include +#include +#include +#include +#include + +class BookmarksDb + : public DbConnection + , QSqlDatabase +{ + Q_OBJECT +public: + static QSharedPointer current(); + BookmarksDb(); + void open(); + Type type(); + /** + * @brief sets the given ::Verse as the last position reached in the current + * active khatmah + * @param v - ::Verse reached in khatmah + */ + bool saveActiveKhatmah(QList vInfo); + /** + * @brief get all available khatmah ids + * @return QList of khatmah id(s) + */ + QList getAllKhatmah(); + /** + * @brief get the name of the khatmah with id given + * @return QString containing the khatmah name + */ + QString getKhatmahName(const int id); + /** + * @brief gets the last position saved for the khatmah with the id given and + * stores the position in the ::Verse v + * @return boolean indicating a successful operation (false in case of error + * and in case id does not exist) + */ + bool loadVerse(const int khatmahId, QList& vInfo); + /** + * @brief add a new khatmah/replace khatmah with given id with position of + * ::Verse v + * @param v - ::Verse to set as the khatmah position + * @param name - new khatmah name + * @param id - id of khatmah to replace, -1 means do not replace (default: -1) + * @return id of newly added khatmah or id parameter if defined + */ + int addKhatmah(QList vInfo, const QString name, const int id = -1); + /** + * @brief rename the khatmah with the given id to newName + * @param khatmahId - id of khatmah to rename + * @param newName - new name to set + * @return boolean indicating a successful operation (false in case the name + * exists) + */ + bool editKhatmahName(const int khatmahId, QString newName); + /** + * @brief remove the khatmah with the given id from database + * @param id - id of khatmah to remove + */ + void removeKhatmah(const int id); + /** + * @brief gets a QList of ::Verse instances representing the bookmarked verse + * within the given sura (default gets all) + * @param surahIdx - sura number (-1 returns all bookmarks) + * @return QList of bookmarked verses + */ + QList> bookmarkedVerses(int surahIdx = -1); + /** + * @brief checks whether the given ::Verse is bookmarked + * @param vInfo - ::Verse instance to check + * @return boolean + */ + bool isBookmarked(QList vInfo); + /** + * @brief add the given ::Verse to bookmarks + * @param vInfo - ::Verse instance to add + * @return boolean + */ + bool addBookmark(QList vInfo); + /** + * @brief remove the given ::Verse from bookmarks + * @param vInfo - ::Verse instance to remove + * @return boolean indicating successful removal + */ + bool removeBookmark(QList vInfo); + /** + * @brief setter for m_activeKhatmah + * @param id - id of the active khatmah + */ + void setActiveKhatmah(const int id); + /** + * MODIFIED + */ + void saveThoughts(QList vInfo, const QString& text); + /** + * MODIFIED + */ + QString getThoughts(QList vInfo); + + int activeKhatmah() const; + +signals: + void bookmarkAdded(); + void bookmarkRemoved(); + +private: + const QSharedPointer m_quranDb = QuranDb::current(); + const QSharedPointer m_settings = Settings::settings; + const QSharedPointer m_configDir = DirManager::configDir; + /** + * @brief integer id of the current active khatmah + */ + int m_activeKhatmah = 0; +}; + +#endif // BOOKMARKSDB_H diff --git a/src/database/glyphsdb.cpp b/src/database/glyphsdb.cpp new file mode 100644 index 00000000..8fc6df28 --- /dev/null +++ b/src/database/glyphsdb.cpp @@ -0,0 +1,98 @@ +#include "glyphsdb.h" +#include + +QSharedPointer +GlyphsDb::current() +{ + static QSharedPointer glyphsDb = QSharedPointer::create(); + return glyphsDb; +} + +GlyphsDb::GlyphsDb() + : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "GlyphsCon")) +{ + GlyphsDb::open(); +} + +void +GlyphsDb::open() +{ + setDatabaseName(m_assetsDir->absoluteFilePath("glyphs.db")); + if (!QSqlDatabase::open()) + qFatal("Error opening glyphs db"); +} + +DbConnection::Type +GlyphsDb::type() +{ + return DbConnection::Glyphs; +} + +QStringList +GlyphsDb::getPageLines(const int page) +{ + QSqlQuery dbQuery(*this); + + QString query = "SELECT %0 FROM pages WHERE page_no=%1"; + query = query.arg("qcf_v" + QString::number(m_qcfVer), QString::number(page)); + + dbQuery.prepare(query); + if (!dbQuery.exec()) + qFatal("Couldn't execute getPageLines query!"); + + dbQuery.next(); + QStringList lines = dbQuery.value(0).toString().trimmed().split('\n'); + + return lines; +} + +QString +GlyphsDb::getSurahNameGlyph(const int sura) +{ + QSqlQuery dbQuery(*this); + + dbQuery.prepare("SELECT qcf_v1 FROM surah_glyphs WHERE surah=:i"); + dbQuery.bindValue(0, sura); + if (!dbQuery.exec()) { + qCritical() << "Error occurred during getSurahNameGlyph SQL statment exec"; + } + + dbQuery.next(); + + return dbQuery.value(0).toString(); +} + +QString +GlyphsDb::getJuzGlyph(const int juz) +{ + QSqlQuery dbQuery(*this); + + dbQuery.prepare("SELECT text FROM juz_glyphs WHERE juz=:j"); + dbQuery.bindValue(0, juz); + if (!dbQuery.exec()) { + qCritical() << "Error occurred during getJuzGlyph SQL statment exec"; + } + + dbQuery.next(); + + return dbQuery.value(0).toString(); +} + +QString +GlyphsDb::getVerseGlyphs(const int sIdx, const int vIdx) +{ + QSqlQuery dbQuery(*this); + + QString query = "SELECT %0 FROM ayah_glyphs WHERE surah=%1 AND ayah=%2"; + query = query.arg("qcf_v" + QString::number(m_qcfVer), + QString::number(sIdx), + QString::number(vIdx)); + + dbQuery.prepare(query); + if (!dbQuery.exec()) + qFatal("Couldn't execute getVerseGlyphs query!"); + + dbQuery.next(); + + return dbQuery.value(0).toString(); +} diff --git a/src/database/glyphsdb.h b/src/database/glyphsdb.h new file mode 100644 index 00000000..7697725b --- /dev/null +++ b/src/database/glyphsdb.h @@ -0,0 +1,52 @@ +#ifndef GLYPHSDB_H +#define GLYPHSDB_H + +#include +#include +#include +#include +#include +#include + +class GlyphsDb + : public DbConnection + , QSqlDatabase +{ +public: + static QSharedPointer current(); + GlyphsDb(); + void open(); + Type type(); + /** + * @brief get Quran page QCF glyphs separated as lines + * @param page - Quran page number + * @return QList of page lines + */ + QStringList getPageLines(const int page); + /** + * @brief gets the surah name glyph for the QCF_BSML font, used to render + * surah frame in Quran page + * @param sura - sura number (1-114) + * @return QString of glyphs + */ + QString getSurahNameGlyph(const int sura); + /** + * @brief gets the juz name in arabic, used in page header + * @param juz - juz number + * @return QString of the juz name + */ + QString getJuzGlyph(const int juz); + /** + * @brief gets the verse QCF glyphs for the corresponding QCF page font + * @param sIdx - sura number (1-114) + * @param vIdx - verse number + * @return QString of verse glyphs + */ + QString getVerseGlyphs(const int sIdx, const int vIdx); + +private: + const int m_qcfVer = Settings::qcfVersion; + const QSharedPointer m_assetsDir = DirManager::assetsDir; +}; + +#endif // GLYPHSDB_H diff --git a/src/database/qurandb.cpp b/src/database/qurandb.cpp new file mode 100644 index 00000000..a507b4ed --- /dev/null +++ b/src/database/qurandb.cpp @@ -0,0 +1,380 @@ +#include "qurandb.h" +#include +#include + +QSharedPointer +QuranDb::current() +{ + static QSharedPointer quranDb = QSharedPointer::create(); + return quranDb; +} + +QuranDb::QuranDb() + : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "QuranCon")) +{ + QuranDb::open(); + for (int i = 1; i <= 114; i++) + m_surahNames.append(surahName(i)); +} + +void +QuranDb::open() +{ + setDatabaseName(m_assetsDir->absoluteFilePath("quran.db")); + if (!QSqlDatabase::open()) + qFatal("Error opening quran db"); +} + +DbConnection::Type +QuranDb::type() +{ + return DbConnection::Quran; +} + +QPair +QuranDb::pageMetadata(const int page) +{ + QSqlQuery dbQuery(*this); + dbQuery.prepare( + "SELECT sura_no,jozz FROM verses_v1 WHERE page=:p ORDER BY id"); + dbQuery.bindValue(0, page); + + if (!dbQuery.exec()) + qCritical() << "Error occurred during getPageMetadata SQL statment exec"; + + dbQuery.next(); + // { surahIdx, jozz } + return { dbQuery.value(0).toInt(), dbQuery.value(1).toInt() }; +} + +int +QuranDb::getVersePage(const int& surahIdx, const int& verse) +{ + QSqlQuery dbQuery(*this); + + QString query = "SELECT page FROM verses_v%0 WHERE sura_no=%1 AND aya_no=%2"; + dbQuery.prepare(query.arg(QString::number(m_qcfVer), + QString::number(surahIdx), + QString::number(verse))); + + if (!dbQuery.exec()) { + qCritical() << "Error occurred during getVersePage SQL statment exec"; + } + dbQuery.next(); + + return dbQuery.value(0).toInt(); +} + +int +QuranDb::getJuzStartPage(const int juz) +{ + QSqlQuery dbQuery(*this); + + QString query = + "SELECT page FROM verses_v1 WHERE jozz=" + QString::number(juz) + + " ORDER BY id"; + dbQuery.prepare(query); + + if (!dbQuery.exec()) { + qCritical() << "Error occurred during getJuzStartPage SQL statment exec"; + } + dbQuery.next(); + + return dbQuery.value(0).toInt(); +} + +int +QuranDb::getJuzOfPage(const int page) +{ + QSqlQuery dbQuery(*this); + + QString query = + "SELECT jozz FROM verses_v1 WHERE page=" + QString::number(page); + dbQuery.prepare(query); + + if (!dbQuery.exec()) { + qCritical() << "Error occurred during getJuzOfPage SQL statment exec"; + } + dbQuery.next(); + + return dbQuery.value(0).toInt(); +} + +QList> +QuranDb::verseInfoList(const int page) +{ + QList> viList; + QSqlQuery dbQuery(*this); + + QString query = + "SELECT sura_no,aya_no FROM verses_v%0 WHERE page=%1 ORDER BY id"; + dbQuery.prepare(query.arg(QString::number(m_qcfVer), QString::number(page))); + + if (!dbQuery.exec()) { + qCritical() << "Error occurred during getVerseInfoList SQL statment exec"; + } + + while (dbQuery.next()) { + QList v{ page, dbQuery.value(0).toInt(), dbQuery.value(1).toInt() }; + viList.append(v); + } + + return viList; +} + +QString +QuranDb::verseText(const int sIdx, const int vIdx) +{ + QSqlQuery dbQuery(*this); + if (m_verseType == VerseType::Annotated) + dbQuery.prepare("SELECT aya_text_annotated FROM verses_v1 WHERE sura_no=:s " + "AND aya_no=:v"); + else + dbQuery.prepare( + "SELECT aya_text FROM verses_v1 WHERE sura_no=:s AND aya_no=:v"); + + dbQuery.bindValue(0, sIdx); + dbQuery.bindValue(1, vIdx); + + if (!dbQuery.exec()) { + qCritical() << "Error occurred during getVerseText SQL statment exec"; + } + dbQuery.next(); + + return dbQuery.value(0).toString(); +} + +int +QuranDb::surahVerseCount(const int surahIdx) +{ + QSqlQuery dbQuery(*this); + + dbQuery.prepare( + "SELECT aya_no FROM verses_v1 WHERE sura_no=:idx ORDER BY aya_no DESC"); + dbQuery.bindValue(0, surahIdx); + + if (!dbQuery.exec()) { + qCritical() << "Error occurred during getSurahVerseCount SQL statment exec"; + return -1; + } + + dbQuery.next(); + return dbQuery.value(0).toInt(); +} + +int +QuranDb::surahStartPage(int surahIdx) +{ + QSqlQuery dbQuery(*this); + + dbQuery.prepare("SELECT page FROM verses_v1 WHERE sura_no=:sn AND aya_no=1"); + dbQuery.bindValue(0, surahIdx); + + if (!dbQuery.exec()) { + qCritical() << "Error occurred during getSurahStartPage SQL statment exec"; + } + dbQuery.next(); + + return dbQuery.value(0).toInt(); +} + +QString +QuranDb::surahName(const int sIdx, bool ar) +{ + QSqlQuery dbQuery(*this); + + if (m_languageCode == QLocale::Arabic || ar) + dbQuery.prepare("SELECT sura_name_ar FROM verses_v1 WHERE sura_no=:i"); + else + dbQuery.prepare("SELECT sura_name_en FROM verses_v1 WHERE sura_no=:i"); + + dbQuery.bindValue(0, sIdx); + if (!dbQuery.exec()) { + qCritical() << "Error occurred during getSurahName SQL statment exec"; + } + + dbQuery.next(); + return dbQuery.value(0).toString(); +} + +int +QuranDb::verseId(const int sIdx, const int vIdx) +{ + QSqlQuery dbQuery(*this); + dbQuery.prepare("SELECT id FROM verses_v1 WHERE sura_no=:s AND aya_no=:v"); + dbQuery.bindValue(0, sIdx); + dbQuery.bindValue(1, vIdx); + + if (!dbQuery.exec()) { + qCritical() << "Error occurred during getVerseId SQL statment exec"; + } + + dbQuery.next(); + return dbQuery.value(0).toInt(); +} + +QList +QuranDb::verseById(const int id) +{ + QSqlQuery dbQuery(*this); + dbQuery.prepare("SELECT page,sura_no,aya_no FROM verses_v1 WHERE id=:i"); + dbQuery.bindValue(0, id); + + if (!dbQuery.exec()) + qCritical() << "Error occurred during getVerseById SQL statement exec"; + + dbQuery.next(); + + return { dbQuery.value(0).toInt(), + dbQuery.value(1).toInt(), + dbQuery.value(2).toInt() }; +} + +int +QuranDb::versePage(const int& surahIdx, const int& verse) +{ + QSqlQuery dbQuery(*this); + + QString query = "SELECT page FROM verses_v%0 WHERE sura_no=%1 AND aya_no=%2"; + dbQuery.prepare(query.arg(QString::number(m_qcfVer), + QString::number(surahIdx), + QString::number(verse))); + + if (!dbQuery.exec()) { + qCritical() << "Error occurred during getVersePage SQL statment exec"; + } + dbQuery.next(); + + return dbQuery.value(0).toInt(); +} + +QList +QuranDb::searchSurahNames(QString text) +{ + QList results; + QSqlQuery dbQuery(*this); + QString q = + "SELECT DISTINCT sura_no FROM verses_v1 WHERE (sura_name_ar like '%" + + text + + "%' OR " + "sura_name_en like '%" + + text + "%')"; + + dbQuery.prepare(q); + if (!dbQuery.exec()) { + qCritical() << "Error occurred during searchSurahNames SQL statment exec"; + } + + while (dbQuery.next()) { + results.append(dbQuery.value(0).toInt()); + } + + return results; +} + +QList> +QuranDb::searchSurahs(QString searchText, + const QList surahs, + const bool whole) +{ + QList> results; + QSqlQuery dbQuery(*this); + + QString q = "SELECT page,sura_no,aya_no FROM verses_v" + + QString::number(m_qcfVer) + " WHERE ("; + for (int i = 0; i < surahs.size(); i++) { + q.append("sura_no=" + QString::number(surahs.at(i)) + ' '); + if (i != surahs.size() - 1) + q.append("OR "); + } + + if (whole) + q.append(") AND (aya_text_emlaey like '" + searchText + + " %' OR aya_text_emlaey like '% " + searchText + + " %') ORDER BY id"); + else + q.append(") AND (aya_text_emlaey like '%" + searchText + "%') ORDER BY id"); + + dbQuery.prepare(q); + if (!dbQuery.exec()) { + qCritical() << "Error occurred during searchSurahs SQL statment exec"; + } + + while (dbQuery.next()) { + results.append({ dbQuery.value(0).toInt(), + dbQuery.value(1).toInt(), + dbQuery.value(2).toInt() }); + } + + return results; +} + +QList> +QuranDb::searchVerses(QString searchText, const int range[], const bool whole) +{ + QList> results; + QSqlQuery dbQuery(*this); + + QString q = "SELECT page,sura_no,aya_no FROM verses_v" + + QString::number(m_qcfVer) + + " WHERE (page >= " + QString::number(range[0]) + + " AND page <= " + QString::number(range[1]) + ")"; + + if (whole) + q.append(" AND (aya_text_emlaey like '" + searchText + + " %' OR aya_text_emlaey like '% " + searchText + + " %') ORDER BY id"); + else + q.append(" AND (aya_text_emlaey like '%" + searchText + "%') ORDER BY id"); + + dbQuery.prepare(q); + if (!dbQuery.exec()) { + qCritical() << "Error occurred during searchVerses SQL statment exec"; + } + + while (dbQuery.next()) { + QList entry{ dbQuery.value(0).toInt(), + dbQuery.value(1).toInt(), + dbQuery.value(2).toInt() }; + results.append(entry); + } + + return results; +} + +QList +QuranDb::randomVerse() +{ + QSqlQuery dbQuery(*this); + + int id = QRandomGenerator::global()->bounded(1, 6237); + dbQuery.prepare("SELECT page,sura_no,aya_no FROM verses_v" + + QString::number(m_qcfVer) + + " WHERE id=" + QString::number(id)); + + if (!dbQuery.exec()) { + qCritical() << "Error occurred during randomVerse SQL statment exec"; + } + dbQuery.next(); + return { dbQuery.value(0).toInt(), + dbQuery.value(1).toInt(), + dbQuery.value(2).toInt() }; +} + +void +QuranDb::setVerseType(VerseType newVerseType) +{ + m_verseType = newVerseType; +} + +VerseType +QuranDb::verseType() const +{ + return m_verseType; +} + +QStringList +QuranDb::surahNames() const +{ + return m_surahNames; +} diff --git a/src/database/qurandb.h b/src/database/qurandb.h new file mode 100644 index 00000000..beeeb429 --- /dev/null +++ b/src/database/qurandb.h @@ -0,0 +1,160 @@ +#ifndef QURANDB_H +#define QURANDB_H + +#include +#include +#include +#include +#include +#include +typedef Settings::VerseType VerseType; + +class QuranDb + : public DbConnection + , QSqlDatabase +{ +public: + static QSharedPointer current(); + QuranDb(); + void open(); + Type type(); + /** + * @brief gets the surah number and juz number of the first verse in the page, + * used to display page header information + * @param page - Quran page number + * @return QList of 2 integers [0: surah index, 1: juz number] + */ + QPair pageMetadata(const int page); + /** + * @brief gets the page where the verse is found + * @param surahIdx - sura number + * @param verse - verse number + * @return page number + */ + int getVersePage(const int& surahIdx, const int& verse); + /** + * @brief gets the page where the corresponding juz starts + * @param juz - juz number + * @return page number + */ + int getJuzStartPage(const int juz); + /** + * @brief get the juz which the passed page is a part of + * @param page - page number + * @return juz number + */ + int getJuzOfPage(const int page); + /** + * @brief gets a QList of ::Verse instances for the page verses + * @param page - Quran page number + * @return QList of ::Verse instances + */ + QList> verseInfoList(const int page); + /** + * @brief gets the verse text + * @param sIdx - sura number (1-114) + * @param vIdx - verse number + * @return QString of the verse text + */ + QString verseText(const int sIdx, const int vIdx); + /** + * @brief gets the number of the last verse in the surah passed + * @param surahIdx - surah number (1-114) + * @return number of verses in the sura + */ + int surahVerseCount(const int surahIdx); + /** + * @brief gets the page where the surah begins + * @param surahIdx - sura number + * @return page of the first verse in the sura + */ + int surahStartPage(int surahIdx); + /** + * @brief gets the surah name in English or Arabic (default is English) + * @param sIdx - sura number + * @param ar - boolean to return arabic sura name + * @return QString containing the sura name + */ + QString surahName(const int sIdx, bool ar = false); + /** + * @brief gets the corresponding id for the verse in the database + * @param sIdx - sura number + * @param vIdx - verse number + * @return id of the verse + */ + int verseId(const int sIdx, const int vIdx); + /** + * @brief get the verse with the corresponding id and return it as a ::Verse + * instance + * @param id - verse id + * @return ::Verse instance + */ + QList verseById(const int id); + /** + * @brief gets the page where the verse is found + * @param surahIdx - sura number + * @param verse - verse number + * @return page number + */ + int versePage(const int& surahIdx, const int& verse); + /** + * @brief searches the database for surahs matching the given text pattern, + * the pattern can be either in English or Arabic + * @param text - name / part of the name of the sura + * @return QList of sura numbers which contain the given text + */ + QList searchSurahNames(QString text); + /** + * @brief search specific surahs for the given search text + * @param searchText - text to search for + * @param surahs - QList of surah numbers to search in + * @param whole - boolean value to search for whole words only + * @return QList of ::Verse instances representing the search results + */ + QList> searchSurahs(QString searchText, + const QList surahs, + const bool whole = false); + /** + * @brief search a range of pages for the given search text + * @param searchText - text to search for + * @param range - array of start & end page numbers + * @param whole - boolean value to indicate search for whole words only + * @return QList of ::Verse instances representing the search results + */ + QList> searchVerses(QString searchText, + const int range[2] = new int[2]{ 1, 604 }, + const bool whole = false); + /** + * @brief gets a random verse from the Quran + * @return QPair of ::Verse instance and verse text + */ + QList randomVerse(); + /** + * @brief setVerseType + * @param newVerseType + */ + void setVerseType(VerseType newVerseType); + /** + * @brief verseType + * @return + */ + VerseType verseType() const; + /** + * @brief surahNames + * @return + */ + QStringList surahNames() const; + +private: + const int m_qcfVer = Settings::qcfVersion; + const QLocale::Language m_languageCode = Settings::language; + const QSharedPointer m_assetsDir = DirManager::assetsDir; + VerseType m_verseType = Settings::Qcf; + /** + * @brief QList of sura names (Arabic if UI language is Arabic, Otherwise + * English) + */ + QStringList m_surahNames; +}; + +#endif // QURANDB_H diff --git a/src/database/tafsirdb.cpp b/src/database/tafsirdb.cpp new file mode 100644 index 00000000..2f808783 --- /dev/null +++ b/src/database/tafsirdb.cpp @@ -0,0 +1,79 @@ +#include "tafsirdb.h" +#include + +QSharedPointer +TafsirDb::current() +{ + static QSharedPointer tafsirDb = QSharedPointer::create(); + return tafsirDb; +} + +TafsirDb::TafsirDb() + : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "TafsirCon")) +{ + updateLoadedTafsir(); +} + +void +TafsirDb::open() +{ + setDatabaseName(m_tafsirFile.absoluteFilePath()); + if (!QSqlDatabase::open()) + qFatal("Error opening tafsir db"); +} + +DbConnection::Type +TafsirDb::type() +{ + return DbConnection::Tafsir; +} + +void +TafsirDb::updateLoadedTafsir() +{ + int curr = m_settings->value("Reader/Tafsir").toInt(); + setCurrentTafsir(curr); +} + +bool +TafsirDb::setCurrentTafsir(int idx) +{ + if (idx < 0 || idx >= m_tafasir.size()) + return false; + if (m_currTafsir == m_tafasir[idx]) + return true; + + m_currTafsir = m_tafasir[idx]; + const QDir& baseDir = + m_currTafsir->isExtra() ? *m_downloadsDir : *m_assetsDir; + QString path = "tafasir/" + m_currTafsir->filename(); + if (!baseDir.exists(path)) + return false; + + m_tafsirFile.setFile(baseDir.filePath(path)); + TafsirDb::open(); + return true; +} + +QString +TafsirDb::getTafsir(const int sIdx, const int vIdx) +{ + QSqlQuery dbQuery(*this); + + dbQuery.prepare("SELECT text FROM content WHERE sura=:s AND aya=:v"); + dbQuery.bindValue(0, sIdx); + dbQuery.bindValue(1, vIdx); + + if (!dbQuery.exec()) + qCritical("Couldn't execute getTafsir query!"); + + dbQuery.next(); + + return dbQuery.value(0).toString(); +} + +QSharedPointer +TafsirDb::currTafsir() const +{ + return m_currTafsir; +} diff --git a/src/database/tafsirdb.h b/src/database/tafsirdb.h new file mode 100644 index 00000000..c3ef29a0 --- /dev/null +++ b/src/database/tafsirdb.h @@ -0,0 +1,60 @@ +#ifndef TAFSIRDB_H +#define TAFSIRDB_H + +#include +#include +#include +#include +#include +#include +#include + +class TafsirDb + : public DbConnection + , QSqlDatabase + +{ +public: + static QSharedPointer current(); + TafsirDb(); + void open(); + Type type(); + /** + * @brief set tafsir to the one in the settings, update the selected db + */ + void updateLoadedTafsir(); + /** + * @brief sets the active tafsir + * @param tafsirName - DBManager::Tafsir entry + */ + bool setCurrentTafsir(int idx); + /** + * @brief gets the tafsir content for the given verse using the active + * DBManager::Tafsir + * @param sIdx - surah number + * @param vIdx - verse number + * @return QString containing the tafsir of the verse + */ + QString getTafsir(const int sIdx, const int vIdx); + /** + * @brief getter for m_currTafsir + * @return the currently set DBManager::Tafasir + */ + QSharedPointer<::Tafsir> currTafsir() const; + +private: + const QSharedPointer m_settings = Settings::settings; + const QSharedPointer m_assetsDir = DirManager::assetsDir; + const QSharedPointer m_downloadsDir = DirManager::downloadsDir; + const QList>& m_tafasir = Tafsir::tafasir; + /** + * @brief the current active DBManager::Tafasir + */ + QSharedPointer<::Tafsir> m_currTafsir; + /** + * @brief path to the currently active tafsir database file + */ + QFileInfo m_tafsirFile; +}; + +#endif // TAFSIRDB_H diff --git a/src/database/translationdb.cpp b/src/database/translationdb.cpp new file mode 100644 index 00000000..1a3b79c7 --- /dev/null +++ b/src/database/translationdb.cpp @@ -0,0 +1,78 @@ +#include "translationdb.h" +#include + +QSharedPointer +TranslationDb::current() +{ + static QSharedPointer translationDb = + QSharedPointer::create(); + return translationDb; +} + +TranslationDb::TranslationDb() + : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "TranslationCon")) +{ +} + +void +TranslationDb::open() +{ + setDatabaseName(m_translationFile.absoluteFilePath()); + if (!QSqlDatabase::open()) + qFatal("Error opening translation db"); +} + +DbConnection::Type +TranslationDb::type() +{ + return DbConnection::Translation; +} + +void +TranslationDb::updateLoadedTranslation() +{ + int curr = m_settings->value("Reader/Translation").toInt(); + setCurrentTranslation(curr); +} + +bool +TranslationDb::setCurrentTranslation(int idx) +{ + if (idx < 0 || idx >= m_translations.size()) + return false; + if (m_currTr == m_translations[idx]) + return true; + + m_currTr = m_translations[idx]; + const QDir& baseDir = m_currTr->isExtra() ? *m_downloadsDir : *m_assetsDir; + QString path = "translations/" + m_currTr->filename(); + if (!baseDir.exists(path)) + return false; + + m_translationFile.setFile(baseDir.filePath(path)); + TranslationDb::open(); + return true; +} + +QString +TranslationDb::getTranslation(const int sIdx, const int vIdx) +{ + QSqlQuery dbQuery(*this); + + dbQuery.prepare("SELECT text FROM content WHERE sura=:s AND aya=:v"); + dbQuery.bindValue(0, sIdx); + dbQuery.bindValue(1, vIdx); + + if (!dbQuery.exec()) + qCritical("Couldn't execute getTranslation query!"); + + dbQuery.next(); + + return dbQuery.value(0).toString(); +} + +QSharedPointer +TranslationDb::currTranslation() const +{ + return m_currTr; +} diff --git a/src/database/translationdb.h b/src/database/translationdb.h new file mode 100644 index 00000000..4a614fe2 --- /dev/null +++ b/src/database/translationdb.h @@ -0,0 +1,66 @@ +#ifndef TRANSLATIONDB_H +#define TRANSLATIONDB_H + +#include +#include +#include +#include +#include +#include +#include +#include + +class TranslationDb + : public DbConnection + , QSqlDatabase +{ + Q_OBJECT +public: + static QSharedPointer current(); + TranslationDb(); + void open(); + Type type(); + /** + * @brief sets the active translation + * @param translationName - DBManager::Translation entry + */ + bool setCurrentTranslation(int idx); + /** + * @brief gets the translation of the given verse using the active + * DBManager::Translation + * @param sIdx - surah number + * @param vIdx - verse number + * @return QString containing the verse translation + */ + QString getTranslation(const int sIdx, const int vIdx); + /** + * @brief currTranslation + * @return + * + * MODIFIED + */ + QSharedPointer<::Translation> currTranslation() const; + +public slots: + /** + * @brief set translation to the one in the settings, update the selected db + */ + void updateLoadedTranslation(); + +private: + const QSharedPointer m_assetsDir = DirManager::assetsDir; + const QSharedPointer m_downloadsDir = DirManager::downloadsDir; + const QSharedPointer m_settings = Settings::settings; + const QList>& m_translations = + Translation::translations; + /** + * @brief the current active DBManager::Translation + */ + QSharedPointer<::Translation> m_currTr; + /** + * @brief path to the currently active translation database file + */ + QFileInfo m_translationFile; +}; + +#endif // TRANSLATIONDB_H diff --git a/src/dialogs/aboutdialog.h b/src/dialogs/aboutdialog.h index 0fed0e77..1c66ed55 100644 --- a/src/dialogs/aboutdialog.h +++ b/src/dialogs/aboutdialog.h @@ -1,8 +1,8 @@ #ifndef ABOUTDIALOG_H #define ABOUTDIALOG_H -#include "utils/settings.h" #include +#include namespace Ui { class AboutDialog; diff --git a/src/dialogs/bookmarksdialog.cpp b/src/dialogs/bookmarksdialog.cpp index 5f429aea..fa03b048 100644 --- a/src/dialogs/bookmarksdialog.cpp +++ b/src/dialogs/bookmarksdialog.cpp @@ -5,9 +5,9 @@ #include "bookmarksdialog.h" #include "ui_bookmarksdialog.h" -#include "utils/fontmanager.h" -#include "utils/stylemanager.h" #include +#include +#include BookmarksDialog::BookmarksDialog(QWidget* parent) : QDialog(parent) @@ -82,7 +82,7 @@ BookmarksDialog::loadBookmarks(int surah) { if (m_shownSurah != surah) { m_shownSurah = surah; - m_shownVerses = Verse::fromList(m_dbMgr->bookmarkedVerses(surah)); + m_shownVerses = Verse::fromList(m_bookmarksDb->bookmarkedVerses(surah)); if (m_shownSurah == -1) m_allBookmarked = m_shownVerses; } @@ -107,7 +107,7 @@ BookmarksDialog::loadBookmarks(int surah) for (int i = m_startIdx; i < end; i++) { const Verse& verse = m_shownVerses.at(i); QString fontName = - FontManager::verseFontname(m_dbMgr->getVerseType(), verse.page()); + FontManager::verseFontname(m_quranDb->verseType(), verse.page()); QFrame* frame = new QFrame(ui->scrlBookmarks); frame->setProperty("bookmark", true); @@ -131,13 +131,18 @@ BookmarksDialog::loadBookmarks(int surah) removeFromFav, &QPushButton::clicked, this, &BookmarksDialog::btnRemove); QString info = tr("Surah: ") + - m_dbMgr->surahNameList().at(verse.surah() - 1) + " - " + + m_quranDb->surahNames().at(verse.surah() - 1) + " - " + tr("Verse: ") + QString::number(verse.number()); + QString glyphs = + m_quranDb->verseType() == Settings::Qcf + ? m_glpyhsDb->getVerseGlyphs(verse.surah(), verse.number()) + : m_quranDb->verseText(verse.surah(), verse.number()); + lbMeta->setText(info); lbMeta->setAlignment(Qt::AlignLeft); verseLb->setFont(QFont(fontName, 15)); - verseLb->setText(m_dbMgr->getVerseGlyphs(verse.surah(), verse.number())); + verseLb->setText(glyphs); verseLb->setAlignment(Qt::AlignLeft); verseLb->setWordWrap(true); verseLb->setMargin(5); @@ -182,7 +187,7 @@ BookmarksDialog::loadSurahs() } for (int s : surahs) { - item = new QStandardItem(m_dbMgr->surahNameList().at(s - 1)); + item = new QStandardItem(m_quranDb->surahNames().at(s - 1)); item->setData(Qt::AlignCenter, Qt::TextAlignmentRole); item->setToolTip(item->text()); item->setData(s, Qt::UserRole); @@ -222,7 +227,7 @@ BookmarksDialog::btnRemove() QStringList info = sender()->parent()->objectName().split('-'); Verse verse{ info.at(0).toInt(), info.at(1).toInt(), info.at(2).toInt() }; - if (m_dbMgr->removeBookmark(verse.toList())) { + if (m_bookmarksDb->removeBookmark(verse.toList())) { QFrame* frm = qobject_cast(sender()->parent()); int idx = m_frames.indexOf(frm); if (idx != -1) diff --git a/src/dialogs/bookmarksdialog.h b/src/dialogs/bookmarksdialog.h index c353cf77..a3168679 100644 --- a/src/dialogs/bookmarksdialog.h +++ b/src/dialogs/bookmarksdialog.h @@ -6,9 +6,6 @@ #ifndef BOOKMARKSDIALOG_H #define BOOKMARKSDIALOG_H -#include "types/verse.h" -#include "utils/dbmanager.h" -#include "utils/settings.h" #include #include #include @@ -16,6 +13,10 @@ #include #include #include +#include +#include +#include +#include namespace Ui { class BookmarksDialog; @@ -119,7 +120,9 @@ private slots: private: Ui::BookmarksDialog* ui; const int m_qcfVer = Settings::qcfVersion; - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_quranDb = QuranDb::current(); + QSharedPointer m_bookmarksDb = BookmarksDb::current(); + QSharedPointer m_glpyhsDb = GlyphsDb::current(); /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/dialogs/contentdialog.cpp b/src/dialogs/contentdialog.cpp index 369a7a37..bf02472d 100644 --- a/src/dialogs/contentdialog.cpp +++ b/src/dialogs/contentdialog.cpp @@ -4,10 +4,10 @@ */ #include "contentdialog.h" -#include "types/tafsir.h" #include "ui_contentdialog.h" -#include "utils/fontmanager.h" -#include "utils/stylemanager.h" +#include +#include +#include ContentDialog::ContentDialog(QWidget* parent) : QDialog(parent) @@ -54,12 +54,12 @@ ContentDialog::showVerseTafsir(const Verse& v) { static bool reload = false; if (reload) { - m_dbMgr->updateLoadedTafsir(); + m_tafsirDb->updateLoadedTafsir(); reload = false; } - if (!m_dbMgr->currTafsir()->isAvailable()) { - int i = m_tafasir.indexOf(m_dbMgr->currTafsir()); + if (!m_tafsirDb->currTafsir()->isAvailable()) { + int i = m_tafasir.indexOf(m_tafsirDb->currTafsir()); reload = true; emit missingTafsir(i); return; @@ -75,12 +75,12 @@ ContentDialog::showVerseTranslation(const Verse& v) { static bool reload = false; if (reload) { - m_dbMgr->updateLoadedTranslation(); + m_translationDb->updateLoadedTranslation(); reload = false; } - if (!m_dbMgr->currTranslation()->isAvailable()) { - int i = m_translations.indexOf(m_dbMgr->currTranslation()); + if (!m_translationDb->currTranslation()->isAvailable()) { + int i = m_translations.indexOf(m_translationDb->currTranslation()); reload = true; emit missingTranslation(i); return; @@ -111,14 +111,18 @@ void ContentDialog::setShownVerse(const Verse& newShownVerse) { m_shownVerse = newShownVerse; + if (m_shownVerse.number() == 0) + m_shownVerse.setNumber(1); - QString title = tr("Surah: ") + m_dbMgr->getSurahName(m_shownVerse.surah()) + + QString title = tr("Surah: ") + m_quranDb->surahName(m_shownVerse.surah()) + " - " + tr("Verse: ") + QString::number(m_shownVerse.number()); QString glyphs = - m_dbMgr->getVerseGlyphs(m_shownVerse.surah(), m_shownVerse.number()); + m_quranDb->verseType() == Settings::Qcf + ? m_glyphsDb->getVerseGlyphs(m_shownVerse.surah(), m_shownVerse.number()) + : m_quranDb->verseText(m_shownVerse.surah(), m_shownVerse.number()); QString fontFamily = - FontManager::verseFontname(m_dbMgr->getVerseType(), m_shownVerse.page()); + FontManager::verseFontname(m_quranDb->verseType(), m_shownVerse.page()); ui->lbVerseInfo->setText(title); ui->lbVerseText->setWordWrap(true); @@ -182,7 +186,7 @@ ContentDialog::tafsirChanged() { m_tafsir = ui->cmbContent->currentData().toInt(); m_settings->setValue("Reader/Tafsir", m_tafsir); - if (m_dbMgr->setCurrentTafsir(m_tafsir)) + if (m_tafsirDb->setCurrentTafsir(m_tafsir)) loadVerseTafsir(); } @@ -263,26 +267,26 @@ ContentDialog::cmbLoadTranslations() void ContentDialog::loadVerseTafsir() { - if (m_dbMgr->currTafsir()->isText()) + if (m_tafsirDb->currTafsir()->isText()) ui->tedContent->setText( - m_dbMgr->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); + m_tafsirDb->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); else ui->tedContent->setHtml( - m_dbMgr->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); + m_tafsirDb->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); } void ContentDialog::loadVerseTranslation() { - m_dbMgr->setCurrentTranslation(m_translation); - ui->tedContent->setText( - m_dbMgr->getTranslation(m_shownVerse.surah(), m_shownVerse.number())); + m_translationDb->setCurrentTranslation(m_translation); + ui->tedContent->setText(m_translationDb->getTranslation( + m_shownVerse.surah(), m_shownVerse.number())); } void ContentDialog::loadVerseThoughts() { - ui->tedContent->setText(m_dbMgr->getThoughts(m_shownVerse.toList())); + ui->tedContent->setText(m_bookmarkDb->getThoughts(m_shownVerse.toList())); ui->tedContent->setReadOnly(false); ui->tedContent->setCursorWidth(1); } @@ -292,7 +296,8 @@ ContentDialog::saveVerseThoughts() { ui->tedContent->setCursorWidth(0); ui->tedContent->setReadOnly(true); - m_dbMgr->saveThoughts(m_shownVerse.toList(), ui->tedContent->toPlainText()); + m_bookmarkDb->saveThoughts(m_shownVerse.toList(), + ui->tedContent->toPlainText()); } void diff --git a/src/dialogs/contentdialog.h b/src/dialogs/contentdialog.h index 59bd47dd..07d2daa7 100644 --- a/src/dialogs/contentdialog.h +++ b/src/dialogs/contentdialog.h @@ -6,11 +6,16 @@ #ifndef CONTENTDIALOG_H #define CONTENTDIALOG_H -#include "types/verse.h" -#include "utils/dbmanager.h" #include #include #include +#include +#include +#include +#include +#include +#include +#include namespace Ui { class ContentDialog; @@ -87,7 +92,11 @@ private slots: private: Ui::ContentDialog* ui; - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_quranDb = QuranDb::current(); + QSharedPointer m_glyphsDb = GlyphsDb::current(); + QSharedPointer m_bookmarkDb = BookmarksDb::current(); + QSharedPointer m_tafsirDb = TafsirDb::current(); + QSharedPointer m_translationDb = TranslationDb::current(); const int m_qcfVer = Settings::qcfVersion; const QSharedPointer m_settings = Settings::settings; const QList>& m_tafasir = Tafsir::tafasir; diff --git a/src/dialogs/copydialog.cpp b/src/dialogs/copydialog.cpp index c08f6618..c9960c37 100644 --- a/src/dialogs/copydialog.cpp +++ b/src/dialogs/copydialog.cpp @@ -17,13 +17,13 @@ void CopyDialog::copyVerseText(const Verse v) { QClipboard* clip = QApplication::clipboard(); - QString text = m_dbMgr->getVerseText(v.surah(), v.number()); + QString text = m_quranDb->verseText(v.surah(), v.number()); QString vNum = QString::number(v.number()); text.remove(text.size() - 1, 1); text = text.trimmed(); text = "{" + text + "}"; text += ' '; - text += "[" + m_dbMgr->surahNameList().at(v.surah() - 1) + ":" + vNum + "]"; + text += "[" + m_quranDb->surahNames().at(v.surah() - 1) + ":" + vNum + "]"; clip->setText(text); emit verseCopied(); } @@ -43,13 +43,13 @@ CopyDialog::copyRange() QString final = "{ "; QClipboard* clip = QApplication::clipboard(); for (int i = from; i <= to; i++) { - QString text = m_dbMgr->getVerseText(m_currVerse->surah(), i); + QString text = m_quranDb->verseText(m_currVerse->surah(), i); text.remove(text.size() - 1, 1); text += "(" + QString::number(i) + ") "; final.append(text); } - final += "} [" + m_dbMgr->surahNameList().at(m_currVerse->surah() - 1) + "]"; + final += "} [" + m_quranDb->surahNames().at(m_currVerse->surah() - 1) + "]"; clip->setText(final); emit rangeCopied(); } @@ -59,7 +59,7 @@ CopyDialog::show() { ui->cmbCopyFrom->clear(); ui->cmbCopyTo->clear(); - ui->lbCopySurahName->setText(m_dbMgr->getSurahName(m_currVerse->surah())); + ui->lbCopySurahName->setText(m_quranDb->surahName(m_currVerse->surah())); for (int i = 1; i <= m_currVerse->surahCount(); i++) { ui->cmbCopyFrom->addItem(QString::number(i)); diff --git a/src/dialogs/copydialog.h b/src/dialogs/copydialog.h index 2c6cc25d..be073789 100644 --- a/src/dialogs/copydialog.h +++ b/src/dialogs/copydialog.h @@ -1,13 +1,12 @@ #ifndef COPYDIALOG_H #define COPYDIALOG_H -#include "types/verse.h" -#include "utils/dbmanager.h" #include #include #include #include #include +#include namespace Ui { class CopyDialog; @@ -40,7 +39,7 @@ public slots: private: Ui::CopyDialog* ui; QSharedPointer m_currVerse = Verse::current(); - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_quranDb = QuranDb::current(); void copyRange(); QPointer m_verseValidator; diff --git a/src/dialogs/downloaderdialog.cpp b/src/dialogs/downloaderdialog.cpp index a66f7ce3..6a0c0d77 100644 --- a/src/dialogs/downloaderdialog.cpp +++ b/src/dialogs/downloaderdialog.cpp @@ -4,17 +4,17 @@ */ #include "downloaderdialog.h" -#include "downloader/contentjob.h" -#include "downloader/qcfjob.h" -#include "downloader/surahjob.h" #include "ui_downloaderdialog.h" -#include "utils/stylemanager.h" +#include +#include +#include +#include DownloaderDialog::DownloaderDialog(QWidget* parent, JobManager* manager) : QDialog(parent) , ui(new Ui::DownloaderDialog) , m_jobMgr(manager) - , m_surahDisplayNames(m_dbMgr->surahNameList()) + , m_surahDisplayNames(m_quranDb->surahNames()) { ui->setupUi(this); diff --git a/src/dialogs/downloaderdialog.h b/src/dialogs/downloaderdialog.h index 39dec4e0..65ebe429 100644 --- a/src/dialogs/downloaderdialog.h +++ b/src/dialogs/downloaderdialog.h @@ -6,10 +6,6 @@ #ifndef DOWNLOADERDIALOG_H #define DOWNLOADERDIALOG_H -#include "downloader/jobmanager.h" -#include "types/reciter.h" -#include "utils/dbmanager.h" -#include "widgets/downloadprogressbar.h" #include #include #include @@ -18,6 +14,13 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include namespace Ui { class DownloaderDialog; @@ -119,7 +122,7 @@ private slots: private: Ui::DownloaderDialog* ui; - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_quranDb = QuranDb::current(); const int m_languageCode = Settings::language; const QList>& m_reciters = Reciter::reciters; const QList>& m_tafasir = Tafsir::tafasir; diff --git a/src/dialogs/khatmahdialog.cpp b/src/dialogs/khatmahdialog.cpp index 6ba69f01..017aa3bb 100644 --- a/src/dialogs/khatmahdialog.cpp +++ b/src/dialogs/khatmahdialog.cpp @@ -1,8 +1,8 @@ #include "khatmahdialog.h" #include "ui_khatmahdialog.h" -#include "utils/settings.h" -#include "utils/stylemanager.h" #include +#include +#include KhatmahDialog::KhatmahDialog(QWidget* parent) : QDialog(parent) @@ -10,7 +10,8 @@ KhatmahDialog::KhatmahDialog(QWidget* parent) { ui->setupUi(this); setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_list)); - ui->lbCurrKhatmah->setText(m_dbMgr->getKhatmahName(m_dbMgr->activeKhatmah())); + ui->lbCurrKhatmah->setText( + m_bookmarksDb->getKhatmahName(m_bookmarksDb->activeKhatmah())); loadAll(); connect(ui->btnStartKhatmah, @@ -23,7 +24,7 @@ QPointer KhatmahDialog::loadKhatmah(const int id) { QList vInfo(3); - m_dbMgr->loadVerse(id, vInfo); + m_bookmarksDb->loadVerse(id, vInfo); QFrame* frame = new QFrame(ui->scrlDialogContent); // property used for styling frame->setProperty("khatmah", true); @@ -38,7 +39,7 @@ KhatmahDialog::loadKhatmah(const int id) QHBoxLayout* frmLayout = new QHBoxLayout(); QVBoxLayout* lbLayout = new QVBoxLayout(); QVBoxLayout* btnLayout = new QVBoxLayout(); - InputField* ifName = new InputField(frame, m_dbMgr->getKhatmahName(id)); + InputField* ifName = new InputField(frame, m_bookmarksDb->getKhatmahName(id)); QLabel* lbPosition = new QLabel(frame); QPushButton* activate = new QPushButton(tr("Set as active"), frame); QPushButton* remove = new QPushButton(tr("Remove"), frame); @@ -49,13 +50,13 @@ KhatmahDialog::loadKhatmah(const int id) activate->setStyleSheet( "QPushButton { min-width: 150px; max-width: 150px; }"); remove->setStyleSheet("QPushButton { min-width: 150px; max-width: 150px; }"); - if (id == m_dbMgr->activeKhatmah()) { + if (id == m_bookmarksDb->activeKhatmah()) { m_currActive = frame; activate->setDisabled(true); remove->setDisabled(true); } - QString info = tr("Surah: ") + m_dbMgr->surahNameList().at(vInfo[1] - 1) + + QString info = tr("Surah: ") + m_quranDb->surahNames().at(vInfo[1] - 1) + " - " + tr("Verse: ") + QString::number(vInfo[2]); lbPosition->setText(info); @@ -94,10 +95,10 @@ KhatmahDialog::loadKhatmah(const int id) void KhatmahDialog::loadAll() { - m_khatmahIds = m_dbMgr->getAllKhatmah(); + m_khatmahIds = m_bookmarksDb->getAllKhatmah(); for (int i = 0; i < m_khatmahIds.size(); i++) { m_names.insert(m_khatmahIds.at(i), - m_dbMgr->getKhatmahName(m_khatmahIds.at(i))); + m_bookmarksDb->getKhatmahName(m_khatmahIds.at(i))); loadKhatmah(m_khatmahIds.at(i)); } } @@ -105,9 +106,9 @@ KhatmahDialog::loadAll() void KhatmahDialog::startNewKhatmah() { - int id = m_dbMgr->addKhatmah(m_currVerse->toList(), "new"); + int id = m_bookmarksDb->addKhatmah(m_currVerse->toList(), "new"); QString gen = tr("Khatmah ") + QString::number(id); - m_dbMgr->editKhatmahName(id, gen); + m_bookmarksDb->editKhatmahName(id, gen); InputField* inpField = loadKhatmah(id); m_khatmahIds.append(id); m_names.insert(id, gen); @@ -119,14 +120,14 @@ KhatmahDialog::renameKhatmah(QString name) { InputField* caller = qobject_cast(sender()); int id = caller->parent()->objectName().toInt(); - bool ok = m_dbMgr->editKhatmahName(id, name); + bool ok = m_bookmarksDb->editKhatmahName(id, name); if (!ok) caller->setText(m_names.value(id)); else { m_names[id] = name; caller->setText(name); caller->clearFocus(); - if (id == m_dbMgr->activeKhatmah()) + if (id == m_bookmarksDb->activeKhatmah()) ui->lbCurrKhatmah->setText(name); } } @@ -135,7 +136,7 @@ void KhatmahDialog::removeKhatmah() { int id = sender()->parent()->objectName().toInt(); - m_dbMgr->removeKhatmah(id); + m_bookmarksDb->removeKhatmah(id); QFrame* rem = ui->scrlDialogContent->findChild(QString::number(id)); m_frmLst[m_frmLst.indexOf(rem)] = nullptr; delete rem; @@ -149,9 +150,9 @@ KhatmahDialog::setActiveKhatmah() QVariant id = newActive->objectName(); m_settings->setValue("Reader/Khatmah", id); - m_dbMgr->saveActiveKhatmah(m_currVerse->toList()); - m_dbMgr->setActiveKhatmah(id.toInt()); - m_dbMgr->loadVerse(id.toInt(), vInfo); + m_bookmarksDb->saveActiveKhatmah(m_currVerse->toList()); + m_bookmarksDb->setActiveKhatmah(id.toInt()); + m_bookmarksDb->loadVerse(id.toInt(), vInfo); newActive->findChild("activate")->setEnabled(false); newActive->findChild("remove")->setEnabled(false); @@ -159,7 +160,7 @@ KhatmahDialog::setActiveKhatmah() m_currActive->findChild("remove")->setEnabled(true); m_currActive = newActive; - ui->lbCurrKhatmah->setText(m_dbMgr->getKhatmahName(id.toInt())); + ui->lbCurrKhatmah->setText(m_bookmarksDb->getKhatmahName(id.toInt())); emit navigateToVerse(vInfo); } diff --git a/src/dialogs/khatmahdialog.h b/src/dialogs/khatmahdialog.h index 03b28b69..1417f49a 100644 --- a/src/dialogs/khatmahdialog.h +++ b/src/dialogs/khatmahdialog.h @@ -1,13 +1,13 @@ #ifndef KHATMAHDIALOG_H #define KHATMAHDIALOG_H -#include "types/verse.h" -#include "utils/dbmanager.h" -#include "widgets/inputfield.h" #include #include #include #include +#include +#include +#include namespace Ui { class KhatmahDialog; @@ -75,7 +75,8 @@ private slots: private: Ui::KhatmahDialog* ui; const QSharedPointer m_currVerse = Verse::current(); - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_quranDb = QuranDb::current(); + QSharedPointer m_bookmarksDb = BookmarksDb::current(); QSharedPointer m_settings = Settings::settings; /** * @brief load all khatmah entries available diff --git a/src/dialogs/searchdialog.cpp b/src/dialogs/searchdialog.cpp index f53d15b2..c282c0ca 100644 --- a/src/dialogs/searchdialog.cpp +++ b/src/dialogs/searchdialog.cpp @@ -5,14 +5,14 @@ #include "searchdialog.h" #include "ui_searchdialog.h" -#include "utils/fontmanager.h" -#include "utils/stylemanager.h" -#include "widgets/clickablelabel.h" +#include +#include +#include SearchDialog::SearchDialog(QWidget* parent) : QDialog(parent) , ui(new Ui::SearchDialog) - , m_surahNames{ m_dbMgr->surahNameList() } + , m_surahNames(m_quranDb->surahNames()) { setWindowIcon( StyleManager::awesome->icon(fa::fa_solid, fa::fa_magnifying_glass)); @@ -74,13 +74,13 @@ SearchDialog::getResults() ui->spnEndPage->setValue(range[0]); range[1] = ui->spnEndPage->value(); - m_currResults = Verse::fromList(m_dbMgr->searchVerses( + m_currResults = Verse::fromList(m_quranDb->searchVerses( m_searchText, range, ui->chkWholeWord->isChecked())); } else { m_currResults = - Verse::fromList(m_dbMgr->searchSurahs(m_searchText, - m_selectedSurahMap.values(), - ui->chkWholeWord->isChecked())); + Verse::fromList(m_quranDb->searchSurahs(m_searchText, + m_selectedSurahMap.values(), + ui->chkWholeWord->isChecked())); } ui->lbResultCount->setText(QString::number(m_currResults.size()) + tr(" Search results")); @@ -114,7 +114,7 @@ SearchDialog::showResults() for (int i = m_startResult; i < endIdx; i++) { Verse v = m_currResults.at(i); QString fontName = - FontManager::verseFontname(m_dbMgr->getVerseType(), v.page()); + FontManager::verseFontname(m_quranDb->verseType(), v.page()); VerseFrame* vFrame = new VerseFrame(ui->srclResults); QLabel* lbInfo = new QLabel(vFrame); @@ -122,6 +122,10 @@ SearchDialog::showResults() QString info = tr("Surah: ") + m_surahNames.at(v.surah() - 1) + " - " + tr("Verse: ") + QString::number(v.number()); + QString glyphs = m_quranDb->verseType() == Settings::Qcf + ? m_glyphsDb->getVerseGlyphs(v.surah(), v.number()) + : m_quranDb->verseText(v.surah(), v.number()); + lbInfo->setText(info); lbInfo->setMaximumHeight(50); lbInfo->setAlignment(Qt::AlignLeft); @@ -132,7 +136,7 @@ SearchDialog::showResults() QString::number(v.surah()) + '-' + QString::number(v.number())); clkLb->setFont(QFont(fontName, 15)); - clkLb->setText(m_dbMgr->getVerseGlyphs(v.surah(), v.number())); + clkLb->setText(glyphs); clkLb->setAlignment(Qt::AlignLeft); clkLb->setWordWrap(true); diff --git a/src/dialogs/searchdialog.h b/src/dialogs/searchdialog.h index 6061fffa..ed3d5a6a 100644 --- a/src/dialogs/searchdialog.h +++ b/src/dialogs/searchdialog.h @@ -6,9 +6,6 @@ #ifndef SEARCHDIALOG_H #define SEARCHDIALOG_H -#include "types/verse.h" -#include "utils/dbmanager.h" -#include "widgets/verseframe.h" #include #include #include @@ -17,6 +14,9 @@ #include #include #include +#include +#include +#include namespace Ui { class SearchDialog; @@ -98,7 +98,8 @@ private slots: private: Ui::SearchDialog* ui; const QLocale::Language m_lang = Settings::language; - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_quranDb = QuranDb::current(); + QSharedPointer m_glyphsDb = GlyphsDb::current(); /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/dialogs/settingsdialog.cpp b/src/dialogs/settingsdialog.cpp index e57e3443..d716bbf2 100644 --- a/src/dialogs/settingsdialog.cpp +++ b/src/dialogs/settingsdialog.cpp @@ -5,9 +5,9 @@ #include "settingsdialog.h" #include "ui_settingsdialog.h" -#include "utils/fontmanager.h" -#include "utils/stylemanager.h" -#include "widgets/shortcutdelegate.h" +#include +#include +#include SettingsDialog::SettingsDialog(QWidget* parent, VersePlayer* vPlayerPtr) : QDialog(parent) diff --git a/src/dialogs/settingsdialog.h b/src/dialogs/settingsdialog.h index 92c5cb8d..c4848480 100644 --- a/src/dialogs/settingsdialog.h +++ b/src/dialogs/settingsdialog.h @@ -6,9 +6,6 @@ #ifndef SETTINGSDIALOG_H #define SETTINGSDIALOG_H -#include "utils/settings.h" -#include "utils/shortcuthandler.h" -#include "utils/verseplayer.h" #include #include #include @@ -24,6 +21,11 @@ #include #include #include +#include +#include +#include +#include +#include typedef Settings::ReaderMode ReaderMode; namespace Ui { diff --git a/src/dialogs/versedialog.cpp b/src/dialogs/versedialog.cpp index 81a37066..f651c95d 100644 --- a/src/dialogs/versedialog.cpp +++ b/src/dialogs/versedialog.cpp @@ -47,7 +47,7 @@ VerseDialog::votdShown() void VerseDialog::genVerseOfTheDay() { - m_votd = m_dbMgr->randomVerse(); + m_votd = m_quranDb->randomVerse(); } void @@ -82,11 +82,11 @@ void VerseDialog::updateLabels() { ui->lbVerse->setText( - "ﵩ " + m_dbMgr->getVerseText(m_votd.surah(), m_votd.number()) + " ﵨ"); + "ﵩ " + m_quranDb->verseText(m_votd.surah(), m_votd.number()) + " ﵨ"); ui->lbContent->setText( - m_dbMgr->getTranslation(m_votd.surah(), m_votd.number())); + m_translationDb->getTranslation(m_votd.surah(), m_votd.number())); ui->lbInfo->setText(qApp->translate("BookmarksDialog", "Surah: ") + - m_dbMgr->getSurahName(m_votd.surah()) + " - " + + m_quranDb->surahName(m_votd.surah()) + " - " + qApp->translate("BookmarksDialog", "Verse: ") + QString::number(m_votd.number())); } diff --git a/src/dialogs/versedialog.h b/src/dialogs/versedialog.h index 46c7d1ab..7ed39791 100644 --- a/src/dialogs/versedialog.h +++ b/src/dialogs/versedialog.h @@ -1,11 +1,11 @@ #ifndef VERSEDIALOG_H #define VERSEDIALOG_H -#include "types/verse.h" -#include "utils/dbmanager.h" -#include "utils/dirmanager.h" #include #include +#include +#include +#include namespace Ui { class VerseDialog; @@ -35,7 +35,8 @@ public slots: private: Ui::VerseDialog* ui; - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_quranDb = QuranDb::current(); + QSharedPointer m_translationDb = TranslationDb::current(); const QSharedPointer m_settings = Settings::settings; QFile m_timestampFile = DirManager::configDir->absoluteFilePath("votd.log"); /** diff --git a/src/downloader/contentjob.cpp b/src/downloader/contentjob.cpp index 4dfe6564..89865c0d 100644 --- a/src/downloader/contentjob.cpp +++ b/src/downloader/contentjob.cpp @@ -1,6 +1,6 @@ #include "contentjob.h" -#include "downloader/tafsirtask.h" -#include "downloader/translationtask.h" +#include +#include ContentJob::ContentJob(Type type, int idx) : m_idx(idx) diff --git a/src/downloader/contentjob.h b/src/downloader/contentjob.h index 416bb740..7cd82873 100644 --- a/src/downloader/contentjob.h +++ b/src/downloader/contentjob.h @@ -1,10 +1,10 @@ #ifndef CONTENTJOB_H #define CONTENTJOB_H -#include "interfaces/downloadjob.h" #include "taskdownloader.h" -#include "types/tafsir.h" -#include "types/translation.h" +#include +#include +#include class ContentJob : public DownloadJob { diff --git a/src/downloader/jobmanager.h b/src/downloader/jobmanager.h index 81f1cfc3..ec550c7d 100644 --- a/src/downloader/jobmanager.h +++ b/src/downloader/jobmanager.h @@ -1,10 +1,10 @@ #ifndef JOBMANAGER_H #define JOBMANAGER_H -#include "interfaces/downloadjob.h" #include #include #include +#include class JobManager : public QObject { diff --git a/src/downloader/qcfjob.h b/src/downloader/qcfjob.h index ac3113f0..579cb192 100644 --- a/src/downloader/qcfjob.h +++ b/src/downloader/qcfjob.h @@ -1,9 +1,9 @@ #ifndef QCFJOB_H #define QCFJOB_H -#include "downloader/qcftask.h" -#include "interfaces/downloadjob.h" #include "taskdownloader.h" +#include +#include #include diff --git a/src/downloader/qcftask.h b/src/downloader/qcftask.h index 26861c9b..dfa138db 100644 --- a/src/downloader/qcftask.h +++ b/src/downloader/qcftask.h @@ -1,8 +1,8 @@ #ifndef QCFTASK_H #define QCFTASK_H -#include "interfaces/downloadtask.h" -#include "utils/dirmanager.h" +#include +#include class QcfTask : public DownloadTask { diff --git a/src/downloader/recitationtask.cpp b/src/downloader/recitationtask.cpp index 934925e9..c447971d 100644 --- a/src/downloader/recitationtask.cpp +++ b/src/downloader/recitationtask.cpp @@ -43,7 +43,7 @@ RecitationTask::url() const const Reciter& r = *m_reciters.at(m_reciter); QString url = r.baseUrl(); if (r.useId()) - url.append(QString::number(m_dbMgr->getVerseId(m_surah, m_verse)) + ".mp3"); + url.append(QString::number(m_quranDb->verseId(m_surah, m_verse)) + ".mp3"); else url.append(QString::number(m_surah).rightJustified(3, '0') + QString::number(m_verse).rightJustified(3, '0') + ".mp3"); diff --git a/src/downloader/recitationtask.h b/src/downloader/recitationtask.h index 88006a7f..153c86d0 100644 --- a/src/downloader/recitationtask.h +++ b/src/downloader/recitationtask.h @@ -1,10 +1,10 @@ #ifndef RECITATIONTASK_H #define RECITATIONTASK_H -#include "interfaces/downloadtask.h" -#include "types/reciter.h" -#include "utils/dbmanager.h" -#include "utils/dirmanager.h" +#include +#include +#include +#include class RecitationTask : public DownloadTask { @@ -21,7 +21,7 @@ class RecitationTask : public DownloadTask QFileInfo destination() const override; private: - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_quranDb = QuranDb::current(); QSharedPointer m_downloadsDir = DirManager::downloadsDir; const QList>& m_reciters = Reciter::reciters; int m_reciter; diff --git a/src/downloader/surahjob.cpp b/src/downloader/surahjob.cpp index bcfc647c..8d01e408 100644 --- a/src/downloader/surahjob.cpp +++ b/src/downloader/surahjob.cpp @@ -4,7 +4,7 @@ SurahJob::SurahJob(int reciter, int surah) : m_reciter(reciter) , m_surah(surah) , m_completed(0) - , m_surahCount(m_dbMgr->getSurahVerseCount(surah)) + , m_surahCount(m_quranDb->surahVerseCount(surah)) , m_isDownloading(false) , m_taskDlr(this) { @@ -118,7 +118,7 @@ QString SurahJob::name() { return m_reciters.at(m_reciter)->displayName() + " - " + - m_dbMgr->surahNameList().at(m_surah - 1); + m_quranDb->surahNames().at(m_surah - 1); } int diff --git a/src/downloader/surahjob.h b/src/downloader/surahjob.h index 941d8aa9..21ee5a98 100644 --- a/src/downloader/surahjob.h +++ b/src/downloader/surahjob.h @@ -1,11 +1,11 @@ #ifndef SURAHJOB_H #define SURAHJOB_H -#include "interfaces/downloadjob.h" #include "recitationtask.h" #include "taskdownloader.h" #include #include +#include class SurahJob : public DownloadJob { @@ -32,7 +32,7 @@ private slots: void taskFailed(); private: - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_quranDb = QuranDb::current(); QList>& m_reciters = Reciter::reciters; TaskDownloader m_taskDlr; QQueue m_queue; diff --git a/src/downloader/tafsirtask.h b/src/downloader/tafsirtask.h index 90b1e8dc..924c956e 100644 --- a/src/downloader/tafsirtask.h +++ b/src/downloader/tafsirtask.h @@ -1,9 +1,9 @@ #ifndef TAFSIRTASK_H #define TAFSIRTASK_H -#include "interfaces/downloadtask.h" -#include "types/tafsir.h" -#include "utils/dirmanager.h" +#include +#include +#include class TafsirTask : public DownloadTask { diff --git a/src/downloader/taskdownloader.h b/src/downloader/taskdownloader.h index ebee80ec..cb743f5b 100644 --- a/src/downloader/taskdownloader.h +++ b/src/downloader/taskdownloader.h @@ -1,10 +1,10 @@ #ifndef TASKDOWNLOADER_H #define TASKDOWNLOADER_H -#include "interfaces/downloadtask.h" #include #include #include +#include class TaskDownloader : public QObject { diff --git a/src/downloader/translationtask.h b/src/downloader/translationtask.h index e6244960..581cc3e8 100644 --- a/src/downloader/translationtask.h +++ b/src/downloader/translationtask.h @@ -1,9 +1,9 @@ #ifndef TRANSLATIONTASK_H #define TRANSLATIONTASK_H -#include "interfaces/downloadtask.h" -#include "types/translation.h" -#include "utils/dirmanager.h" +#include +#include +#include class TranslationTask : public DownloadTask { diff --git a/src/interfaces/dbconnection.h b/src/interfaces/dbconnection.h new file mode 100644 index 00000000..84258ac3 --- /dev/null +++ b/src/interfaces/dbconnection.h @@ -0,0 +1,28 @@ +#ifndef DBCONNECTION_H +#define DBCONNECTION_H + +#include +#include + +class DbConnection : public QObject +{ +public: + /** + * @brief Type enum holds different values representing database files + * used in different member functions. + */ + enum Type + { + Quran, ///< (quran.db) main Quran database file + Glyphs, ///< (glyphs.db) QCF glyphs database + Betaqat, + Bookmarks, ///< (bookmarks.db) bookmarked verses and khatmah database + Tafsir, ///< currently selected tafsir database file + Translation ///< currently selected translation database file + }; + + virtual void open() = 0; + virtual Type type() = 0; +}; + +#endif // DBCONNECTION_H diff --git a/src/main.cpp b/src/main.cpp index 80a30743..7021fd4d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,18 +3,18 @@ * @brief Application entry point. */ -#include "core/mainwindow.h" -#include "types/reciter.h" -#include "types/tafsir.h" -#include "types/translation.h" -#include "utils/dirmanager.h" -#include "utils/fontmanager.h" -#include "utils/logger.h" -#include "utils/settings.h" -#include "utils/shortcuthandler.h" -#include "utils/stylemanager.h" #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /** * @brief application entry point diff --git a/src/types/reciter.cpp b/src/types/reciter.cpp index 36a07894..e3a1e593 100644 --- a/src/types/reciter.cpp +++ b/src/types/reciter.cpp @@ -1,8 +1,8 @@ #include "reciter.h" -#include "utils/dirmanager.h" #include #include #include +#include QList> Reciter::reciters; diff --git a/src/types/tafsir.cpp b/src/types/tafsir.cpp index 64315730..be02ac1c 100644 --- a/src/types/tafsir.cpp +++ b/src/types/tafsir.cpp @@ -1,10 +1,10 @@ #include "tafsir.h" #include "content.h" -#include "utils/dirmanager.h" #include #include #include #include +#include QList> Tafsir::tafasir; diff --git a/src/types/translation.cpp b/src/types/translation.cpp index e14b151d..45238b0f 100644 --- a/src/types/translation.cpp +++ b/src/types/translation.cpp @@ -1,9 +1,9 @@ #include "translation.h" -#include "utils/dirmanager.h" #include #include #include #include +#include QList> Translation::translations; diff --git a/src/types/verse.cpp b/src/types/verse.cpp index 68a322bb..fa6fa0a0 100644 --- a/src/types/verse.cpp +++ b/src/types/verse.cpp @@ -107,7 +107,7 @@ Verse::next(bool basmalah) } QList vInfo = - m_dbMgr->getVerseById(m_dbMgr->getVerseId(m_surah, m_number) + 1); + m_quranDb->verseById(m_quranDb->verseId(m_surah, m_number) + 1); if (vInfo[2] == 1 && vInfo[1] != 9 && vInfo[1] != 1 && basmalah) vInfo[2] = 0; @@ -127,14 +127,14 @@ Verse::prev(bool basmalah) m_number = 1; QList vInfo = - m_dbMgr->getVerseById(m_dbMgr->getVerseId(m_surah, m_number) - 1); + m_quranDb->verseById(m_quranDb->verseId(m_surah, m_number) - 1); return Verse(vInfo); } void Verse::updateSurahCount() { - m_surahCount = m_dbMgr->getSurahVerseCount(m_surah); + m_surahCount = m_quranDb->surahVerseCount(m_surah); } void diff --git a/src/types/verse.h b/src/types/verse.h index f405f78a..d1cc6aad 100644 --- a/src/types/verse.h +++ b/src/types/verse.h @@ -1,9 +1,9 @@ #ifndef VERSE_H #define VERSE_H -#include "utils/dbmanager.h" -#include "utils/settings.h" #include +#include +#include /** * @brief Verse class represents a single quran verse @@ -45,7 +45,7 @@ class Verse private: const QSharedPointer m_settings = Settings::settings; - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_quranDb = QuranDb::current(); void updateSurahCount(); int m_page = -1; ///< verse page diff --git a/src/utils/dbmanager.cpp b/src/utils/dbmanager.cpp index 88a880e2..3bfe0f34 100644 --- a/src/utils/dbmanager.cpp +++ b/src/utils/dbmanager.cpp @@ -261,7 +261,7 @@ DBManager::getJuzGlyph(const int juz) QString DBManager::getVerseGlyphs(const int sIdx, const int vIdx) { - if (m_verseType != VerseType::qcf) + if (m_verseType != VerseType::Qcf) return getVerseText(sIdx, vIdx); setOpenDatabase(Database::Glyphs, m_glyphsDbPath.filePath()); @@ -589,7 +589,7 @@ DBManager::getVerseText(const int sIdx, const int vIdx) { setOpenDatabase(Database::Quran, m_quranDbPath.filePath()); QSqlQuery dbQuery(m_openDBCon); - if (m_verseType == VerseType::annotated) + if (m_verseType == VerseType::Annotated) dbQuery.prepare("SELECT aya_text_annotated FROM verses_v1 WHERE sura_no=:s " "AND aya_no=:v"); else diff --git a/src/utils/dbmanager.h b/src/utils/dbmanager.h index 062799b2..58a2b0c0 100644 --- a/src/utils/dbmanager.h +++ b/src/utils/dbmanager.h @@ -384,7 +384,7 @@ public slots: * * MODIFIED */ - VerseType m_verseType = Settings::qcf; + VerseType m_verseType = Settings::Qcf; /** * @brief the current active DBManager::Tafasir */ diff --git a/src/utils/fontmanager.cpp b/src/utils/fontmanager.cpp index 69228b88..9ded1977 100644 --- a/src/utils/fontmanager.cpp +++ b/src/utils/fontmanager.cpp @@ -61,13 +61,13 @@ FontManager::verseFontname(Settings::VerseType type, int page) { QString fontname; switch (type) { - case Settings::qcf: + case Settings::Qcf: fontname = pageFontname(page); break; - case Settings::uthmanic: + case Settings::Uthmanic: fontname = "kfgqpc_hafs_uthmanic _script"; break; - case Settings::annotated: + case Settings::Annotated: fontname = "Emine"; break; } diff --git a/src/utils/settings.cpp b/src/utils/settings.cpp index 43e2289c..0e5a8797 100644 --- a/src/utils/settings.cpp +++ b/src/utils/settings.cpp @@ -1,9 +1,9 @@ #include "settings.h" -#include "utils/dirmanager.h" #include #include #include #include +#include int Settings::themeId = 0; bool Settings::darkMode = false; diff --git a/src/utils/settings.h b/src/utils/settings.h index 644ee116..532cd506 100644 --- a/src/utils/settings.h +++ b/src/utils/settings.h @@ -10,9 +10,9 @@ class Settings public: enum VerseType { - qcf, - uthmanic, - annotated + Qcf, + Uthmanic, + Annotated }; /** * @brief ReaderMode enum represents the available modes for the Quran reader diff --git a/src/utils/shortcuthandler.cpp b/src/utils/shortcuthandler.cpp index 28189800..05b3fff0 100644 --- a/src/utils/shortcuthandler.cpp +++ b/src/utils/shortcuthandler.cpp @@ -4,10 +4,10 @@ */ #include "shortcuthandler.h" -#include "utils/settings.h" #include #include #include +#include QMap ShortcutHandler::shortcutsDescription; diff --git a/src/utils/systemtray.cpp b/src/utils/systemtray.cpp index 9d3d81b5..05868338 100644 --- a/src/utils/systemtray.cpp +++ b/src/utils/systemtray.cpp @@ -4,11 +4,12 @@ */ #include "systemtray.h" +#include SystemTray::SystemTray(QObject* parent) - : QObject{ parent } - , m_sysTray{ new QSystemTrayIcon(this) } - , m_trayMenu{ new QMenu() } + : QObject(parent) + , m_sysTray(new QSystemTrayIcon(this)) + , m_trayMenu(new QMenu()) { addActions(); setTooltip(qApp->translate("MainWindow", "Quran Companion")); diff --git a/src/utils/systemtray.h b/src/utils/systemtray.h index 4e30c1d5..903a8ee6 100644 --- a/src/utils/systemtray.h +++ b/src/utils/systemtray.h @@ -6,7 +6,6 @@ #ifndef SYSTEMTRAY_H #define SYSTEMTRAY_H -#include "dbmanager.h" #include #include #include @@ -84,7 +83,6 @@ class SystemTray : public QObject void openAbout(); private: - QSharedPointer m_dbMgr = DBManager::current(); /** * @brief adds system tray actions and set their connections */ diff --git a/src/utils/verseplayer.h b/src/utils/verseplayer.h index 3b2f9fe4..9a33a014 100644 --- a/src/utils/verseplayer.h +++ b/src/utils/verseplayer.h @@ -6,9 +6,6 @@ #ifndef VERSEPLAYER_H #define VERSEPLAYER_H -#include "dbmanager.h" -#include "types/reciter.h" -#include "types/verse.h" #include #include #include @@ -16,6 +13,8 @@ #include #include #include +#include +#include /*! * @brief VersePlayer class is responsible for the playback of Quran verse @@ -118,7 +117,6 @@ public slots: private: QSharedPointer m_activeVerse = Verse::current(); - QSharedPointer m_dbMgr = DBManager::current(); QDir m_reciterDir = DirManager::downloadsDir->absoluteFilePath("recitations"); const QList>& m_recitersList = Reciter::reciters; /** diff --git a/src/widgets/betaqaviewer.cpp b/src/widgets/betaqaviewer.cpp index a7d08c32..490f00d4 100644 --- a/src/widgets/betaqaviewer.cpp +++ b/src/widgets/betaqaviewer.cpp @@ -37,7 +37,7 @@ void BetaqaViewer::showSurah(int surah) { if (surah != m_surah) { - ui->betaqaTextBrowser->setHtml(m_dbMgr->getBetaqa(surah)); + ui->betaqaTextBrowser->setHtml(m_betaqatDb->getBetaqa(surah)); m_surah = surah; } diff --git a/src/widgets/betaqaviewer.h b/src/widgets/betaqaviewer.h index 885cdd29..3fd00052 100644 --- a/src/widgets/betaqaviewer.h +++ b/src/widgets/betaqaviewer.h @@ -1,12 +1,12 @@ #ifndef BETAQAVIEWER_H #define BETAQAVIEWER_H -#include "utils/dbmanager.h" #include #include #include #include #include +#include namespace Ui { class BetaqaViewer; @@ -30,7 +30,7 @@ public slots: private: Ui::BetaqaViewer* ui; - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_betaqatDb = BetaqatDb::current(); int m_surah = -1; QPointer m_shadowEffect; diff --git a/src/widgets/downloadprogressbar.h b/src/widgets/downloadprogressbar.h index 57272e0a..8e68871f 100644 --- a/src/widgets/downloadprogressbar.h +++ b/src/widgets/downloadprogressbar.h @@ -6,8 +6,8 @@ #ifndef DOWNLOADPROGRESSBAR_H #define DOWNLOADPROGRESSBAR_H -#include "interfaces/downloadjob.h" #include +#include typedef DownloadJob::Type Type; /** diff --git a/src/widgets/notificationpopup.cpp b/src/widgets/notificationpopup.cpp index d599773c..a89290b6 100644 --- a/src/widgets/notificationpopup.cpp +++ b/src/widgets/notificationpopup.cpp @@ -4,8 +4,8 @@ */ #include "notificationpopup.h" -#include "utils/stylemanager.h" #include +#include using namespace fa; NotificationPopup::NotificationPopup(QWidget* parent) @@ -81,14 +81,16 @@ NotificationPopup::notify(QString message, NotificationPopup::Action icon) m_notificationPeriod.start(); } -void NotificationPopup::completedDownload(QSharedPointer job) +void +NotificationPopup::completedDownload(QSharedPointer job) { setStyleSheet(""); QString msg = tr("Download Completed") + ": " + job->name(); this->notify(msg, success); } -void NotificationPopup::downloadError(QSharedPointer job) +void +NotificationPopup::downloadError(QSharedPointer job) { setStyleSheet("QFrame#Popup { background-color: #a50500 }"); QString msg = tr("Download Failed") + ": " + job->name(); diff --git a/src/widgets/notificationpopup.h b/src/widgets/notificationpopup.h index 4c64870a..9ec84421 100644 --- a/src/widgets/notificationpopup.h +++ b/src/widgets/notificationpopup.h @@ -6,9 +6,6 @@ #ifndef NOTIFICATIONPOPUP_H #define NOTIFICATIONPOPUP_H -#include "interfaces/downloadjob.h" -#include "types/reciter.h" -#include "utils/dbmanager.h" #include #include #include @@ -19,6 +16,10 @@ #include #include #include +#include +#include +#include +#include /** * @brief NotificationPopup class represents an in-app popup for notifying the @@ -106,7 +107,6 @@ public slots: void checkUpdate(QString appVer); private: - QSharedPointer m_dbMgr = DBManager::current(); QList>& m_recitersList = Reciter::reciters; QList>& m_tafasir = Tafsir::tafasir; QList>& m_translations = diff --git a/src/widgets/quranpagebrowser.cpp b/src/widgets/quranpagebrowser.cpp index 33c9ae49..6c75162b 100644 --- a/src/widgets/quranpagebrowser.cpp +++ b/src/widgets/quranpagebrowser.cpp @@ -4,11 +4,11 @@ */ #include "quranpagebrowser.h" -#include "utils/fontmanager.h" -#include "utils/stylemanager.h" #include #include #include +#include +#include using namespace fa; QuranPageBrowser::QuranPageBrowser(QWidget* parent, int initPage) @@ -101,7 +101,7 @@ QuranPageBrowser::surahFrame(int surah) QString frmText; frmText.append("ﰦ"); frmText.append("ﮌ"); - frmText.append(m_dbMgr->getSurahNameGlyph(surah)); + frmText.append(m_glyphsDb->getSurahNameGlyph(surah)); // draw on top of the image the surah name text QPainter p(&baseImage); @@ -133,14 +133,14 @@ QuranPageBrowser::setHref(QTextCursor* cursor, int to, QString url) QString QuranPageBrowser::pageHeader(int page) { - m_headerData = m_dbMgr->getPageMetadata(page); + m_headerData = m_quranDb->pageMetadata(page); QString suraHeader, jozzHeader; suraHeader.append("سورة "); - suraHeader.append(m_dbMgr->getSurahName(m_headerData.first, true)); + suraHeader.append(m_quranDb->surahName(m_headerData.first, true)); suraHeader.append("$"); jozzHeader.append("الجزء "); - jozzHeader.append(m_dbMgr->getJuzGlyph(m_headerData.second)); + jozzHeader.append(m_glyphsDb->getJuzGlyph(m_headerData.second)); return suraHeader + jozzHeader; } @@ -161,7 +161,7 @@ QuranPageBrowser::constructPage(int pageNo, bool forceCustomSize) QTextCursor textCursor(this->document()); m_currPageHeader = this->pageHeader(m_page); - m_currPageLines = m_dbMgr->getPageLines(m_page); + m_currPageLines = m_glyphsDb->getPageLines(m_page); // automatic font adjustment check if (!forceCustomSize && m_settings->value("Reader/AdaptiveFont").toBool()) { diff --git a/src/widgets/quranpagebrowser.h b/src/widgets/quranpagebrowser.h index e3c05bfb..17ef26b5 100644 --- a/src/widgets/quranpagebrowser.h +++ b/src/widgets/quranpagebrowser.h @@ -6,7 +6,6 @@ #ifndef QURANPAGEBROWSER_H #define QURANPAGEBROWSER_H -#include "utils/dbmanager.h" #include #include #include @@ -18,6 +17,9 @@ #include #include #include +#include +#include +#include /** * @brief QuranPageBrowser class is a modified QTextBrowser for displaying a @@ -144,7 +146,8 @@ public slots: private: const int m_qcfVer = Settings::qcfVersion; - QSharedPointer m_dbMgr = DBManager::current(); + QSharedPointer m_quranDb = QuranDb::current(); + QSharedPointer m_glyphsDb = GlyphsDb::current(); QSharedPointer const m_settings = Settings::settings; /** * @brief utility for creating menu actions for interacting with the widget From 582f3be5a0ddea277b443304a46f6f04bf950fa6 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:20:49 +0200 Subject: [PATCH 26/51] refactor: move surah verse count logic to verse class --- src/database/qurandb.cpp | 18 ------------------ src/database/qurandb.h | 8 +------- src/downloader/surahjob.cpp | 3 ++- src/types/verse.cpp | 27 ++++++++++++++++++++------- src/types/verse.h | 3 ++- 5 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/database/qurandb.cpp b/src/database/qurandb.cpp index a507b4ed..1d8efac9 100644 --- a/src/database/qurandb.cpp +++ b/src/database/qurandb.cpp @@ -144,24 +144,6 @@ QuranDb::verseText(const int sIdx, const int vIdx) return dbQuery.value(0).toString(); } -int -QuranDb::surahVerseCount(const int surahIdx) -{ - QSqlQuery dbQuery(*this); - - dbQuery.prepare( - "SELECT aya_no FROM verses_v1 WHERE sura_no=:idx ORDER BY aya_no DESC"); - dbQuery.bindValue(0, surahIdx); - - if (!dbQuery.exec()) { - qCritical() << "Error occurred during getSurahVerseCount SQL statment exec"; - return -1; - } - - dbQuery.next(); - return dbQuery.value(0).toInt(); -} - int QuranDb::surahStartPage(int surahIdx) { diff --git a/src/database/qurandb.h b/src/database/qurandb.h index beeeb429..2278837f 100644 --- a/src/database/qurandb.h +++ b/src/database/qurandb.h @@ -14,7 +14,7 @@ class QuranDb , QSqlDatabase { public: - static QSharedPointer current(); + static QSharedPointer current(); QuranDb(); void open(); Type type(); @@ -57,12 +57,6 @@ class QuranDb * @return QString of the verse text */ QString verseText(const int sIdx, const int vIdx); - /** - * @brief gets the number of the last verse in the surah passed - * @param surahIdx - surah number (1-114) - * @return number of verses in the sura - */ - int surahVerseCount(const int surahIdx); /** * @brief gets the page where the surah begins * @param surahIdx - sura number diff --git a/src/downloader/surahjob.cpp b/src/downloader/surahjob.cpp index 8d01e408..a0a54654 100644 --- a/src/downloader/surahjob.cpp +++ b/src/downloader/surahjob.cpp @@ -1,10 +1,11 @@ #include "surahjob.h" +#include SurahJob::SurahJob(int reciter, int surah) : m_reciter(reciter) , m_surah(surah) , m_completed(0) - , m_surahCount(m_quranDb->surahVerseCount(surah)) + , m_surahCount(Verse::surahVerseCount(surah)) , m_isDownloading(false) , m_taskDlr(this) { diff --git a/src/types/verse.cpp b/src/types/verse.cpp index fa6fa0a0..ebf7464d 100644 --- a/src/types/verse.cpp +++ b/src/types/verse.cpp @@ -1,5 +1,24 @@ #include "verse.h" +const QList Verse::verseCount = { + 7, 286, 200, 176, 120, 165, 206, 75, 129, 109, 123, 111, 43, 52, 99, + 128, 111, 110, 98, 135, 112, 78, 118, 64, 77, 227, 93, 88, 69, 60, + 34, 30, 73, 54, 45, 83, 182, 88, 75, 85, 54, 53, 89, 59, 37, + 35, 38, 29, 18, 45, 60, 49, 62, 55, 78, 96, 29, 22, 24, 13, + 14, 11, 11, 18, 12, 12, 30, 52, 52, 44, 28, 28, 20, 56, 40, + 31, 50, 40, 46, 42, 29, 19, 36, 25, 22, 17, 19, 26, 30, 20, + 15, 21, 11, 8, 8, 19, 5, 8, 8, 11, 11, 8, 3, 9, 5, + 4, 7, 3, 6, 3, 5, 4, 5, 6 +}; + +const int +Verse::surahVerseCount(int surah) +{ + if (surah > 114 || surah < 1) + return 0; + return verseCount.at(surah - 1); +} + QSharedPointer Verse::current() { @@ -131,12 +150,6 @@ Verse::prev(bool basmalah) return Verse(vInfo); } -void -Verse::updateSurahCount() -{ - m_surahCount = m_quranDb->surahVerseCount(m_surah); -} - void Verse::setPage(int newPage) { @@ -149,7 +162,7 @@ Verse::setSurah(int newSurah) if (m_surah == newSurah) return; m_surah = newSurah; - updateSurahCount(); + m_surahCount = verseCount.at(m_surah - 1); } void diff --git a/src/types/verse.h b/src/types/verse.h index d1cc6aad..ea409167 100644 --- a/src/types/verse.h +++ b/src/types/verse.h @@ -14,6 +14,8 @@ class Verse { public: + static const QList verseCount; + static const int surahVerseCount(int surah); static QSharedPointer current(); static QList fromList(QList> lst); @@ -46,7 +48,6 @@ class Verse private: const QSharedPointer m_settings = Settings::settings; QSharedPointer m_quranDb = QuranDb::current(); - void updateSurahCount(); int m_page = -1; ///< verse page int m_surah = -1; ///< verse surah number From 87eb5adc8228367509ef4d99f5e841f4cf8707ed Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:23:34 +0200 Subject: [PATCH 27/51] feat: add interfaces for exporting and importing user data --- src/interfaces/userdataexporter.h | 16 ++++++++++++++++ src/interfaces/userdataimporter.h | 15 +++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 src/interfaces/userdataexporter.h create mode 100644 src/interfaces/userdataimporter.h diff --git a/src/interfaces/userdataexporter.h b/src/interfaces/userdataexporter.h new file mode 100644 index 00000000..e458b7ea --- /dev/null +++ b/src/interfaces/userdataexporter.h @@ -0,0 +1,16 @@ +#ifndef USERDATAEXPORTER_H +#define USERDATAEXPORTER_H + +#include + +class UserDataExporter +{ +public: + virtual void setFile(QString path) = 0; + virtual void exportBookmarks() = 0; + virtual void exportKhatmah() = 0; + virtual void exportThoughts() = 0; + virtual bool save() = 0; +}; + +#endif // USERDATAEXPORTER_H diff --git a/src/interfaces/userdataimporter.h b/src/interfaces/userdataimporter.h new file mode 100644 index 00000000..afdef9ca --- /dev/null +++ b/src/interfaces/userdataimporter.h @@ -0,0 +1,15 @@ +#ifndef USERDATAIMPORTER_H +#define USERDATAIMPORTER_H + +#include + +class UserDataImporter +{ +public: + virtual void setFile(QString path) = 0; + virtual void importBookmarks() = 0; + virtual void importKhatmah() = 0; + virtual void importThoughts() = 0; +}; + +#endif // USERDATAIMPORTER_H From 376aa154858eb9bf73a112f2aa5f42c15794d71c Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:23:58 +0200 Subject: [PATCH 28/51] feat: add json implementation for data export interface --- CMakeLists.txt | 10 +++- src/database/bookmarksdb.cpp | 36 +++++++++--- src/database/bookmarksdb.h | 32 ++++++----- src/utils/jsondataexporter.cpp | 100 +++++++++++++++++++++++++++++++++ src/utils/jsondataexporter.h | 30 ++++++++++ 5 files changed, 183 insertions(+), 25 deletions(-) create mode 100644 src/utils/jsondataexporter.cpp create mode 100644 src/utils/jsondataexporter.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 453d10bb..69cf6cef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,6 +82,11 @@ set(PROJECT_SOURCES src/dialogs/versedialog.h src/dialogs/versedialog.cpp src/dialogs/versedialog.ui + src/interfaces/userdataimporter.h + src/interfaces/userdataexporter.h + src/interfaces/downloadjob.h + src/interfaces/downloadtask.h + src/interfaces/dbconnection.h src/utils/settings.h src/utils/settings.cpp src/utils/shortcuthandler.h @@ -100,9 +105,8 @@ set(PROJECT_SOURCES src/utils/fontmanager.cpp src/utils/versionchecker.h src/utils/versionchecker.cpp - src/interfaces/downloadjob.h - src/interfaces/downloadtask.h - src/interfaces/dbconnection.h + src/utils/jsondataexporter.h + src/utils/jsondataexporter.cpp src/downloader/surahjob.h src/downloader/surahjob.cpp src/downloader/recitationtask.h diff --git a/src/database/bookmarksdb.cpp b/src/database/bookmarksdb.cpp index bc6e296f..2254c0a1 100644 --- a/src/database/bookmarksdb.cpp +++ b/src/database/bookmarksdb.cpp @@ -51,7 +51,7 @@ BookmarksDb::saveActiveKhatmah(QList vInfo) } QList -BookmarksDb::getAllKhatmah() +BookmarksDb::getAllKhatmah() const { QList res; QSqlQuery dbQuery(*this); @@ -65,7 +65,7 @@ BookmarksDb::getAllKhatmah() } QString -BookmarksDb::getKhatmahName(const int id) +BookmarksDb::getKhatmahName(const int id) const { QSqlQuery dbQuery(*this); if (!dbQuery.exec("SELECT name FROM khatmah WHERE id=" + QString::number(id))) @@ -76,7 +76,7 @@ BookmarksDb::getKhatmahName(const int id) } bool -BookmarksDb::loadVerse(const int khatmahId, QList& vInfo) +BookmarksDb::loadVerse(const int khatmahId, QList& vInfo) const { QSqlQuery dbQuery(*this); @@ -96,7 +96,9 @@ BookmarksDb::loadVerse(const int khatmahId, QList& vInfo) } int -BookmarksDb::addKhatmah(QList vInfo, const QString name, const int id) +BookmarksDb::addKhatmah(QList vInfo, + const QString name, + const int id) const { QSqlQuery dbQuery(*this); dbQuery.exec( @@ -160,7 +162,7 @@ BookmarksDb::editKhatmahName(const int khatmahId, QString newName) } void -BookmarksDb::removeKhatmah(const int id) +BookmarksDb::removeKhatmah(const int id) const { QSqlQuery dbQuery(*this); if (!dbQuery.exec(QString::asprintf("DELETE FROM khatmah WHERE id=%i", id))) @@ -168,7 +170,7 @@ BookmarksDb::removeKhatmah(const int id) } QList> -BookmarksDb::bookmarkedVerses(int surahIdx) +BookmarksDb::bookmarkedVerses(int surahIdx) const { QList> results; QSqlQuery dbQuery(*this); @@ -190,7 +192,7 @@ BookmarksDb::bookmarkedVerses(int surahIdx) } bool -BookmarksDb::isBookmarked(QList vInfo) +BookmarksDb::isBookmarked(QList vInfo) const { QSqlQuery dbQuery(*this); @@ -277,7 +279,7 @@ BookmarksDb::saveThoughts(QList vInfo, const QString& text) } QString -BookmarksDb::getThoughts(QList vInfo) +BookmarksDb::getThoughts(QList vInfo) const { QSqlQuery dbQuery(*this); dbQuery.prepare( @@ -293,6 +295,24 @@ BookmarksDb::getThoughts(QList vInfo) return dbQuery.value(0).toString(); } +QList, QString>> +BookmarksDb::allThoughts() const +{ + QList, QString>> all; + QSqlQuery dbQuery(*this); + dbQuery.exec("SELECT page,surah,number,text FROM thoughts WHERE text!=''"); + while (dbQuery.next()) { + QList vInfo(3); + vInfo[0] = dbQuery.value(0).toInt(); + vInfo[1] = dbQuery.value(1).toInt(); + vInfo[2] = dbQuery.value(2).toInt(); + + all.append({ vInfo, dbQuery.value(3).toString() }); + } + + return all; +} + void BookmarksDb::setActiveKhatmah(const int id) { diff --git a/src/database/bookmarksdb.h b/src/database/bookmarksdb.h index bda22913..5ad270cf 100644 --- a/src/database/bookmarksdb.h +++ b/src/database/bookmarksdb.h @@ -30,19 +30,19 @@ class BookmarksDb * @brief get all available khatmah ids * @return QList of khatmah id(s) */ - QList getAllKhatmah(); + QList getAllKhatmah() const; /** * @brief get the name of the khatmah with id given * @return QString containing the khatmah name */ - QString getKhatmahName(const int id); + QString getKhatmahName(const int id) const; /** * @brief gets the last position saved for the khatmah with the id given and * stores the position in the ::Verse v * @return boolean indicating a successful operation (false in case of error * and in case id does not exist) */ - bool loadVerse(const int khatmahId, QList& vInfo); + bool loadVerse(const int khatmahId, QList& vInfo) const; /** * @brief add a new khatmah/replace khatmah with given id with position of * ::Verse v @@ -51,7 +51,7 @@ class BookmarksDb * @param id - id of khatmah to replace, -1 means do not replace (default: -1) * @return id of newly added khatmah or id parameter if defined */ - int addKhatmah(QList vInfo, const QString name, const int id = -1); + int addKhatmah(QList vInfo, const QString name, const int id = -1) const; /** * @brief rename the khatmah with the given id to newName * @param khatmahId - id of khatmah to rename @@ -64,20 +64,20 @@ class BookmarksDb * @brief remove the khatmah with the given id from database * @param id - id of khatmah to remove */ - void removeKhatmah(const int id); + void removeKhatmah(const int id) const; /** * @brief gets a QList of ::Verse instances representing the bookmarked verse * within the given sura (default gets all) * @param surahIdx - sura number (-1 returns all bookmarks) * @return QList of bookmarked verses */ - QList> bookmarkedVerses(int surahIdx = -1); + QList> bookmarkedVerses(int surahIdx = -1) const; /** * @brief checks whether the given ::Verse is bookmarked * @param vInfo - ::Verse instance to check * @return boolean */ - bool isBookmarked(QList vInfo); + bool isBookmarked(QList vInfo) const; /** * @brief add the given ::Verse to bookmarks * @param vInfo - ::Verse instance to add @@ -90,11 +90,6 @@ class BookmarksDb * @return boolean indicating successful removal */ bool removeBookmark(QList vInfo); - /** - * @brief setter for m_activeKhatmah - * @param id - id of the active khatmah - */ - void setActiveKhatmah(const int id); /** * MODIFIED */ @@ -102,8 +97,17 @@ class BookmarksDb /** * MODIFIED */ - QString getThoughts(QList vInfo); - + QString getThoughts(QList vInfo) const; + /** + * @brief allThoughts + * @return + */ + QList, QString>> allThoughts() const; + /** + * @brief setter for m_activeKhatmah + * @param id - id of the active khatmah + */ + void setActiveKhatmah(const int id); int activeKhatmah() const; signals: diff --git a/src/utils/jsondataexporter.cpp b/src/utils/jsondataexporter.cpp new file mode 100644 index 00000000..09e06a16 --- /dev/null +++ b/src/utils/jsondataexporter.cpp @@ -0,0 +1,100 @@ +#include "jsondataexporter.h" +#include + +JsonDataExporter::JsonDataExporter() {} + +void +JsonDataExporter::exportBookmarks() +{ + QJsonArray bookmarks; + QList> all = m_bookmarksDb->bookmarkedVerses(); + foreach (const QList& vInfo, all) { + bookmarks.append(verseJson(vInfo)); + } + + m_fileObj["bookmarks"] = bookmarks; +} + +void +JsonDataExporter::exportKhatmah() +{ + QJsonArray khatmah; + QList ids = m_bookmarksDb->getAllKhatmah(); + foreach (const int id, ids) { + QList vInfo(3); + m_bookmarksDb->loadVerse(id, vInfo); + QString name = m_bookmarksDb->getKhatmahName(id); + khatmah.append(khatmahJson({ name, vInfo })); + } + + m_fileObj["khatmah"] = khatmah; +} + +void +JsonDataExporter::exportThoughts() +{ + QJsonArray thoughts; + QList, QString>> all = m_bookmarksDb->allThoughts(); + for (const QPair, QString>& item : all) { + thoughts.append(thoughtJson(item)); + } + m_fileObj["thoughts"] = thoughts; +} + +QJsonObject +JsonDataExporter::verseJson(const Verse& v) +{ + QJsonObject obj; + obj["page"] = v.page(); + obj["surah"] = v.surah(); + obj["number"] = v.number(); + return obj; +} + +QJsonObject +JsonDataExporter::verseJson(const QList& vInfo) +{ + QJsonObject obj; + obj["page"] = vInfo[0]; + obj["surah"] = vInfo[1]; + obj["number"] = vInfo[2]; + return obj; +} + +QJsonObject +JsonDataExporter::khatmahJson(const QPair>& entry) +{ + QJsonObject obj; + obj["name"] = entry.first; + obj["verse"] = verseJson(entry.second); + return obj; +} + +QJsonObject +JsonDataExporter::thoughtJson(const QPair, QString>& entry) +{ + QJsonObject obj; + obj["verse"] = verseJson(entry.first); + obj["text"] = entry.second; + return obj; +} + +void +JsonDataExporter::setFile(QString path) +{ + m_file.setFile(path); +} + +bool +JsonDataExporter::save() +{ + QFile jsonFile(m_file.absoluteFilePath()); + if (!jsonFile.open(QIODevice::WriteOnly)) { + qWarning() << "Failed to open JSON file for writing"; + return false; + } + + jsonFile.write(QJsonDocument(m_fileObj).toJson()); + jsonFile.close(); + return true; +} diff --git a/src/utils/jsondataexporter.h b/src/utils/jsondataexporter.h new file mode 100644 index 00000000..37f8aa7e --- /dev/null +++ b/src/utils/jsondataexporter.h @@ -0,0 +1,30 @@ +#ifndef JSONDATAEXPORTER_H +#define JSONDATAEXPORTER_H + +#include +#include +#include +#include +#include + +class JsonDataExporter : public UserDataExporter +{ +public: + JsonDataExporter(); + void setFile(QString path); + void exportBookmarks(); + void exportKhatmah(); + void exportThoughts(); + bool save(); + +private: + QSharedPointer m_bookmarksDb = BookmarksDb::current(); + QJsonObject verseJson(const Verse& v); + QJsonObject verseJson(const QList& vInfo); + QJsonObject khatmahJson(const QPair>& entry); + QJsonObject thoughtJson(const QPair, QString>& entry); + QJsonObject m_fileObj; + QFileInfo m_file; +}; + +#endif // JSONDATAEXPORTER_H From cb13a386910580262e1a8fbae29f4243889d4444 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Mon, 26 Feb 2024 23:19:17 +0200 Subject: [PATCH 29/51] refactor: replace all qlist verse representation in bookmarks db with verse class --- src/core/mainwindow.cpp | 27 ++++--- src/core/mainwindow.h | 2 +- src/core/quranreader.cpp | 16 ++-- src/database/bookmarksdb.cpp | 123 +++++++++++++++--------------- src/database/bookmarksdb.h | 23 +++--- src/database/qurandb.cpp | 16 ---- src/database/qurandb.h | 7 -- src/dialogs/bookmarksdialog.cpp | 4 +- src/dialogs/contentdialog.cpp | 5 +- src/dialogs/contentdialog.h | 2 +- src/dialogs/khatmahdialog.cpp | 16 ++-- src/downloader/recitationtask.cpp | 4 +- src/interfaces/userdataexporter.h | 11 ++- src/interfaces/userdataimporter.h | 16 +++- src/types/verse.cpp | 25 +++--- src/types/verse.h | 1 + src/utils/jsondataexporter.cpp | 31 +++----- src/utils/jsondataexporter.h | 7 +- src/widgets/quranpagebrowser.cpp | 2 +- 19 files changed, 170 insertions(+), 168 deletions(-) diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 57a27712..73116f5e 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -65,14 +65,14 @@ void MainWindow::loadVerse() { int id = m_settings->value("Reader/Khatmah").toInt(); - QList vInfo = m_currVerse->toList(); - m_bookmarkDb->setActiveKhatmah(id); - if (!m_bookmarkDb->loadVerse(id, vInfo)) { - QString name = id ? tr("Khatmah ") + QString::number(id) : tr("Default"); - m_bookmarkDb->addKhatmah(vInfo, name, id); + Verse v = *m_currVerse; + m_bookmarksDb->setActiveKhatmah(id); + if (!m_bookmarksDb->loadVerse(id, v)) { + QString name = + id ? tr("Khatmah") + " " + QString::number(id) : tr("Default"); + m_bookmarksDb->addKhatmah(v, name, id); } - - m_currVerse->update(vInfo); + m_currVerse->update(v); } void @@ -317,11 +317,11 @@ MainWindow::setupConnections() &CopyDialog::verseCopied, m_popup, &NotificationPopup::copiedToClipboard); - connect(m_bookmarkDb.data(), + connect(m_bookmarksDb.data(), &BookmarksDb::bookmarkAdded, m_popup, &NotificationPopup::bookmarkAdded); - connect(m_bookmarkDb.data(), + connect(m_bookmarksDb.data(), &BookmarksDb::bookmarkRemoved, m_popup, &NotificationPopup::bookmarkRemoved); @@ -643,9 +643,8 @@ MainWindow::updateTrayTooltip(QMediaPlayer::PlaybackState state) void MainWindow::addCurrentToBookmarks() { - QList vInfo = m_currVerse->toList(); - if (!m_bookmarkDb->isBookmarked(vInfo)) - m_bookmarkDb->addBookmark(vInfo); + if (!m_bookmarksDb->isBookmarked(*m_currVerse)) + m_bookmarksDb->addBookmark(*m_currVerse, false); } void @@ -755,7 +754,7 @@ MainWindow::actionKhatmahTriggered() &QuranReader::navigateToVerse); } - m_bookmarkDb->saveActiveKhatmah(m_currVerse->toList()); + m_bookmarksDb->saveActiveKhatmah(*m_currVerse); m_khatmahDlg->show(); } @@ -895,7 +894,7 @@ MainWindow::saveReaderState() m_settings->setValue("Reciter", m_playerControls->currentReciter()); m_settings->sync(); - m_bookmarkDb->saveActiveKhatmah(m_currVerse->toList()); + m_bookmarksDb->saveActiveKhatmah(*m_currVerse); } void diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 299b1360..79ebac88 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -251,7 +251,7 @@ private slots: Ui::MainWindow* ui; QSharedPointer m_currVerse = Verse::current(); QSharedPointer m_quranDb = QuranDb::current(); - QSharedPointer m_bookmarkDb = BookmarksDb::current(); + QSharedPointer m_bookmarksDb = BookmarksDb::current(); QSharedPointer m_translationDb = TranslationDb::current(); QSharedPointer m_shortcutHandler = ShortcutHandler::current(); diff --git a/src/core/quranreader.cpp b/src/core/quranreader.cpp index 6316dbba..3ca6ac50 100644 --- a/src/core/quranreader.cpp +++ b/src/core/quranreader.cpp @@ -220,17 +220,17 @@ QuranReader::addSideContent() m_translationDb->setCurrentTranslation( m_settings->value("Reader/Translation").toInt()); for (int i = m_activeVList->size() - 1; i >= 0; i--) { - const Verse* vInfo = &(m_activeVList->at(i)); + const Verse* verse = &(m_activeVList->at(i)); verseContFrame = new VerseFrame(m_scrlVerseByVerse->widget()); verselb = new ClickableLabel(verseContFrame); contentLb = new QLabel(verseContFrame); glyphs = m_quranDb->verseType() == Settings::Qcf - ? m_glyphsDb->getVerseGlyphs(vInfo->surah(), vInfo->number()) - : m_quranDb->verseText(vInfo->surah(), vInfo->number()); + ? m_glyphsDb->getVerseGlyphs(verse->surah(), verse->number()) + : m_quranDb->verseText(verse->surah(), verse->number()); verseContFrame->setObjectName( - QString("%0_%1").arg(vInfo->surah()).arg(vInfo->number())); + QString("%0_%1").arg(verse->surah()).arg(verse->number())); verselb->setFont(m_versesFont); verselb->setText(glyphs); @@ -238,7 +238,7 @@ QuranReader::addSideContent() verselb->setWordWrap(true); currLbContent = - m_translationDb->getTranslation(vInfo->surah(), vInfo->number()); + m_translationDb->getTranslation(verse->surah(), verse->number()); if (currLbContent == prevLbContent) { currLbContent = '-'; @@ -411,7 +411,7 @@ QuranReader::verseAnchorClicked(const QUrl& hrefUrl) Verse v(m_vLists[browerIdx].at(idx)); QuranPageBrowser::Action chosenAction = - senderBrowser->lmbVerseMenu(m_bookmarksDb->isBookmarked(v.toList())); + senderBrowser->lmbVerseMenu(m_bookmarksDb->isBookmarked(v)); switch (chosenAction) { case QuranPageBrowser::Play: @@ -437,10 +437,10 @@ QuranReader::verseAnchorClicked(const QUrl& hrefUrl) emit copyVerseText(v); break; case QuranPageBrowser::AddBookmark: - m_bookmarksDb->addBookmark(v.toList()); + m_bookmarksDb->addBookmark(v, false); break; case QuranPageBrowser::RemoveBookmark: - m_bookmarksDb->removeBookmark(v.toList()); + m_bookmarksDb->removeBookmark(v, false); default: break; } diff --git a/src/database/bookmarksdb.cpp b/src/database/bookmarksdb.cpp index 2254c0a1..a5905d44 100644 --- a/src/database/bookmarksdb.cpp +++ b/src/database/bookmarksdb.cpp @@ -14,6 +14,18 @@ BookmarksDb::BookmarksDb() : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "BookmarksCon")) { BookmarksDb::open(); + QSqlQuery dbQuery(*this); + dbQuery.exec( + "CREATE TABLE IF NOT EXISTS khatmah(id INTEGER PRIMARY KEY " + "AUTOINCREMENT, name TEXT, page INTEGER, surah INTEGER, number INTEGER)"); + + dbQuery.exec("CREATE TABLE IF NOT EXISTS favorites(id INTEGER PRIMARY KEY " + "AUTOINCREMENT," + "page INTEGER, surah INTEGER, number INTEGER)"); + + dbQuery.exec("CREATE TABLE IF NOT EXISTS thoughts(id INTEGER PRIMARY KEY " + "UNIQUE," + "page INTEGER, surah INTEGER, number INTEGER, text TEXT)"); } void @@ -31,14 +43,14 @@ BookmarksDb::type() } bool -BookmarksDb::saveActiveKhatmah(QList vInfo) +BookmarksDb::saveActiveKhatmah(const Verse& verse) { QSqlQuery dbQuery(*this); QString q = QString::asprintf( "UPDATE khatmah SET page=%i, surah=%i, number=%i WHERE id=%i", - vInfo[0], - vInfo[1], - vInfo[2], + verse.page(), + verse.surah(), + verse.number(), m_activeKhatmah); if (!dbQuery.exec(q)) { qCritical() << "Couldn't save position in mushaf"; @@ -76,7 +88,7 @@ BookmarksDb::getKhatmahName(const int id) const } bool -BookmarksDb::loadVerse(const int khatmahId, QList& vInfo) const +BookmarksDb::loadVerse(const int khatmahId, Verse& verse) const { QSqlQuery dbQuery(*this); @@ -89,38 +101,35 @@ BookmarksDb::loadVerse(const int khatmahId, QList& vInfo) const if (!dbQuery.next()) return false; - vInfo[0] = dbQuery.value(0).toInt(); - vInfo[1] = dbQuery.value(1).toInt(); - vInfo[2] = dbQuery.value(2).toInt(); + verse.setPage(dbQuery.value(0).toInt()); + verse.setSurah(dbQuery.value(1).toInt()); + verse.setNumber(dbQuery.value(2).toInt()); return true; } int -BookmarksDb::addKhatmah(QList vInfo, +BookmarksDb::addKhatmah(const Verse& verse, const QString name, const int id) const { QSqlQuery dbQuery(*this); - dbQuery.exec( - "CREATE TABLE IF NOT EXISTS khatmah(id INTEGER PRIMARY KEY " - "AUTOINCREMENT, name TEXT, page INTEGER, surah INTEGER, number INTEGER)"); QString q; if (id == -1) { q = "INSERT INTO khatmah(name, page, surah, number) VALUES ('%0', %1, %2, " "%3)"; dbQuery.prepare(q.arg(name, - QString::number(vInfo[0]), - QString::number(vInfo[1]), - QString::number(vInfo[2]))); + QString::number(verse.page()), + QString::number(verse.surah()), + QString::number(verse.number()))); } else { q = "REPLACE INTO khatmah VALUES " "(%0, " "'%1', %2, %3, %4)"; dbQuery.prepare(q.arg(QString::number(id), name, - QString::number(vInfo[0]), - QString::number(vInfo[1]), - QString::number(vInfo[2]))); + QString::number(verse.page()), + QString::number(verse.surah()), + QString::number(verse.number()))); } if (!dbQuery.exec()) { @@ -169,10 +178,10 @@ BookmarksDb::removeKhatmah(const int id) const qDebug() << "Couldn't execute query: " << dbQuery.lastQuery(); } -QList> +QList BookmarksDb::bookmarkedVerses(int surahIdx) const { - QList> results; + QList results; QSqlQuery dbQuery(*this); QString q = "SELECT page,surah,number FROM favorites"; if (surahIdx != -1) @@ -183,24 +192,24 @@ BookmarksDb::bookmarkedVerses(int surahIdx) const qCritical() << "Couldn't execute bookmarkedVerses SELECT query"; while (dbQuery.next()) { - results.append({ dbQuery.value(0).toInt(), - dbQuery.value(1).toInt(), - dbQuery.value(2).toInt() }); + results.append(Verse(dbQuery.value(0).toInt(), + dbQuery.value(1).toInt(), + dbQuery.value(2).toInt())); } return results; } bool -BookmarksDb::isBookmarked(QList vInfo) const +BookmarksDb::isBookmarked(const Verse& verse) const { QSqlQuery dbQuery(*this); dbQuery.prepare( "SELECT page FROM favorites WHERE page=:p AND surah=:s AND number=:n"); - dbQuery.bindValue(0, vInfo[0]); - dbQuery.bindValue(1, vInfo[1]); - dbQuery.bindValue(2, vInfo[2]); + dbQuery.bindValue(0, verse.page()); + dbQuery.bindValue(1, verse.surah()); + dbQuery.bindValue(2, verse.number()); if (!dbQuery.exec()) { qWarning() << "Couldn't check if verse is bookmarked"; @@ -213,18 +222,15 @@ BookmarksDb::isBookmarked(QList vInfo) const } bool -BookmarksDb::addBookmark(QList vInfo) +BookmarksDb::addBookmark(const Verse& verse, bool silent) { QSqlQuery dbQuery(*this); - dbQuery.exec("CREATE TABLE IF NOT EXISTS favorites(id INTEGER PRIMARY KEY " - "AUTOINCREMENT," - "page INTEGER, surah INTEGER, number INTEGER)"); dbQuery.prepare( "INSERT INTO favorites(page, surah, number) VALUES (:p, :s, :n)"); - dbQuery.bindValue(0, vInfo[0]); - dbQuery.bindValue(1, vInfo[1]); - dbQuery.bindValue(2, vInfo[2]); + dbQuery.bindValue(0, verse.page()); + dbQuery.bindValue(1, verse.surah()); + dbQuery.bindValue(2, verse.number()); if (!dbQuery.exec()) { qWarning() << "Couldn't add verse to bookmarks db"; @@ -232,44 +238,42 @@ BookmarksDb::addBookmark(QList vInfo) } commit(); - emit bookmarkAdded(); + if (!silent) + emit bookmarkAdded(); return true; } bool -BookmarksDb::removeBookmark(QList vInfo) +BookmarksDb::removeBookmark(const Verse& verse, bool silent) { QSqlQuery dbQuery(*this); dbQuery.prepare( "DELETE FROM favorites WHERE page=:p AND surah=:s AND number=:n"); - dbQuery.bindValue(0, vInfo[0]); - dbQuery.bindValue(1, vInfo[1]); - dbQuery.bindValue(2, vInfo[2]); + dbQuery.bindValue(0, verse.page()); + dbQuery.bindValue(1, verse.surah()); + dbQuery.bindValue(2, verse.number()); if (!dbQuery.exec()) { qWarning() << "Couldn't remove verse from bookmarks"; return false; } - emit bookmarkRemoved(); + if (!silent) + emit bookmarkRemoved(); return true; } void -BookmarksDb::saveThoughts(QList vInfo, const QString& text) +BookmarksDb::saveThoughts(Verse& verse, const QString& text) { - int id = m_quranDb->verseId(vInfo[1], vInfo[2]); + int id = Verse::id(verse.surah(), verse.number()); QSqlQuery dbQuery(*this); - dbQuery.exec("CREATE TABLE IF NOT EXISTS thoughts(id INTEGER PRIMARY KEY " - "UNIQUE," - "page INTEGER, surah INTEGER, number INTEGER, text TEXT)"); - dbQuery.prepare("REPLACE INTO thoughts(id, page, surah, number, text) " "VALUES(:i, :p, :s, :n, :t)"); dbQuery.bindValue(0, id); - dbQuery.bindValue(1, vInfo[0]); - dbQuery.bindValue(2, vInfo[1]); - dbQuery.bindValue(3, vInfo[2]); + dbQuery.bindValue(1, verse.page()); + dbQuery.bindValue(2, verse.surah()); + dbQuery.bindValue(3, verse.number()); dbQuery.bindValue(4, text); if (!dbQuery.exec()) @@ -279,14 +283,14 @@ BookmarksDb::saveThoughts(QList vInfo, const QString& text) } QString -BookmarksDb::getThoughts(QList vInfo) const +BookmarksDb::getThoughts(const Verse& verse) const { QSqlQuery dbQuery(*this); dbQuery.prepare( "SELECT text FROM thoughts WHERE page=:p AND surah=:s AND number=:n"); - dbQuery.bindValue(0, vInfo[0]); - dbQuery.bindValue(1, vInfo[1]); - dbQuery.bindValue(2, vInfo[2]); + dbQuery.bindValue(0, verse.page()); + dbQuery.bindValue(1, verse.surah()); + dbQuery.bindValue(2, verse.number()); if (!dbQuery.exec()) qCritical() << "SQL statement execution error:" << dbQuery.lastError(); @@ -295,19 +299,18 @@ BookmarksDb::getThoughts(QList vInfo) const return dbQuery.value(0).toString(); } -QList, QString>> +QList> BookmarksDb::allThoughts() const { - QList, QString>> all; + QList> all; QSqlQuery dbQuery(*this); dbQuery.exec("SELECT page,surah,number,text FROM thoughts WHERE text!=''"); while (dbQuery.next()) { - QList vInfo(3); - vInfo[0] = dbQuery.value(0).toInt(); - vInfo[1] = dbQuery.value(1).toInt(); - vInfo[2] = dbQuery.value(2).toInt(); + const Verse verse(dbQuery.value(0).toInt(), + dbQuery.value(1).toInt(), + dbQuery.value(2).toInt()); - all.append({ vInfo, dbQuery.value(3).toString() }); + all.append({ verse, dbQuery.value(3).toString() }); } return all; diff --git a/src/database/bookmarksdb.h b/src/database/bookmarksdb.h index 5ad270cf..6e0444cf 100644 --- a/src/database/bookmarksdb.h +++ b/src/database/bookmarksdb.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -25,7 +26,7 @@ class BookmarksDb * active khatmah * @param v - ::Verse reached in khatmah */ - bool saveActiveKhatmah(QList vInfo); + bool saveActiveKhatmah(const Verse& verse); /** * @brief get all available khatmah ids * @return QList of khatmah id(s) @@ -42,7 +43,7 @@ class BookmarksDb * @return boolean indicating a successful operation (false in case of error * and in case id does not exist) */ - bool loadVerse(const int khatmahId, QList& vInfo) const; + bool loadVerse(const int khatmahId, Verse& verse) const; /** * @brief add a new khatmah/replace khatmah with given id with position of * ::Verse v @@ -51,7 +52,9 @@ class BookmarksDb * @param id - id of khatmah to replace, -1 means do not replace (default: -1) * @return id of newly added khatmah or id parameter if defined */ - int addKhatmah(QList vInfo, const QString name, const int id = -1) const; + int addKhatmah(const Verse& verse, + const QString name, + const int id = -1) const; /** * @brief rename the khatmah with the given id to newName * @param khatmahId - id of khatmah to rename @@ -71,38 +74,38 @@ class BookmarksDb * @param surahIdx - sura number (-1 returns all bookmarks) * @return QList of bookmarked verses */ - QList> bookmarkedVerses(int surahIdx = -1) const; + QList bookmarkedVerses(int surahIdx = -1) const; /** * @brief checks whether the given ::Verse is bookmarked * @param vInfo - ::Verse instance to check * @return boolean */ - bool isBookmarked(QList vInfo) const; + bool isBookmarked(const Verse& verse) const; /** * @brief add the given ::Verse to bookmarks * @param vInfo - ::Verse instance to add * @return boolean */ - bool addBookmark(QList vInfo); + bool addBookmark(const Verse& verse, bool silent); /** * @brief remove the given ::Verse from bookmarks * @param vInfo - ::Verse instance to remove * @return boolean indicating successful removal */ - bool removeBookmark(QList vInfo); + bool removeBookmark(const Verse& verse, bool silent); /** * MODIFIED */ - void saveThoughts(QList vInfo, const QString& text); + void saveThoughts(Verse& verse, const QString& text); /** * MODIFIED */ - QString getThoughts(QList vInfo) const; + QString getThoughts(const Verse& verse) const; /** * @brief allThoughts * @return */ - QList, QString>> allThoughts() const; + QList> allThoughts() const; /** * @brief setter for m_activeKhatmah * @param id - id of the active khatmah diff --git a/src/database/qurandb.cpp b/src/database/qurandb.cpp index 1d8efac9..b71dfab3 100644 --- a/src/database/qurandb.cpp +++ b/src/database/qurandb.cpp @@ -179,22 +179,6 @@ QuranDb::surahName(const int sIdx, bool ar) return dbQuery.value(0).toString(); } -int -QuranDb::verseId(const int sIdx, const int vIdx) -{ - QSqlQuery dbQuery(*this); - dbQuery.prepare("SELECT id FROM verses_v1 WHERE sura_no=:s AND aya_no=:v"); - dbQuery.bindValue(0, sIdx); - dbQuery.bindValue(1, vIdx); - - if (!dbQuery.exec()) { - qCritical() << "Error occurred during getVerseId SQL statment exec"; - } - - dbQuery.next(); - return dbQuery.value(0).toInt(); -} - QList QuranDb::verseById(const int id) { diff --git a/src/database/qurandb.h b/src/database/qurandb.h index 2278837f..12dce5a5 100644 --- a/src/database/qurandb.h +++ b/src/database/qurandb.h @@ -70,13 +70,6 @@ class QuranDb * @return QString containing the sura name */ QString surahName(const int sIdx, bool ar = false); - /** - * @brief gets the corresponding id for the verse in the database - * @param sIdx - sura number - * @param vIdx - verse number - * @return id of the verse - */ - int verseId(const int sIdx, const int vIdx); /** * @brief get the verse with the corresponding id and return it as a ::Verse * instance diff --git a/src/dialogs/bookmarksdialog.cpp b/src/dialogs/bookmarksdialog.cpp index fa03b048..e209bf0e 100644 --- a/src/dialogs/bookmarksdialog.cpp +++ b/src/dialogs/bookmarksdialog.cpp @@ -82,7 +82,7 @@ BookmarksDialog::loadBookmarks(int surah) { if (m_shownSurah != surah) { m_shownSurah = surah; - m_shownVerses = Verse::fromList(m_bookmarksDb->bookmarkedVerses(surah)); + m_shownVerses = m_bookmarksDb->bookmarkedVerses(surah); if (m_shownSurah == -1) m_allBookmarked = m_shownVerses; } @@ -227,7 +227,7 @@ BookmarksDialog::btnRemove() QStringList info = sender()->parent()->objectName().split('-'); Verse verse{ info.at(0).toInt(), info.at(1).toInt(), info.at(2).toInt() }; - if (m_bookmarksDb->removeBookmark(verse.toList())) { + if (m_bookmarksDb->removeBookmark(verse, true)) { QFrame* frm = qobject_cast(sender()->parent()); int idx = m_frames.indexOf(frm); if (idx != -1) diff --git a/src/dialogs/contentdialog.cpp b/src/dialogs/contentdialog.cpp index bf02472d..c87e9967 100644 --- a/src/dialogs/contentdialog.cpp +++ b/src/dialogs/contentdialog.cpp @@ -286,7 +286,7 @@ ContentDialog::loadVerseTranslation() void ContentDialog::loadVerseThoughts() { - ui->tedContent->setText(m_bookmarkDb->getThoughts(m_shownVerse.toList())); + ui->tedContent->setText(m_bookmarksDb->getThoughts(m_shownVerse)); ui->tedContent->setReadOnly(false); ui->tedContent->setCursorWidth(1); } @@ -296,8 +296,7 @@ ContentDialog::saveVerseThoughts() { ui->tedContent->setCursorWidth(0); ui->tedContent->setReadOnly(true); - m_bookmarkDb->saveThoughts(m_shownVerse.toList(), - ui->tedContent->toPlainText()); + m_bookmarksDb->saveThoughts(m_shownVerse, ui->tedContent->toPlainText()); } void diff --git a/src/dialogs/contentdialog.h b/src/dialogs/contentdialog.h index 07d2daa7..7e1c33c7 100644 --- a/src/dialogs/contentdialog.h +++ b/src/dialogs/contentdialog.h @@ -94,7 +94,7 @@ private slots: Ui::ContentDialog* ui; QSharedPointer m_quranDb = QuranDb::current(); QSharedPointer m_glyphsDb = GlyphsDb::current(); - QSharedPointer m_bookmarkDb = BookmarksDb::current(); + QSharedPointer m_bookmarksDb = BookmarksDb::current(); QSharedPointer m_tafsirDb = TafsirDb::current(); QSharedPointer m_translationDb = TranslationDb::current(); const int m_qcfVer = Settings::qcfVersion; diff --git a/src/dialogs/khatmahdialog.cpp b/src/dialogs/khatmahdialog.cpp index 017aa3bb..a313469a 100644 --- a/src/dialogs/khatmahdialog.cpp +++ b/src/dialogs/khatmahdialog.cpp @@ -23,8 +23,8 @@ KhatmahDialog::KhatmahDialog(QWidget* parent) QPointer KhatmahDialog::loadKhatmah(const int id) { - QList vInfo(3); - m_bookmarksDb->loadVerse(id, vInfo); + Verse v; + m_bookmarksDb->loadVerse(id, v); QFrame* frame = new QFrame(ui->scrlDialogContent); // property used for styling frame->setProperty("khatmah", true); @@ -56,8 +56,8 @@ KhatmahDialog::loadKhatmah(const int id) remove->setDisabled(true); } - QString info = tr("Surah: ") + m_quranDb->surahNames().at(vInfo[1] - 1) + - " - " + tr("Verse: ") + QString::number(vInfo[2]); + QString info = tr("Surah: ") + m_quranDb->surahNames().at(v.surah() - 1) + + " - " + tr("Verse: ") + QString::number(v.number()); lbPosition->setText(info); activate->setFocusPolicy(Qt::NoFocus); @@ -145,14 +145,14 @@ KhatmahDialog::removeKhatmah() void KhatmahDialog::setActiveKhatmah() { - QList vInfo(3); + Verse v; QFrame* newActive = qobject_cast(sender()->parent()); QVariant id = newActive->objectName(); m_settings->setValue("Reader/Khatmah", id); - m_bookmarksDb->saveActiveKhatmah(m_currVerse->toList()); + m_bookmarksDb->saveActiveKhatmah(*m_currVerse); m_bookmarksDb->setActiveKhatmah(id.toInt()); - m_bookmarksDb->loadVerse(id.toInt(), vInfo); + m_bookmarksDb->loadVerse(id.toInt(), v); newActive->findChild("activate")->setEnabled(false); newActive->findChild("remove")->setEnabled(false); @@ -161,7 +161,7 @@ KhatmahDialog::setActiveKhatmah() m_currActive = newActive; ui->lbCurrKhatmah->setText(m_bookmarksDb->getKhatmahName(id.toInt())); - emit navigateToVerse(vInfo); + emit navigateToVerse(v); } void diff --git a/src/downloader/recitationtask.cpp b/src/downloader/recitationtask.cpp index c447971d..27810611 100644 --- a/src/downloader/recitationtask.cpp +++ b/src/downloader/recitationtask.cpp @@ -1,5 +1,7 @@ #include "recitationtask.h" +#include + RecitationTask::RecitationTask() : m_reciter(-1) , m_surah(-1) @@ -43,7 +45,7 @@ RecitationTask::url() const const Reciter& r = *m_reciters.at(m_reciter); QString url = r.baseUrl(); if (r.useId()) - url.append(QString::number(m_quranDb->verseId(m_surah, m_verse)) + ".mp3"); + url.append(QString::number(Verse::id(m_surah, m_verse)) + ".mp3"); else url.append(QString::number(m_surah).rightJustified(3, '0') + QString::number(m_verse).rightJustified(3, '0') + ".mp3"); diff --git a/src/interfaces/userdataexporter.h b/src/interfaces/userdataexporter.h index e458b7ea..f9e7a710 100644 --- a/src/interfaces/userdataexporter.h +++ b/src/interfaces/userdataexporter.h @@ -1,16 +1,25 @@ #ifndef USERDATAEXPORTER_H #define USERDATAEXPORTER_H +#include #include -class UserDataExporter +class UserDataExporter : public QObject { + Q_OBJECT public: + enum Error + { + IOError + }; virtual void setFile(QString path) = 0; virtual void exportBookmarks() = 0; virtual void exportKhatmah() = 0; virtual void exportThoughts() = 0; virtual bool save() = 0; + +signals: + void error(Error err); }; #endif // USERDATAEXPORTER_H diff --git a/src/interfaces/userdataimporter.h b/src/interfaces/userdataimporter.h index afdef9ca..4a177d6c 100644 --- a/src/interfaces/userdataimporter.h +++ b/src/interfaces/userdataimporter.h @@ -1,15 +1,27 @@ #ifndef USERDATAIMPORTER_H #define USERDATAIMPORTER_H +#include #include -class UserDataImporter +class UserDataImporter : public QObject { + Q_OBJECT public: - virtual void setFile(QString path) = 0; + enum Error + { + IOError, + ParseError, + MissingKeyError, + InvalidValueError + }; virtual void importBookmarks() = 0; virtual void importKhatmah() = 0; virtual void importThoughts() = 0; + virtual void setFile(QString path) = 0; + virtual bool read() = 0; +signals: + void error(Error err); }; #endif // USERDATAIMPORTER_H diff --git a/src/types/verse.cpp b/src/types/verse.cpp index ebf7464d..a8417a02 100644 --- a/src/types/verse.cpp +++ b/src/types/verse.cpp @@ -19,10 +19,20 @@ Verse::surahVerseCount(int surah) return verseCount.at(surah - 1); } +int +Verse::id(int surah, int verse) +{ + int id = 0; + for (int i = 0; i < surah - 1; i++) + id += verseCount.at(i); + id += verse; + return id; +} + QSharedPointer Verse::current() { - static QSharedPointer current = QSharedPointer::create(); + static QSharedPointer current = QSharedPointer::create(1, 1, 1); return current; } @@ -125,13 +135,12 @@ Verse::next(bool basmalah) return *this; } - QList vInfo = - m_quranDb->verseById(m_quranDb->verseId(m_surah, m_number) + 1); + Verse v(m_quranDb->verseById(id(m_surah, m_number) + 1)); - if (vInfo[2] == 1 && vInfo[1] != 9 && vInfo[1] != 1 && basmalah) - vInfo[2] = 0; + if (v.number() == 1 && v.surah() != 9 && v.surah() != 1 && basmalah) + v.setNumber(0); - return Verse(vInfo); + return v; } Verse @@ -145,9 +154,7 @@ Verse::prev(bool basmalah) if (!m_number) m_number = 1; - QList vInfo = - m_quranDb->verseById(m_quranDb->verseId(m_surah, m_number) - 1); - return Verse(vInfo); + return Verse(m_quranDb->verseById(id(m_surah, m_number) - 1)); } void diff --git a/src/types/verse.h b/src/types/verse.h index ea409167..4a01d40c 100644 --- a/src/types/verse.h +++ b/src/types/verse.h @@ -16,6 +16,7 @@ class Verse public: static const QList verseCount; static const int surahVerseCount(int surah); + static int id(int surah, int verse); static QSharedPointer current(); static QList fromList(QList> lst); diff --git a/src/utils/jsondataexporter.cpp b/src/utils/jsondataexporter.cpp index 09e06a16..58844fa0 100644 --- a/src/utils/jsondataexporter.cpp +++ b/src/utils/jsondataexporter.cpp @@ -7,9 +7,9 @@ void JsonDataExporter::exportBookmarks() { QJsonArray bookmarks; - QList> all = m_bookmarksDb->bookmarkedVerses(); - foreach (const QList& vInfo, all) { - bookmarks.append(verseJson(vInfo)); + QList all = m_bookmarksDb->bookmarkedVerses(); + foreach (const Verse& v, all) { + bookmarks.append(verseJson(v)); } m_fileObj["bookmarks"] = bookmarks; @@ -21,10 +21,10 @@ JsonDataExporter::exportKhatmah() QJsonArray khatmah; QList ids = m_bookmarksDb->getAllKhatmah(); foreach (const int id, ids) { - QList vInfo(3); - m_bookmarksDb->loadVerse(id, vInfo); + Verse v; + m_bookmarksDb->loadVerse(id, v); QString name = m_bookmarksDb->getKhatmahName(id); - khatmah.append(khatmahJson({ name, vInfo })); + khatmah.append(khatmahJson({ name, v })); } m_fileObj["khatmah"] = khatmah; @@ -34,8 +34,8 @@ void JsonDataExporter::exportThoughts() { QJsonArray thoughts; - QList, QString>> all = m_bookmarksDb->allThoughts(); - for (const QPair, QString>& item : all) { + QList> all = m_bookmarksDb->allThoughts(); + for (const QPair& item : all) { thoughts.append(thoughtJson(item)); } m_fileObj["thoughts"] = thoughts; @@ -52,17 +52,7 @@ JsonDataExporter::verseJson(const Verse& v) } QJsonObject -JsonDataExporter::verseJson(const QList& vInfo) -{ - QJsonObject obj; - obj["page"] = vInfo[0]; - obj["surah"] = vInfo[1]; - obj["number"] = vInfo[2]; - return obj; -} - -QJsonObject -JsonDataExporter::khatmahJson(const QPair>& entry) +JsonDataExporter::khatmahJson(const QPair& entry) { QJsonObject obj; obj["name"] = entry.first; @@ -71,7 +61,7 @@ JsonDataExporter::khatmahJson(const QPair>& entry) } QJsonObject -JsonDataExporter::thoughtJson(const QPair, QString>& entry) +JsonDataExporter::thoughtJson(const QPair& entry) { QJsonObject obj; obj["verse"] = verseJson(entry.first); @@ -91,6 +81,7 @@ JsonDataExporter::save() QFile jsonFile(m_file.absoluteFilePath()); if (!jsonFile.open(QIODevice::WriteOnly)) { qWarning() << "Failed to open JSON file for writing"; + emit UserDataExporter::error(IOError); return false; } diff --git a/src/utils/jsondataexporter.h b/src/utils/jsondataexporter.h index 37f8aa7e..2d4b93a0 100644 --- a/src/utils/jsondataexporter.h +++ b/src/utils/jsondataexporter.h @@ -11,18 +11,17 @@ class JsonDataExporter : public UserDataExporter { public: JsonDataExporter(); - void setFile(QString path); void exportBookmarks(); void exportKhatmah(); void exportThoughts(); + void setFile(QString path); bool save(); private: QSharedPointer m_bookmarksDb = BookmarksDb::current(); QJsonObject verseJson(const Verse& v); - QJsonObject verseJson(const QList& vInfo); - QJsonObject khatmahJson(const QPair>& entry); - QJsonObject thoughtJson(const QPair, QString>& entry); + QJsonObject khatmahJson(const QPair& entry); + QJsonObject thoughtJson(const QPair& entry); QJsonObject m_fileObj; QFileInfo m_file; }; diff --git a/src/widgets/quranpagebrowser.cpp b/src/widgets/quranpagebrowser.cpp index 6c75162b..e90e2cba 100644 --- a/src/widgets/quranpagebrowser.cpp +++ b/src/widgets/quranpagebrowser.cpp @@ -188,7 +188,7 @@ QuranPageBrowser::constructPage(int pageNo, bool forceCustomSize) setHref(&textCursor, 1, "#F" + QString::number(m_headerData.first)); } - setMinimumWidth(m_pageLineSize.width() + 70); + parentWidget()->setMinimumWidth(m_pageLineSize.width() + 70); // page lines drawing int counter = 0, prevAnchor = pageNo < 3 ? 0 : m_currPageHeader.size() + 1; From d7b9ba51eaf55a35f5e465f5a5f5a21e9628ab0d Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Mon, 26 Feb 2024 23:19:56 +0200 Subject: [PATCH 30/51] feat: add jsondataimporter class for importing user data --- CMakeLists.txt | 2 + src/utils/jsondataimporter.cpp | 179 +++++++++++++++++++++++++++++++++ src/utils/jsondataimporter.h | 33 ++++++ 3 files changed, 214 insertions(+) create mode 100644 src/utils/jsondataimporter.cpp create mode 100644 src/utils/jsondataimporter.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 69cf6cef..461c0dd5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,6 +107,8 @@ set(PROJECT_SOURCES src/utils/versionchecker.cpp src/utils/jsondataexporter.h src/utils/jsondataexporter.cpp + src/utils/jsondataimporter.h + src/utils/jsondataimporter.cpp src/downloader/surahjob.h src/downloader/surahjob.cpp src/downloader/recitationtask.h diff --git a/src/utils/jsondataimporter.cpp b/src/utils/jsondataimporter.cpp new file mode 100644 index 00000000..2909d8f6 --- /dev/null +++ b/src/utils/jsondataimporter.cpp @@ -0,0 +1,179 @@ +#include "jsondataimporter.h" +#include +#include + +JsonDataImporter::JsonDataImporter() {} + +void +JsonDataImporter::importBookmarks() +{ + if (!validArray("bookmarks")) + return; + QJsonArray arr = m_fileObj.value("bookmarks").toArray(); + foreach (const QJsonValue& item, arr) { + Verse bookmark = verseFromJson(item.toObject()); + if (!m_bookmarksDb->isBookmarked(bookmark)) + m_bookmarksDb->addBookmark(bookmark, true); + } +} + +void +JsonDataImporter::importKhatmah() +{ + if (!validArray("khatmah")) + return; + QJsonArray arr = m_fileObj.value("khatmah").toArray(); + foreach (const QJsonValue& item, arr) { + QPair khatmah = khatmahFromJson(item.toObject()); + m_bookmarksDb->addKhatmah(khatmah.second, khatmah.first); + } +} + +void +JsonDataImporter::importThoughts() +{ + if (!validArray("thoughts")) + return; + QJsonArray arr = m_fileObj.value("thoughts").toArray(); + foreach (const QJsonValue& item, arr) { + QPair thought = thoughtFromJson(item.toObject()); + m_bookmarksDb->saveThoughts(thought.first, thought.second); + } +} + +bool +JsonDataImporter::validArray(QString key) +{ + if (!m_fileObj.contains(key)) { + emit UserDataImporter::error(MissingKeyError); + return false; + } + if (!m_fileObj.value(key).isArray()) { + emit UserDataImporter::error(InvalidValueError); + return false; + } + return true; +} + +bool +JsonDataImporter::validVerse(const QJsonObject& obj) +{ + if (!obj.contains("page") || !obj.contains("surah") || + !obj.contains("number")) { + emit UserDataImporter::error(MissingKeyError); + return false; + } + + int page = obj.value("page").toInt(), surah = obj.value("surah").toInt(), + number = obj.value("number").toInt(); + if (page < 1 || page > 604 || surah < 1 || surah > 114 || number < 1 || + number > 286) { + emit UserDataImporter::error(InvalidValueError); + return false; + } + + return true; +} + +bool +JsonDataImporter::validKhatmah(const QJsonObject& obj) +{ + if (!obj.contains("name") || !obj.contains("verse")) { + emit UserDataImporter::error(MissingKeyError); + return false; + } + + if (!obj.value("name").isString() || + !validVerse(obj.value("verse").toObject())) { + emit UserDataImporter::error(InvalidValueError); + return false; + } + + return true; +} + +bool +JsonDataImporter::validThought(const QJsonObject& obj) +{ + if (!obj.contains("text") || !obj.contains("verse")) { + emit UserDataImporter::error(MissingKeyError); + return false; + } + + if (!obj.value("text").isString() || + !validVerse(obj.value("verse").toObject())) { + emit UserDataImporter::error(InvalidValueError); + return false; + } + + return true; +} + +Verse +JsonDataImporter::verseFromJson(const QJsonObject& obj) +{ + Verse v; + if (!validVerse(obj)) + return v; + + v.setPage(obj.value("page").toInt()); + v.setSurah(obj.value("surah").toInt()); + v.setNumber(obj.value("number").toInt()); + return v; +} + +QPair +JsonDataImporter::khatmahFromJson(const QJsonObject& obj) +{ + QString name; + Verse v; + if (!validKhatmah(obj)) + return { name, v }; + + name = obj.value("name").toString(); + v = verseFromJson(obj.value("verse").toObject()); + return { name, v }; +} + +QPair +JsonDataImporter::thoughtFromJson(const QJsonObject& obj) +{ + Verse v; + QString text; + if (!validThought(obj)) + return { v, text }; + + v = verseFromJson(obj.value("verse").toObject()); + text = obj.value("text").toString(); + return { v, text }; +} + +void +JsonDataImporter::setFile(QString path) +{ + m_filepath = path; +} + +bool +JsonDataImporter::read() +{ + QFile jsonFile(m_filepath); + if (!jsonFile.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open json file during import"; + emit UserDataImporter::error(IOError); + return false; + } + + QJsonParseError* err = nullptr; + QJsonDocument document = QJsonDocument::fromJson(jsonFile.readAll(), err); + if (document.isNull()) { + qWarning() << "Failed to parse json file"; + emit UserDataImporter::error(ParseError); + if (err) + qWarning() << "Error string:" << err->errorString(); + return false; + } + + m_fileObj = document.object(); + return true; +} diff --git a/src/utils/jsondataimporter.h b/src/utils/jsondataimporter.h new file mode 100644 index 00000000..737f7331 --- /dev/null +++ b/src/utils/jsondataimporter.h @@ -0,0 +1,33 @@ +#ifndef JSONDATAIMPORTER_H +#define JSONDATAIMPORTER_H + +#include +#include +#include +#include +#include + +class JsonDataImporter : public UserDataImporter +{ +public: + JsonDataImporter(); + void importBookmarks(); + void importKhatmah(); + void importThoughts(); + void setFile(QString path); + bool read(); + +private: + QSharedPointer m_bookmarksDb = BookmarksDb::current(); + bool validArray(const QString key); + bool validVerse(const QJsonObject& obj); + bool validKhatmah(const QJsonObject& obj); + bool validThought(const QJsonObject& obj); + Verse verseFromJson(const QJsonObject& obj); + QPair khatmahFromJson(const QJsonObject& obj); + QPair thoughtFromJson(const QJsonObject& obj); + QString m_filepath; + QJsonObject m_fileObj; +}; + +#endif // JSONDATAIMPORTER_H From eb4eacf7eb5ad36bf6fbcdb0cfb9f27030a2c4dc Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Tue, 27 Feb 2024 23:45:39 +0200 Subject: [PATCH 31/51] feat: add dialog for selecting what to export/import and add import/export buttons to settings dialog --- CMakeLists.txt | 5 + src/dialogs/fileselector.cpp | 68 ++++++++++ src/dialogs/fileselector.h | 23 ++++ src/dialogs/importexportdialog.cpp | 150 +++++++++++++++++++++ src/dialogs/importexportdialog.h | 51 +++++++ src/dialogs/importexportdialog.ui | 133 ++++++++++++++++++ src/dialogs/settingsdialog.cpp | 29 +++- src/dialogs/settingsdialog.h | 13 +- src/dialogs/settingsdialog.ui | 210 ++++++++++++++++------------- src/interfaces/userdataexporter.h | 2 +- src/interfaces/userdataimporter.h | 3 +- src/utils/jsondataexporter.cpp | 3 +- src/utils/jsondataimporter.cpp | 32 +++-- src/utils/jsondataimporter.h | 13 +- 14 files changed, 620 insertions(+), 115 deletions(-) create mode 100644 src/dialogs/fileselector.cpp create mode 100644 src/dialogs/fileselector.h create mode 100644 src/dialogs/importexportdialog.cpp create mode 100644 src/dialogs/importexportdialog.h create mode 100644 src/dialogs/importexportdialog.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index 461c0dd5..fd37e707 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,6 +82,11 @@ set(PROJECT_SOURCES src/dialogs/versedialog.h src/dialogs/versedialog.cpp src/dialogs/versedialog.ui + src/dialogs/fileselector.h + src/dialogs/fileselector.cpp + src/dialogs/importexportdialog.h + src/dialogs/importexportdialog.cpp + src/dialogs/importexportdialog.ui src/interfaces/userdataimporter.h src/interfaces/userdataexporter.h src/interfaces/downloadjob.h diff --git a/src/dialogs/fileselector.cpp b/src/dialogs/fileselector.cpp new file mode 100644 index 00000000..3ca41f9e --- /dev/null +++ b/src/dialogs/fileselector.cpp @@ -0,0 +1,68 @@ +#include "fileselector.h" +#include +#include + +FileSelector::FileSelector(QWidget* parent) + : QFileDialog(parent) +{ + setFileMode(QFileDialog::AnyFile); + setDirectory(DirManager::downloadsDir->absolutePath()); +} + +QString +FileSelector::selectJson(Mode mode) +{ + setCaption(mode); + setAcceptMode(mode); + setDefaultSuffix("json"); + setNameFilter("JSON Document (*.json)"); + if (!exec() || !validFile(mode, selectedFiles().at(0))) + return ""; + return selectedFiles().at(0); +} + +void +FileSelector::setCaption(Mode mode) +{ + switch (mode) { + case Mode::Read: + setWindowTitle(qApp->translate("QFileDialog", "Open File")); + break; + case Mode::Write: + setWindowTitle(qApp->translate("QFileDialog", "Save File")); + break; + default: + break; + } +} + +void +FileSelector::setAcceptMode(Mode mode) +{ + switch (mode) { + case Mode::Read: + QFileDialog::setAcceptMode(QFileDialog::AcceptOpen); + break; + case Mode::Write: + QFileDialog::setAcceptMode(QFileDialog::AcceptSave); + break; + default: + break; + } +} + +bool +FileSelector::validFile(Mode mode, const QString& file) +{ + if (file.isEmpty()) + return false; + + QFileInfo fInf(file); + if (mode == Read) + return fInf.isReadable(); + if (mode == Write) + return fInf.isWritable() || + QFileInfo(fInf.dir().absolutePath()).isWritable(); + + return false; +} diff --git a/src/dialogs/fileselector.h b/src/dialogs/fileselector.h new file mode 100644 index 00000000..03665d20 --- /dev/null +++ b/src/dialogs/fileselector.h @@ -0,0 +1,23 @@ +#ifndef FILESELECTOR_H +#define FILESELECTOR_H + +#include + +class FileSelector : public QFileDialog +{ +public: + enum Mode + { + Read, + Write + }; + FileSelector(QWidget* parent = nullptr); + QString selectJson(Mode mode); + +private: + void setCaption(Mode mode); + void setAcceptMode(Mode mode); + bool validFile(Mode mode, const QString& file); +}; + +#endif // FILESELECTOR_H diff --git a/src/dialogs/importexportdialog.cpp b/src/dialogs/importexportdialog.cpp new file mode 100644 index 00000000..e153119c --- /dev/null +++ b/src/dialogs/importexportdialog.cpp @@ -0,0 +1,150 @@ +#include "importexportdialog.h" +#include "ui_importexportdialog.h" + +#include + +ImportExportDialog::ImportExportDialog(QWidget* parent, + QPointer importer, + QPointer exporter) + : ui(new Ui::ImportExportDialog) + , m_importer(importer) + , m_exporter(exporter) + , QDialog(parent) +{ + ui->setupUi(this); + connect(ui->buttonBox, + &QDialogButtonBox::clicked, + this, + &ImportExportDialog::dialogButtonClicked); + connect(m_importer, + &UserDataImporter::error, + this, + &ImportExportDialog::importError); + connect(m_exporter, + &UserDataExporter::error, + this, + &ImportExportDialog::exportError); +} + +void +ImportExportDialog::selectImports(QString filepath) +{ + m_mode = Mode::Import; + m_importer->setFile(filepath); + if (m_importer->read()) + setCheckedImports(); + open(); +} + +void +ImportExportDialog::selectExports(QString filepath) +{ + m_mode = Mode::Export; + m_exporter->setFile(filepath); + setCheckedExports(); + open(); +} + +void +ImportExportDialog::setCheckedImports() +{ + ui->chkBookmarks->setEnabled(m_importer->fileContains("bookmarks")); + ui->chkKhatmah->setEnabled(m_importer->fileContains("khatmah")); + ui->chkThoughts->setEnabled(m_importer->fileContains("thoughts")); + + ui->chkBookmarks->setChecked(ui->chkBookmarks->isEnabled()); + ui->chkKhatmah->setChecked(ui->chkKhatmah->isEnabled()); + ui->chkThoughts->setChecked(ui->chkThoughts->isEnabled()); +} + +void +ImportExportDialog::setCheckedExports() +{ + ui->chkBookmarks->setEnabled(true); + ui->chkKhatmah->setEnabled(true); + ui->chkThoughts->setEnabled(true); + + ui->chkBookmarks->setChecked(true); + ui->chkKhatmah->setChecked(true); + ui->chkThoughts->setChecked(true); +} + +void +ImportExportDialog::importSelected() const +{ + if (ui->chkBookmarks->isChecked()) + m_importer->importBookmarks(); + if (ui->chkKhatmah->isChecked()) + m_importer->importKhatmah(); + if (ui->chkThoughts->isChecked()) + m_importer->importThoughts(); +} + +void +ImportExportDialog::exportSelected() const +{ + if (ui->chkBookmarks->isChecked()) + m_exporter->exportBookmarks(); + if (ui->chkKhatmah->isChecked()) + m_exporter->exportKhatmah(); + if (ui->chkThoughts->isChecked()) + m_exporter->exportThoughts(); + + m_exporter->save(); +} + +void ImportExportDialog::setExporter(QPointer newExporter) +{ + m_exporter = newExporter; +} + +void ImportExportDialog::setImporter(QPointer newImporter) +{ + m_importer = newImporter; +} + +void +ImportExportDialog::accept() +{ + switch (m_mode) { + case Mode::Import: + importSelected(); + break; + case Mode::Export: + exportSelected(); + break; + default: + break; + } + hide(); +} + +void +ImportExportDialog::importError(UserDataImporter::Error err, QString str) +{ + QString msg(tr("The following error occured during import") + ":" + "\n%0"); + QMessageBox::warning(this, tr("Error"), msg.arg(str)); + reject(); +} + +void +ImportExportDialog::exportError(UserDataExporter::Error err, QString str) +{ + QString msg(tr("The following error occured during export") + ":" + "\n%0"); + QMessageBox::warning(this, tr("Error"), msg.arg(str)); + reject(); +} + +void +ImportExportDialog::dialogButtonClicked(QAbstractButton* btn) +{ + if (ui->buttonBox->buttonRole(btn) == QDialogButtonBox::AcceptRole) + accept(); + else + reject(); +} + +ImportExportDialog::~ImportExportDialog() +{ + delete ui; +} diff --git a/src/dialogs/importexportdialog.h b/src/dialogs/importexportdialog.h new file mode 100644 index 00000000..1b21e6a1 --- /dev/null +++ b/src/dialogs/importexportdialog.h @@ -0,0 +1,51 @@ +#ifndef IMPORTEXPORTDIALOG_H +#define IMPORTEXPORTDIALOG_H + +#include +#include +#include +#include +#include + +namespace Ui { +class ImportExportDialog; +} + +class ImportExportDialog : public QDialog +{ + Q_OBJECT +public: + enum Mode + { + Import, + Export + }; + explicit ImportExportDialog(QWidget* parent, + QPointer importer, + QPointer exporter); + ~ImportExportDialog(); + + void selectImports(QString filepath); + void selectExports(QString filepath); + + void setImporter(QPointer newImporter); + void setExporter(QPointer newExporter); + +private slots: + void dialogButtonClicked(QAbstractButton* btn); + void importError(UserDataImporter::Error err, QString str); + void exportError(UserDataExporter::Error err, QString str); + void accept(); + +private: + Ui::ImportExportDialog* ui; + void setCheckedImports(); + void setCheckedExports(); + void importSelected() const; + void exportSelected() const; + QPointer m_importer; + QPointer m_exporter; + Mode m_mode; +}; + +#endif // IMPORTEXPORTDIALOG_H diff --git a/src/dialogs/importexportdialog.ui b/src/dialogs/importexportdialog.ui new file mode 100644 index 00000000..744c5d1f --- /dev/null +++ b/src/dialogs/importexportdialog.ui @@ -0,0 +1,133 @@ + + + ImportExportDialog + + + + 0 + 0 + 295 + 176 + + + + + 295 + 176 + + + + Dialog + + + + + + + + + Bookmarks + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + + + Khatmah + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + + + Thoughts + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/src/dialogs/settingsdialog.cpp b/src/dialogs/settingsdialog.cpp index d716bbf2..9f01a4bd 100644 --- a/src/dialogs/settingsdialog.cpp +++ b/src/dialogs/settingsdialog.cpp @@ -13,6 +13,7 @@ SettingsDialog::SettingsDialog(QWidget* parent, VersePlayer* vPlayerPtr) : QDialog(parent) , ui(new Ui::SettingsDialog) , m_vPlayerPtr(vPlayerPtr) + , m_selectorDlg(new FileSelector(this)) { ui->setupUi(this); ui->cmbQuranFontSz->setValidator(new QIntValidator(10, 72)); @@ -22,9 +23,11 @@ SettingsDialog::SettingsDialog(QWidget* parent, VersePlayer* vPlayerPtr) ui->tableViewShortcuts->horizontalHeader()->setStretchLastSection(true); ui->tableViewShortcuts->setItemDelegate(new ShortcutDelegate); + m_importExportDlg = + new ImportExportDialog(this, &m_jsonImporter, &m_jsonExporter); + fillLanguageCombobox(); setCurrentSettingsAsRef(); - // connectors setupConnections(); } @@ -35,6 +38,14 @@ SettingsDialog::setupConnections() &QDialogButtonBox::clicked, this, &SettingsDialog::btnBoxAction); + connect(ui->btnImport, + &QPushButton::clicked, + this, + &SettingsDialog::importUserData); + connect(ui->btnExport, + &QPushButton::clicked, + this, + &SettingsDialog::exportUserData); } void @@ -380,6 +391,22 @@ SettingsDialog::btnBoxAction(QAbstractButton* btn) } } +void +SettingsDialog::importUserData() +{ + QString path = m_selectorDlg->selectJson(FileSelector::Read); + if (!path.isEmpty()) + m_importExportDlg->selectImports(path); +} + +void +SettingsDialog::exportUserData() +{ + QString path = m_selectorDlg->selectJson(FileSelector::Write); + if (!path.isEmpty()) + m_importExportDlg->selectExports(path); +} + void SettingsDialog::showWindow() { diff --git a/src/dialogs/settingsdialog.h b/src/dialogs/settingsdialog.h index c4848480..75d993f6 100644 --- a/src/dialogs/settingsdialog.h +++ b/src/dialogs/settingsdialog.h @@ -6,6 +6,8 @@ #ifndef SETTINGSDIALOG_H #define SETTINGSDIALOG_H +#include "fileselector.h" +#include "importexportdialog.h" #include #include #include @@ -23,6 +25,8 @@ #include #include #include +#include +#include #include #include #include @@ -39,7 +43,6 @@ class SettingsDialog; class SettingsDialog : public QDialog { Q_OBJECT - public: /** * @brief Class constructor @@ -213,6 +216,10 @@ public slots: */ void closeEvent(QCloseEvent* event); +private slots: + void importUserData(); + void exportUserData(); + private: Ui::SettingsDialog* ui; const int m_qcfVer = Settings::qcfVersion; @@ -313,6 +320,10 @@ public slots: * @brief pointer to VersePlayer instance. */ QPointer m_vPlayerPtr; + QPointer m_selectorDlg; + QPointer m_importExportDlg; + JsonDataImporter m_jsonImporter; + JsonDataExporter m_jsonExporter; /** * @brief model used by the shortcuts QTableView */ diff --git a/src/dialogs/settingsdialog.ui b/src/dialogs/settingsdialog.ui index 0c9c5f92..23c0ebdd 100644 --- a/src/dialogs/settingsdialog.ui +++ b/src/dialogs/settingsdialog.ui @@ -29,7 +29,7 @@ QTabWidget::Rounded

VLNY%L)rlp0~*5%*devuhmrwNsWth8E>S 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�iu48yt;I8Ez!`rj1MZRZ6!d4J2J^0IGC75!C7a(oNZaNKN&eIS`&jMK>ZdEe z@+Ip09V*3iKT2*6HOkyAO+Od8m-=^5rmA_e2FC8DuqS3PKwK$n^ks(zYbt2qDYGiW zOMl`{??-*jGmQ@-#^Xp2JH7)cE&?$?yUorN;H&r=iE#eLDcUE+FeQ_Tu7W?xxQ4L& zefGPlAF1Ue$wfL!1wmOm+H=`G?m#B$ptz~&kApZ;3{XGOjK%*IsLkQS%^T=Yy6ZK$s$l(H|1 zZR5N;zq{AyyC`S*#)i|i-0}z(u7Vf4O;a56926<5RtFm@LZP@|E=4~TNlCDkgn|^I z0}?~YMF2dsy;zgLBtt5mX>Sqy1V?CR##YwIlghr!KDa%{j%n=tBo6xU5b87|#E&?E z8dCdFt%>cK2<4SGV+TZ|r(BGFcVyftuMN zu`aVW@eUa|c?@&lM&^{F1s5s_Xt*QXdBtKy{FWu!$1_2LpG5-zSWN9ysS{&^0DkDl z>Cx%Q$_JQ_`rjHY|7+jLkN^MQmkI*C5BA-efBAf~sUtfp7Uc9gIJ+eYj#4#?GCCnG zg$ugK$zBk#i&i>h4op!VD1OPICoi3Ek;Ik=5A7f{vdAFT#ax0T{CG++<1a+J^jbO7 z&w4bOD`mu)&U8p^3JrzZkUW}gl)M(KIh^=&N7Yp$FW1_jg-~$;E*r+Us z?5Q!q97nUr1EJ!nwuqU&qC|sGbkAk$Z?@e#+@F;pR9O(kjS68=a6<^fvbUHBF#^YQFk#e*&WsL^GV3X%zpZWVn`tmwVceDj3n_s~untu%}zkWJK(+TLw6 z{u5`>2tx`b6fa5$7la8GH9=oS&K*0s&^b=*NpS;#CwvrOD?A4@{p?x2)2Fe$Jy(P} z$Z2V?g%NDDe$8g#fhB1P^81 z9)NOV==q64KZ~bH95?5rvF{+((v?;eTdRkv@QY|6j!u zPYZ5jY-NK$&JJyZRr9W}AmrsPy0<5e!^t8c6!{lilPBBOdfYE0Dg;GVSCV)EF~kKl z3(WL?x#oX8$Ml~^YF97;tXlb@izk3}FYmjxZttq*8}|T3m*R5h7DQ=m32&YyB*6}# z1Raf=CEM@}x=8RX9Nr#h!fzm{u9L!+1PrhA5C`~&FEpr ztB9hDLHgwx3nW4lW45nNWQ0S@Q?qUU;M$@K_1pT#jZ(KaV_dvmUPAY{9;-W(>ohq# z{J@K`Y3r8#c%L-!F^JRxPdWOzJ|>O?3!gr)dMa0r*7>ktQ)STX$$m8FJ=nbB5E5G| zf~676AcKZE3vNG>U|TFq+c$rFQrFe#h8# zck|M1d$vp4Smfa7yi!JWxJj9D-zwXqx59 z%>yVk?=vug+c=f8C!L!vHh6|%kl9KJn-dmm`rkyu|9Yjit@^jf0RFkJaQFX8<(~e> zgoz8yON5CKT(ZBi|IA+A;J)mf>_~amgp4!^+_A0yoNhkURBO47TJM|el&P?*`>wuQ zfH_^1?XvGssJ6(+AaL#o`Iy;y=Fo~T zTMiCKN=2%!C`gD01LHEe=d9pw$%~N&;R?W{Mhb$WgG8i4G}6Z*NG9UW6NQ_aR2KA(?nK{xc$aQz%;{Ks2p6v* zS~YnQdT6wqNben*`jsS8Uaw*?a02Dgn;coRs0okzX?6P%Irx#|@_&h$u4@Jd3AlL_ z9FkNq>|+F*6J=*R>Qj0iwTV!m`;Y_F_>h^xx~%(&zMKCpEjHBaZ_73>HhBw3RDB>z ze1jO33A-VGBbaPQf5uuI`ki!RZjBv+1K*O~$6cB+c_ub|*Az z)R&Q1vLL(Y`;#W{A61b(S(3REg|!q=$Sb2`oNTciq9`C^n&<_W~6|H~@( zRq9_~`}x|ttDmX-+sfDT=j;A=xF2ncZ*I&^$n9$9k-bvE;?2DXNaJ&kk%w{gqdt_A zi>{o=4UT8q6aDB5d`n}7LxJX^zbrCmxOZpcd7(j|&vrOLDm%%7(UN0w`bEt`9F|&9NSwmX zKzbOZ)s@$+qF$sNDUo6FWfxq7Tj>6?EwvIwq1n!Ua#bDUxBBi`%<`s>h^8Va=*3tQ z1Mhf3nt93kOC>g_SYJ|vB0|NnpdE6XZ-dNTCb>gw zhew2mkzho3Pd_>wzuGvNo;~V6eL#(n)f|_{#9+|WcNLSF)H|9*B3{Ffp#Eu}y+xt$ zZX7NZU1^d420@jySSn9L&U*Pl;i>0H>9IhEe3QI>k?^!?8BRN?d7*ux@4m?0hZ`rf z2VPXRcA{o7I7ce3)H1ZtAaX$IHY7Zo{n+412wWr@T9ZpX98Le1R_?FVpQ@d!{u2Ga z?tk5+pDygar|(wDy{X3WaUGQgT^s;_shkZS=Fo{1SFkVIlj@|+CRjcPye;dY3cjF0 zYg0L$N55}qk_k&7r#ZDs1OTSiZPKbqER;Oijx$?2vz`{|M4AKk7!mx=9C>_C_T6Q9 zX?tTzKt|vw`UU{)Of}I&rLh9>a2_I9yeRon{n~O!AayW9A`1?`7Y#s;ed4iN{{~2> z6bQZoZ>3(Xov8j!wa2fo<6m#Q z@8;4gS2a4K9nJ-{c0&i|u;-F~-O9-2C?cva4(0pC;Tw_%NRweFrjMrdV&+1|i}XJm zfk;8=d4)_1V8Dw>os6bVg$sA1NZopAOi7%0>Ll(0GZ)oi=Wzc?hIpm=ldPsIE~)Ji z<%W?+0Fn?USC)rKW(SP1@;IeyMQ#6Vd$NB}7Gn3o#>cFa1llWU5(N){2uG1_sTwgS zwJNzCz~iV0&kzs8(+Tnq10LnX|wKvdhF&1!W1PrLa8$@DyiuV+<#cF?`>~&_FxwMhh{OhIbYr zfO&%r$C3QJ%a093p#Ut}018i}L9#ojUWtCvD_<)G2@4?xF%Ti7R*>nX z!`!KWLvb)E|NjrE|Ev8@?IYFSuD)FP!`Hz8{`6n+YX5-g;pN6h^062EI0+e98&=$% zX3c@Px$`WhwKN=Ij<7iZ|hf^f_6s+sezvaS=<3xpc#5$4(vrCv{}~T!Y({<`}ES@n;TE*rKN`DeJT~}b(PpF1;9?1 zqNo5pOxl%AXP}gs$7{;tyZR4$F((=yHUkXnQ3{u%toa%F#8m#A9r>jYLA#%y!_62P zYN=nqd&4-h0*C9wx{_hK{(3;4bc}V;UDfZYv_M)No63n$&-7!+;GK;lk^7#QXVR31 z9L+h0B1r_+B*?jlJ)5E&HaKVzLv7oRzizOxkCG9w;kS zIzBfZXmj`!RuFYvYLtDO$3fQkM#@(E^JQ6Mv@(PUf%_t`r|gwqg=vPof0qt{oEqk6b{P#b!qOgs$yV+8{vH!!r!iqvjH(@u-SIKThv3$g^n=Jw^5%KQ zwh1GWhdV?~K5Q**D6yK$l2(Q2lA}lW%Z-Tw5VHst8i#dKmaH6to%>FuCwr>HoS)KP z;0Uzi_tMMsFm09xA|A8s17CxU>vj#|?fh%eBiyt)K{NvMYRBV1qy)wksD{&IFE2`1=G|g(t$B zlJnCp3O)hA1$<&ucIa`BNmWe#|BqMd-#`T5aP@~Pe^Q~p=TpI^EgU>3ZKX772 zLKM}IiYpita1eMXz{nz2DeOrUss8WM%AQKSQF{k{c(3Kx&+i{@wD)SMxX})*KW##2 zO1yF(XfTYrfAZ$7IswNrv^t!a#n~fK(#V3WBc#zvMkUTXVF=reL<{yAnIl{9cnB3$ z?JWzuPz^#<@g=O)D8jEP@*2as01h8XH2^dzHR|d9T|R?58skSGACKs@SAg@9-JAk+ z@exd$2bBKcz)Xc=Qa8r}Z9T%H-EH^ZW4wE+vHwuqtIYH)mBV?#{K#4S5*-Dtv~+vh zogM;fae@a35DSa2gtP_Qj4a2RgEKGy$0T{6s&kE1J37H~aYp%SeQ$^qmH>8SE|x}U zkOm0PIo9{&m!$_9`$BgXofyH2WNCq5*wv{9L{6dg&|tlz@WIGs9U_f;NZ$y6`EkZ? z^lTM?v3*!fxce16FBpH}&;!A+zo;-(wzl7pQnrg4m|@nJU5QD;(%w-G{^J2IgtGl=K?=ThZZ|vQdPKwA~^RRx}z zYGmL%ybMOEQsWo1<5KVBKwu%$^kEhF{MS?`j)nzDb#p!>WR7bNhQyYWYAr&`=ITLI z|7oSZqygJJxjozWNTKeL#vX-6P^q`seZgf&d?-&`b7zs66A6s4EChy5Cw!%Gl&OTT zbi49i=?>EXIUAkr?xFrp%ereD_vsW#?VOfwFjQ>kqq1~|qXvY6LucqXs5^3i#$IxE>%IR{e|9XPm!2#kruIYz`?V zVWjxqpRLr#YM-utfAz|*(EOinrSGOG-B%i$Z4FZ@iu8}_Ch%E#qpUwc%i1gmoAt)P znIPv}D6RA1j)YIc-`SB`^g`7n7O6&OlpPSAPWCih#ul>G<>x|YJ!~At8sT0Y8tP4n z>F66-K5v;i4v~Um8U?rDp}Eo0SP|?PBDBkFT@u-9A%~jo5g#$}bZ{Mz1(u)56^e&4 zbneKio1ZMbv(bdwkp4p}g#A$4a@^%iq@ftqiAb>eAaN^HsI%cek)h76EGt}b#@E&W z#A1xdd|{$~#MzfT9A9GS4oznBSIbZuM*|l~7N(zNZk6)kx0i>uX-7Qw9wm05q$7GTHW{yir5WXY&n&?dt(MWZZ$Z=Kt;KY(AfhV4r znk zHkMT|&N0%DZ66ihT}3AlhxA;O_GT27*A`EpTO_E@fEZp``Mv%^4qu`QIRQE{dCzhU zpH8j6)o5I8yk}1g+?7p`0EqZ!bwW6yF_>#Xg|a3umc^||C;WuLee*TS-c~x#kdmM4 z%17KwY!D}Zuh#d3lI3dS&O@o-JX@d61CJ;1E|EqqiUDO=nv6X~ht4udU_xs*<#?fX z9wYcOb8u@MVv|n85%pRArw9gH)}{Hi-n7Y3E+h3(^~3#KzX67Wvz1?ANKg&U@7XJV zCaIO)&3b$b9P}vDm(3{-Nc{n>M6f7sU^#G=IrTp7d!Wkh#=Cvl{ciP&HH8hUfVzg{ zPWNxnY2k{`4SM01^2Z;szXsL}ynE(i-G6}1#e zW6A&jN~OL?{m)wUXR41>n1}Rb`bUMGhq)}Z8tpv*6KF-di)<+d8fu`zi&N92q~y+K zAPDqj%z9+kM7aKdZn{*<)DpGNI8F%JD*SWmcoY7MpbjVuCu%TS! zyqX~k1P|Y&n7L|j;G(;_5A;2_-o1>FsK~1D-S7s|Q;v6zQC% zr#NQhM8-y)3l&aeX)t(RHBC7Q1=-|@L+~EkZ4oZuUJ%m!8hmq{G*O8vvN->N-c{ik#Q{{{X`|0t31WR~8ujcwzyHY}T*n9I9~?oPXx z;9|r?C0^tm;tRB>gPl{SI_Q9ya1kQW*OcfaEy~7AD%}yU#eb!S#5i2(iH>lvabJjj z5LP*9F>EEl3seiOLgN3NTtg~{+DbmMM|+TueA~oHAjbc;MLMu`J2*aUm)=hz&|)@n zQi;8)k#NH>{*c!z!j@qm%q9cOs1Zh|y+A;PevTRtn^+ZlMidf;(~+3dOWC7jSCJ7e zE8E81*KfH1uW{=HSYt(q^$rs$izDH*$WD*;7v58HdJt0eObbG)x0nNw!RM6*PfXc4 zQ!g#fz%h~jp?0yv@KtF~B*ur|#HoN$dlTQ9X<~P)VL+xMNE;<3V>fEg3<_|g?|~M* z8yjyAUm*_3B!Pk-=mZ3MFP*n%hg^|EkymD?ffXDvcwmdH*8B2b5}TU8#e=5G#-})m zY}>)!06obbST?8oo?+2_s&Vr^sVRCEiCju%4n7SsrN2YxJExt+@u~}|-g=ao1H>F< zfUyIxDn>nLt~Y3FlWQ51s*l8#>0E+l`z2JqD3X zL0*#{?o=$6jUA7QRyINoxkjfu72fJ=oczV3F`S_zUl5u) zTf?4hSVep-Lftk1#aSfT!r7jiH|dn+2?7CC_pdH61S43SB%A4t^;uB{H68YyY(L(= z$~x)RhPjK+x4;&^t1}73()u-?AnuN6g6R#pi-d)IWVWHy6hzD$Z3A7|xbbiXj=4AO zK3x9-QG$+==PEH2O~@q}hLuy%ll^M?9B>Y^ z{sk^`-UtI!7U4tcW_p?dLGXu5`W~IoyR>oL{W!8P@XcV0cue8lM(vRINC&k+u3v<0 z0@?ilIfjac;2|H4LveWU_YN|cJQvUqX_@T*8!Pqm_3LWytPZM`%GXqwPMiMC^kdq= z6OC)LjlS^0A^?JYt4Njqgd#_GV&0(a#c-`o3KNs47ft%_GMWRbvLSaqkWd7lrBt-% z`Z4R^nZ{S`Pgx(zVPs57#Iq;M=74gDO-+KTIxW2ctUr#e1wrbQyh`(30y%1%fX>K} zDG@mQaSo5y0@CWLz6Tlf-r9KEQ=i>kV-=HCa$f=xBNr9=z@zFE6$p3YTl$_kK&JdPdxV+Jt|^buTNN?@=!M0jc+4V? zJXOFNMG^>PLl@8NsSs>h`yHNnOz06P*_u=)p7Y){w1j}xTSt(6{ zr3Pxjg3c<3&C|eW=^a9v3o#JT%YEQC?VFyuDMOkkh-}fh7FA2AM+Vcrm^#$b&4qZT zji>vbbkMz{@s`7%TjjIr+c~{`Nb?&a|E%u;-39eRYi;|Zg?Ye z4zRZ$Movlx4j?8Cn6E*y9FodnWyqB@ImI6+)1ru>kB8D7qW|}gSBU;!Q~l1$k7vUF z)AQf%u~oOBCokpA7UVW8Pt3_WHC590!4flOvVaMS>qbh6GV{cI=4kKiehx-Xe>-%$ zpoct4Mcqm?QCUy?w+N^z;6o}0TJ@s}ugfs^6H%Vg;L!Wh0a7VAgO|u6Hnq`QR8Fa| z72oH%q;J@XtAtnTmz1;7o>_I@`tB2rH*FJNumB1bS4A&@>;&L2XCGJ7AhA3JEac;} zb&6kgI4p)w;uJ(yCW8PJ5eLv95vkqXy6VR7z1@wg^g02RjJ(ci)=%c&B7ayf0%bLM z7O~7LgOdxG5|)9gg(eYY5#T@PA|At90iMWlqeu;c)u~A#*|^5oZ8zIF=_3-h?cVNH zH-}$-x$#CVl6R$mq#v6SNclo9QF@g-&#;1EGZQjlxyLEcPU6&cxlLXYWJ5`v`O*!p z(!*_=>6j>*^}qnM_1j2y}PIJ2H}Qb{Q6vLp;K|nX)-qA z3yvx2gpQ4rnm#Qn+`+Z(wX1G+PBHuyp&&s&$wia;xMeXkY|>em#iq2T)?sxrFW76Y zXwTvv&Jplx2)5)C0Z@5s6xg zsAlt;N@u=FLm{>4F35WLyE2NJvWU6L4=&RLR3`}#C+S|L4nkrCBBx=8T)TI2)jiKw z4mK{^h6@ioEee;@j0qPcr{Db$Zzr?yq<6;Rxk~7hr9gTk$x^5gL!GTL9}Z(aB&RK$ zaA*KC3b^z56#La`^LSF+axUNA#y)94qH^k-bIN)m8`T=ITzS|?G?sQ#wE519Qd|sW zoj!~cjs!2(!zZTP#};bF!b|=}f6LQh`ntryo1v^i?w5%N0Vuq0_eP+GVi~7tuPn?B z4nW?zT@|`E)BmQ*9jgCbsQyXySmj6fEFIio%VR>m|(?kl;I0ot+;~xnSgqj@I zOz8|wIw0UeWU(gQ<8^J2(qyH@si3;b!*MvV1rj)Lb|au}U3KetBEpv*rbkc;o{XMb z04klDn7@w8qIZMiiUX3C$#MDEf|jC<843VB<8^MNV6Y2D{odB)x)911n`jg(ekQDbb~3E4G=;PtskK?x$Z^ zzFsm8ddNgJkSP+k{P+CNQ?>8H`~RNGg$?F^espyf5Y@VOk5&GRo*Z5^9+|wA*_O>t zNa8({i)YjLUMxS8a)C#Vh)&KWXMtomGt*YxYP0vDu~#3=DPvIm8FHcEee7X4l|d@4 zs;He@W-MSS-l^KwXlwGUaR%FCrR^>I!C9oZylR>M2WD~5`VeH++0gA)?$$^W=6I|# zWZw$JAt%NHD0B5{bke+S?33XE2*GEt=B{8FHgZQ2Q!k$eff;O~OMa&qg-*E6e{+wC z5L(_EYDeDxjvOQc``K)A31{h@->QnKAUq+gX;+-q7XLyt_z!kdCMg8uHWLV~6jfFE=%l_Odk6G6xWH zUKex9#W9(Qq5tfQAim(Ar!oku_8gt>(DxfyiUwFY%&CLiB@RoaHacds7v@_by3vrg zO~S2EI2Mxs@3BgK6CQx8t1nb9t9(!8vDZ=nUi-VJSED!Q9b+HY9@&}syYRq897$}z z_-2yvlRhc^7;F&yC_{gRYps1`)vY?a?;JZnq1<%I6w!e2$>tRuMR$pFJFaNHDCFh? z>*CQ8$+|VA8L7RWXzXPB#A-C!e0c07Gq?b1lISTTlfhD`9+BQ@4v31p9@v4>>_Aw# z5x{qKx2#TDf4X<<9B7OVI*ful6MxDaH7T!RP&C7vLugCE8bKZ@;4I1?5_(moj~?kp zNFWP&q6LBrtj9Ta_ia_8F+{O1J0^D^4I{;#;MHn0@_caYMV(?YNAjW&AzL1rqD^=f zPMkpstqgI@{AFhbHZpl$NDImp52NX}uZu;;P$pE=`Y})rP|} z=%rjt;R?%10kqc$KeDC@ZNVu{*a`8&xvNDK?n%14Qh3L&7(=WnQ0nHsZq3&`k+9bjvmAFM`J?a=kwXG3{6mYsR|N~-}HY;c84?x{)?$%tBATPjnp5nIQ>XfBiiW>9 z@TP?%?+;=(@k4T9TH$Vh(vaXD=P{DX$+{`TGy2cJ=`b|?jUEPoD1$nmm-*KQs;^4 zFAynGix)2J73n|WOqB(ayevcsi;=%r`RTNAwH|jF2>uLq8>_WN%v15>e1K zKIAeb9erYE1@km54JXDKEP$$PQJLI7OkJ4q3^JY~ktH5HGp(NAXII@KvO6_)Qqq1{ zRb0rA*40Cg6m0#b9qAmhcaQ5Vv=Yl$=Zzb67GAgNwvN5K#@yBs(PwY3G08|~WG9Gq z&lUp!;((Q$Rc?zA6x5gL1F4;vnu;<3=~WOr{Kcg_?!Ss~gks1x63d}A1X(L!7ZHnK zYJAB5qK)C6c{-ULOV4X}A6j)M$EC*Daoldv0ELQ~^QkRLu+&Lhl2A3Y1{Nj@c-X3N z-^XO9a)o@_P>D;_ZJW754{%B^$r398J(h?B6>tkx$9yY$M#aX<`)nF}+>#^GD)&!x zLLl{BtM2dEdv;V5CnIC zKOTre)*2u06q*qW5cm>qk!Vhql9F!&m*sX-r^d{qX9Ek6%I&#TH*V}cGd3xmHquRW ziXsQ)W(@7!QHxxxF7zxoV+kMnjCodhDj{45O>P{d+2-ABPxMd!@2koFzoGgY)vaIg z{@?hA+-kbl7@Ip3UM7|D`1}&*B=RN%YO}h{=KVDE4A>{*zBHSE>BWl zO0&y&TkuIJ6GnM-HTp2`8Jjt*?g8m*9fnBP`FHFQ;y~=AU(U1ZAQfv(F_|HBebqbW zXK-^&;ANotYIJ3O+t{fI5fliY#g-RkDo?4Rd=U9vNx!iniDQ<}MI2(6Tv6;Q0Gyu| z%gUTv2d#?>(JPTnqT;D`_r$6@FfTnk_WTj&h85R{#f_KnqFslK7|eU)dwX!8HDRX2I^5($+Enc&RL*9(tXb< z*QLW5zpyxcjMR@fV@owQ0{++E>$_c5ap=B(FjA6rq9^&WU& zTr}-EA|_61(_>RV^q#(v);`5hrdryLgI@L zCn+j8DREqSmB|Z`4_QsUhgRJ^bfq!&5#c763oGsEw{0sf!AsD$FzpF>Yf-{c2L~ng z<%Z)>5=YEN9V^9BMR}In2h3&|9_j>vx*bYV5x#s5q$>m`>t@zbl#&OgIfBacqbY?x zc|3SfYP)eIG(I!aA<@p(7uF@K=7m~039O~RJ~K7}3_H|G(=1t+iaW6@DQqlZWEN^- zFySB<3MapOyL0jI#3PDPA&sx+`+>BA}FbpHZ zSx-|BPu3j};fZ{f90_q2`LRBVc%7;Ph+gB)qmf05+XTuB0!3(9QqN$}yWpulF1s)iC6|f*q$< z_X@4MPmLYf!&`hAx`<+G0h&xKMJ}>=0K%4&4MYwYg_thGV#%{(F^p3{{GNx;w!1H^ zx~J^Yb7Sr)Ys2p%n-+xtvzi1(cv<$19d&;Xo+5>$_&VbW2NZi0+X^QOPJC&Vasj!w z4El>4`^By1N)qQHaC&r!-l-KQ9$=MP8D7FaUaEi`q8a7+azTAHdk`U30%gL|oi}=%xGO>ak z5zqer->B4nw{|_fzklP)E&jhO@9Q>K-DC2&K80|wQNIXuT&M|s$o@31eO?(W5n_^0zbb@5&w7WN~ zM(@%0k3Fh04?3cd!j1q5Q%;aqmb%^C>`~U}4rLf2so+AQNZ7`9#D;?6ii|9o==xR+ zqAZ?EhPy=q<@zX~5)7_#Qzjzt!ShdX#KWs@Gm1%cP%AV!q-9PijiOyEqzqTq(Hmv*m#iHp{ynxP6?tY_>#09Lr&+BJ$nR$>N=eU$bGA z%jsrx7ZpSYaUyignE3D61cd+pNAmwj?VJ5V7w zJ`?wOv;!UVL)*7P&*MrHl4mA96f_!%V?t)pW8?^>luG>f@@bm*7pcxUR7lMy^Q_6uZIzYMJ zQ{&|WF;r5|!GHbYR3z=e!yTUiJ8%jhp|aZUwd)hP2@>wneNcFWVwLpz=#}hPWXKy< zu33nxFyxYef_>%yPl98s{C}4+?zi^q)n4_+%1>WB`8P-vVdb9nz4wnzhB;;1QOaQ`oU+T7+dnyGckq3sn85?MuC{h+O#dwde-e)~^>)2sq zjkYHuOEFZfBSe=@ozK;qp8j&vBuI?}V}jJ2v%1j!*s8nMckdm0oQFb_IET0wsH^L1 zIL}1rPIny6QPhSc$ig^=-M%<=-UOL*$QAY`nRt7<_u*ByqF>oL=2rBvNNdHC#V$BR z42@(X!clxdYV3oflZ;0R)k0UGLwO{;j6NtLUFM4#mO-XlLM2V`RaITGV%FVt3^$kc zY-bLMQUqA!e2M^q#K8mOLhX^>rck8Q-K{PO&%HW+$tK&}j9Nou9#e=3m5EEJ&X3(G zds=Pn6a|YIc(541h$UZa(iXfVjqrrhfb9*d?!3SJ{xNsn-w3vXfY${7WF{I+LHtFb zc_GR=K;U^=m_ugxIDmi{g#I{&PNTtA=^2Hu)dfqTjw>|jHpuc^8Gf>Sxfi`Fxh3?% z(eKV^wwtw$&g_{u(0H|_1lmVJX8xql^w3|_E|cbatb%v<4~Ol#cue1KdTy8JPJzD_*y;8x}@bF@gb2gsj_HMKmE z2@>v2D8SxLJyFdwq6HN4O^2(3xEb2K3y3IEH@cTJgn7{R@ zcK4-K3-6`bv2m?4*0hfD&pDh6zzY5)C*DR(0>w?zqfKyZvX`6X z72zndQ^~~2qpbg9_pk?*&U>^-7XIGXaPTbgbY3W;z`@0A}IB^1j=0INT6?4dImmaW@9Tks|2q6t5DmJp~js~ zohPa9jD#A0i+)Frl+UQp{%q%kqO!aD^lJ3qzhi7)39QfnAQ<426e^c5#3$Mdig#dX zWK;`|2vuGu7A7qvIUL+T(0j*3S9VJ0sQU7v(yLYttc+k~-YXACL=Q~=ANJk_$jiTf6oj*TmZB(#q9iUvQCx_-homTqBtU8fkRWzRfgp(`L5QNL1MFh4#9qK%EI~-7 zfw~XtwY3+LKX~v!;ZR)Y>G#9s>#_lv}I-aC)6OU8Zmt6Y$ z|DX5$zHb-cEi&aK^^QdBp7UMa_j#ZD)t-kG)L?MElNm{Opu8U?49DI)I%w$==cP~b zx$01!)<2|icX~@((RTmD$l)2`c*Aid*5m-7^I8rEw;Jr+C)MuR2OjMRvksY5UvsbL1yTLX?Ev6BP z3z3>74p8*~=@LFtS~VUY-lIHZU0d<~pmr?+R_gz^qIG?9JC9Sq?2@%{ zc-eQD>X>Y*mLn+LltS=u`y~S@(LePp)@PEv0QCop<>D|KPMMMcI!?9Ksv&T1E6MouZH> zw6$QT%cz}X!C@;DjjxJheY-QWAR)G>+MkEFq7(l5$RQn@?DGWf&;f(Gg$ZP8!yV8l zgaKyZn6GxPi3UZPMmKM=I2&V-o>3xZk|zo+FT8HAFTiDT!5hMH;(VfsyYlq}Mv=5HO>bM)VRhKpwcWRew#Kz&qL!0~ zE``$I($eJH2d?Tanff=R)H^{fqFTyv0x|6JfyQMk>dWYpV@AfGAc44vY#>s27-cO} zpxBivmc_`ynYflx7UIYJ!6!xV#b2pZ4^FDEHpo#aU=RKOS8L6C8b5~o|JCm)^}kmy z>pin|myhPQkuL;q<-iF5Q$iEayGrW>NKrbNN=x>EALMGBOQJKtn`=E303@H==Y{0p z-%zXouRYs2O(%mqP5{-sNWR-~Cj2@2I6ObFk-t(HNyl{xMuy!$jpXXqXS}KI$RpF) zm*G@F32h~%B#C4NMuW%2cl@S^9gzSJ`q;Z1-Rz~=+w}wzp^#uCBI7N)tyREHio}W> zFbcxAqgr-yQFaH{AkM^6B`s}3fx7xIOI>e@UMPB}a=Yl@rSVy+n~>4GiV{H>fD%Cn zah57u7`rZEZs68FGlrW-9)8^W$vR$Ec=g1@fv4C;MDxXdQ#Z6SsXrFzv&S18R5N;< zsa`7eKVFzPE|^Q-9#u?)D5g&6glj*)v^C~axOwEjR5$>D3BLPiUYXNy%-wQiHHk6j z@s!+NAVhq`g$FeO?dIeB?09=SR$d?5Q z5*A79d6tZW&gk>eUDR)nU2!zIgMsb;8)~by=K01yX?$N}cm1W>Z}aDS`18%JI|abr zrIGz7Y|P+tuEbafhs-4jv!JBjq~amu7NoZUGGvE)go4TJ=ZtjbB1;Xn$3Q2Kl@Kxl zT`nPV%AE_#a$aE~&u_V3fA7hWhu(rY%zD0^6u}8K;wVQ0^@9(n zP^wWQ4F?L9eL(7L3_Vs8MJ9{lYP=w5J zk~Sme;S?sEkGbp>g^0u{zm2PNLQ@3dJIh<2^1+-Oc>uYBuU;yOT%>Qgy5t^r-JMMY z3h!Kg!wZnc_{yyQq_tIj&zd6XjNVCFEL()6x)ny!C9MaNMfsOG?TkcffTN$}u?-gX_q1OG`MaJU2BgfZ;Wg={I zj@WNnV!0MjFjZ-=gz-2sIs18#@&yC~6{U-xMQ{3>M?UwQ&NU(eIhb1Ubnw>1*0P5~ zI41~L?FwdAR04CgeWkoddtvCBwlyE>DvFspYQe0?AEAp?eNHbUPqjC1bUL0BfhSd9g3l46OLV4;X4MyNT&g+a&AWv zRYyY`6jlgw-HUUb!&|rcH8+fWc1BXwZ~z2wK!>=(p}+@8co8oy=}duuC~NKX#vVWY=L zTD@Q~;R*2~OkJoFXOKm}L!zq+j}lrH1xJe}K)rZra%s!$@_Q#n?mlMR+&*E?g?(BI zK2xQ{qo=RZ5LHodGCO%!G=(nhXRS0!n`z-H1$IN zSQp1(Uan({b`)Hd>@AkDrm0nNKwz$4m{d@wbd|TNS8({WRgsXZ1yh$X9&Y--zV_#8 z&F`oG=NB8z`U-#itN25b=q;jU%&qYg!h@(_1J727^XptLu9B38!VqDSEJq6q(pU%- zO%S8c(szD|Dt{@OrU83ZwwsRtx=^V_CgDA?OmZDh@#uWy8NInSvTAspn&zqaJ$Zdb zwmza=Y2PIb4D-ky~Rbea8IdlYS78~IGs_yA2_S;^yPoi4{P-z^{&8C3uXWrNx}aLP}D8^}ve@MCdh zc3vDO3Cl`n3FYr{fdCNw(8LV%*ET!B4Uyyt$e~@C;^&s%T&nwmI*Vj6WyI1Eg8@drg$w&pCec@XA=h=@*mAS_-o+7`)q^)v z)|%e62P2~}d5_{SrGsAy7)4XT;1L*?*dB-(mBNPozd-(9{pad;))w9~((moxy2+UT z>d2=9Q6LRw?x2|9mJd|mY+$x_KA(bz>r|t@g2K@*mSJ2~aJM4G0P`zt8&OOwKzXM+ z#mEyw(?)y4cq3^cp1Lm*J*0XGYc%#fZpcBgO!k>d#(V88ch=vyb>xo23d$Q0Tmkw37Nf0_;Fq7m9W0>1r*zHae{0}iN|SfCWrvKwaErNwBGb3> ztxP|-!%WP$)8zL|q>Zt&M(D+oE;AgA7+S`y?Ir)uQvk^SyFH;`*$GEPx_@+v6_i*e z{&P|AjJew>ba`IxedlPV7$$h+>$EbtGR|Ot^S(%1(ak)AwWJm)ajR4rB{7X*ae!^s zDk$RQQJqmE7;zS5{RTfR#QnG5 z7I7Zm8W?Ts)Y2L4-1z@AR;=||?+{aV@95#8GvyI%b68)+t>yE;$jp#d8O%qvV(`Gk z$Zc_hPwuepteg$bamidA$ZB}3;!6c`&|Ao*yl5M{?51U|Rw97drp!W;hJs*K+3gl% zr3zAHD~$<*_Kk0}w>*j9%B>@x5CaJLAgeRP^PAqOS!7n~&^rN2vL3VkH_0+G0FM}; zk6R_ro=U7rNA#_spc`04(Fw!0_!EPMn?}b%M^R!znECLcppBmVJ&%Wt<|pK(?10GVgyny|Rck)dc%uIO{PO?fpI5hHCdA7lABX!GLv>0N znbN3N><7_HA6W=m&0u6gbAlYI(i@E|BaaD5TxR}|+k!!tG1MN=@{ z%J$HnWZk*~Nc}0$q4we%w*npX1*S&SzF@>V2!CbEvm!3PIC85O5_ab`O97{nI}klJ zk{+}RBfeK}R)lkyeaaMJhC!~#32!1Q13HQN=*z=}vA~}Y5Z7g8YoBN`BfU9??TT=! zN~?2HLcq#zd#qdu8&zhW$k0x6Eg=u2t7z5;_}=8!4Q9UI82M_U3qlM3Ch#h0=l`5mQuJBp{|kHzD!9(4u{x)UD#^QKJ|cT?R#1 zU`0A!k@S*B+cHh2I$Ai)!zz`hUzt0+eMCGBoMN+7Mp@|+e;d}WEn_C`rvC$m) z@JY$Hg5)cn)N^`E99) z$pK1~UYcO2Us@qoCqA3UsdkQS)%@&RM{d%J-t?g0QEzcfd|MpS%D}VTLAej=iQ%e= zV6B@?n-`>fXpleN$OBSfH3kBp*6hf(%Kp%6w0e~MzrOam`hWcU_09TF?N|8Yd+>97 z@J+++_K_Q>HQZ4!3H_^RbcM*@bt*eS2sg49*b68vWycXTo?#Bvqi_Z zbOhYLLh*~zq)=2njhsixO_RwErw68YI5}_;EHLbsvJ{9#+6e2-uOCZJlzrYNx15vW zpdeN1CMekW;<5q}cqIQ`)E{0qh#@35j0_1NV)`i$xHubNtBM$LB_N)?1aU~bFD3Iu zCZCeE*ksP;OJos|ST^j3R8sYdshafv7+ey%(*L8CPUf2DarMNJ*d!-~D<%I%<7o2yDlv<6Qsp*R)K9Je zPLb^APa@aPzjjf6lyGUm=>);;+b+UBZ+f1E6Momgqd_*>BOhRyC5^pJDx3{cQ3w=a z5ITV`HX;GUH=J63r*IC& zeVjyZxK9#`*%Mr9s64~=a3MF*XXcji+m|+cmomg8Xp7aMtpXZC1?DAz{z{O*4z(U? zBrgXvXb8s50}sL2*f%o6y3$Id1ZZ*b7_Y?M&%jaei3eJ)i`2j;KBqw!Q7~(uMY@#v zMuufTHVoEx0MW_8k`%f(+NVVVPG4Y~soki^71Q zMX&FS4;BQm6YZB%!Sw9h8HpbOqfqlk*G{{E%i6f)f2%fHYyLuWz45b+A86de41h=K zjoRO+{YdS#+UW6tXBPBsZ+{h^STqZOK^1!A_YO|$ z)^6L~jcnoz=+j;fCF1}Bua@#X@Djq5X#p|bJ1}t5`i-^r3&*X?TXKt4!;aKElX5^D z?Nt!7pzz`wF(?*P6oWKw9OY5e!_J(O8?7J;L>67nN28aN1%ijEd)lUCT%;Be+Mn!Z zaBNI2YE>Ds)HQPAWAk!0r!i!m;{!L!2OmzbDr-SSrlP>6*r@48WOQKyr+nz=V{+?j z{FP8c2N9w;j-zwW;A;Zp%kAd{znMhfo}daR)8!!kgc5K^AC;6?p<>{{L9{$yYCk9V zB;lbg>-uXmdcC~hIy_l56~&m3QcXw^VO^a}hS4c$9o&&}3cz)HBA^LiBnZHjkB#QB z9oU*?^3GL?iK6K;*E&xP+#|g=)OIa>MFnE%90dvmx*E(Ejv979ws5pMe64JhWIgGT z!ApYNrS@@gH|>1vpt^hR15nYr4Aa+HAz)%$Kw+GQ20HE9^gmRasWpGPx!U|lC;*Kj?#1NL1z^xahW~Xng)zn zp)xGoc6}d+rM~N(BLny1?d@*6RQ{rT^O51U^R>}aNur^$P^6Itxcvx@B-KpspIxLf`?8>LaSO*LIhfgt2a>nR`zwK2GLh{sQoxQiv}so zy%8yZi(-Lro0M*VvKJ&ahBn7=cSI3nCq8@$m^uF6svW8||GVZJ%})>k_{qle%mI46 z{{Gre)xORbZ~Jrq;8>vCVV&L%1rwyU$`aBBp-n)N|FOiq4-B5teM{}f3OFP*ii}oZ zp~)(m2Yv7iOagY8?tEr&)QERm`_aT-a)D)%JnoC4*vB{)ApI9li`^yeM`9QDD;M$$ zFelzjg3G?n(!kATdnej1>jTN6EZd@6M~V|LG+ZTnWII4{8LP-#K{5<(WhZSoI7dDV zTpIvzcIq#~`>{5_5%J$n@0Ni(%5L1;#eE9X#bLX)i&;c&GYcp`=ywiwSbzejUF{5HP zYV2Be+%;BadjiL^92uy4vz^BW(b;iV`+)lKBj%Dc+rA8EzmpZFXG(iX6BsHw?*oI! zf|mAIzzq&tKXc?0h4w=1(shoDc2m%3RQmt!+C8=AmF8y~f2;8yHeP2A_|Mk&Qw4B^ zkFWZ9X5ik8z1!Lk6=xvG7R4K3-2f$9nWxs*+}>q;^qA1B0(Oz0S;M}`6NBh|cvt(u zZO=40Q8SOCE1S+>)H7C3`+#&5cGV=PWe5u$F+=J3J9%>8rbQTk4{Qfa1^t|Sk;hAj ze+$1|S_$EXItlrtmy_YA<$#O?3%+PBwBG)FvFby1se7Z5)RCX-jyTwLv}!gEin3K! zZ^wuFdCAv|B|kSgdaSgFsPV$V!Q*1N)s;?uWe}Z-?r+~;+1jC4a`y}(5+{im>ULtQ zK0}+LPu+_8V7QZk*yKeI1?;>ua8sh*1MSa^O|4u^%Z7APH!uKYnju1yc)KN;^M_nO zS2pcp0X`D1N$&(RI{OBX2xFgV-)9ys>zRkUscV{OhYvLPKNj7?y7pxIv%VIXDJ8%G zZcu>R_HD?5IDTy4o`}6$+xN_@(5hJ0H`pmm$y~$_Q1zsW8tpgX38H%C^@>GCx}RRd z+B%m8Ze7<~Y2U34Lbn98)agQw0vWa_Jj>Vt>N()2gq9&*k17=sQhgj)26_jPRpoHO z|MV{#_q$1_R>7sJ25BOWu4MN|^#5$tmTS$QX)ZSJZ?+msjfwhi)vweatNl}E0W4p| z0KTgaCI=7dB;ROv@(g#WC2VMqCri&JrUYMruv7_SJ5vKMRB#J+8x6n!;SwsL@{z6ibN;Uk+y$lgO#3t1Q;M+{=qYsr z4kXta2o*hRqH~EWx>6>tnV{a6SrsaiVdBT-YLU6|hX(g)v9GrGO=b0SMx2!Yq362= zs89`MhkBoNqNg?bwkwz@& z^Jye^18zHc3k{UB$XOx2sPr6tdT@`n_hkFiGbx8ECoh#}uCd^q&kgR@h4bw@ie1de z!qCzCtmF=3IbrhT+UeP;fm<(N_J69luh{*wsUdHb^ibE+GMTy=bLlr|G(ZiS^wMh@6>1O zBefsmqU!;Hv%Jx7vG)hnXFV zw_>b_r)5tpdcY-@T$|_8tbzBaBupAvi9p#aB##5TKCkYHtIE?=15Q3T#jxgcSXlB3 z@y*=q-e(3bvPZvB1R#XivC(PlyEWQRN>0cD!#OZg$VfN~DMT`|Q^5*YfZHI95zc-<_2r#g{l*{) zt=HP5GvbgEhEBsqpj_KFO0MW4Z$e3w|LiQJYR{;?;&C8ngeb-ltkOd$28awanvG#bIvrm4#&u&asdUmcVpwPG@h#e ze*HhFKVRQd`)~O8yZm`^;A%HCF(nmg@mm4;((?n5|dn__xv_3zn;+FmM zVlk1x*?D;IVPVe2b}O}|)SB)5$1i4M-OfY+&T5t2mYzxeDW=u_=|NPEwcERed4#7p z6dRhX11VYEc3~4mv5Ph$1a;8@bl7=r5EWu~wm-lc(pg$E(wQ)Km)Tw^SaK3nz-oeN zWzU^w2Cm0~)9n&co&d>wKKdmx+YK~I2jx3VopQvu%T-9=tJ&<4b#Q5+yWCkDxJs&b zy8V8xau7vK(Sa15K;mB3Rf;I+4l{p42%t`X`z)OK{AMWoV* zV}OI_`5_iFT3?2a2sM`jAQp2=RRYH%IV5?ax@HpU zdJ6-CmGf{C1!e#5s=Zum{)OhD=KC8z)A&OD*XpNgKgUOZ7(Xuz-0c8I`5qyexKTKk zx1rk!{APJLmM(D)n7yTe7wk}|CV)>bv!MkCfIc;F4}p!t?GGm~7~ymk!7QbRU{)_6 zxM3UdoX{_$#}zUcsGU}5w1_~^?kXn?v62hoGzT>EA%GY%v@r^}yWYC0*?!*iiUYY%>k&D&@esGBb)qGD+UU747m zoKi<6-A`ev&dUQgE$AI+e<wwD6Fz>78b`B2d?t(J=b>9X=2c3WtI+#h;nXz zo`U=E00oZ=k)ce~l$!&qM{ZS4Rsl3uz(6?PJ zh7@W*Udg|a)oL(Lhgu2&9-!geDjK+8+_3sBD2U4)U8iN&l0c*?uU)-8?Slku(51S( z|JknBz#|0(PDy!eiTRg}<+>1A4%j~n=7K(*-sc9c3)+|){#=b)j4PuwdB`PB&5Fqn4m}WHy0#>^*G}Ju93T$FD`Wc5XBE?)_IW%y& zPp>|#-YPhwFimsDklR-0)kbeKj$Nc-4Nh0fIkh%^WZ(jt-tOUhZMA_Q9?BwAQ!$VH z)c78}WfkQRX46!*tS1ZB)V`{1Ia#PtZ*pSzo@3z^&<)voPJAhQVYmsMz?N5~t%6=! zaTe&$oJT(I}R;3FU!+Vo~1B)K^bdW zN5rTs|F5h4nHt$YzYFbunqU7F`02FHaDpUhX z&+|jd#0E2$!ab>aJs})oD4EKN31EB#5=BNWH0UQ%Vk;&Yi9;N-u zSUFv1l^J}}HK2c?s$He~9RItncDB~MpQ!)WYrn>?@5#@reHT#lULLl?(rh^edXkr| z@Pr;!LiAdfYzuThn?hFjlWMNUFz!>qVF_)PFwdk-S&4TE45y$>LwenQv>$~D_YQwj z1Tck234n>7L;;{4_@iE#{56$BS=TPRN>wXd7`&MqG0t?}JIb!m1H^!mB1mPCh{gFi z;!|K1MGY6ox!u!`Dula+_iA0F2NnKAm3P9saMGhV!X7|xmX+&&vSuG# z(K6i(;u@FeS)*4Kt#md%)^|<9mHUU&X4wzWJApEmyUv@?RS>!G=q>hcDazwRuUe)2CUZ0Z)m1izWG6Jwaixsa zw+B^pCpgeM(s!lA<=w-#%_z77?w(?zmXtCnthp$X*mC zi8CaHT4l8SzrJ>))_kFH4}Sl>wf~eq-u=&4`%$>~(C|K4At4JMkIYC#G=w2gs%BiN zuuOjz$!DS9A=#?ZC}pXNkW_xxd95Gyf(yec^N#16QSk6>UaOP9B~G`Ck6P4 zSwOjM;$tA=WSr}|N& zczF1Z?F&kw1x#E@R0#P=nzE5?Fy{quKmzjXix)_|QV|a6SBns-hWUlkETsU43I%SO z$3u6!;(WG~bV*s;kNg0yW+&0B(5pzoSBrr_w?E|)@8z?ca-EY5d8C~jhO%f8ih{@z zb7C8BV&U=d<#V_ZnSIia(#6BWpE{ZmR_A7xWI`8|U_K5+@vm*ih;&ARKb(Y}q+V{k zT*w8EB)P)rctcT971v@YORo|;0QeK5$E^u?5As9?J;G!w%@B+Fza?^i&(wbk3*hg) z+xowc_oGznfnj%5@ebwbVE9+0*NH+yXl@67j>A@Fn2RZ^6nFHv+A$qwdUDRZv;rw{ zjg=dj>5G69NlrIo{3noUlh0{Eueg`#>N>BR|i8|xGPT$2-8z+Zv4X`Rv zmO=8NKjgJ!DTH-H%VfSrsj_OD$lM97JX3IrmNeoS>B}jkiF3MwgT`y*U$v|$G#b|K zOsl}cxN3GKWgjRZH^4~G zUSdhQR4jTjp>CcSC2*CC4=X2=@0&1&qVTlcSD96PXd`zbk2oA2gWJeRCJ+x_m4WMZSUoJ-<4$F{?@SL4ab?VK{q@)hmqn_ zxS7&$Tk8Z)yd>O$I8O5`=f2E~!yJw3513vMoMf(J zK9#|svTYm_Ft44qfz7+JRbc4CMJ*^0K-lDXpGbs2V!+TMIXM0lZ_UNAdX9}9wI3AM zjpR0kr+An@lzfteo31!)6F(`m6Ryq5>)L6$bc^XJQ8zqSau}4>Qp!o%Hm+jhXx}Aw z-`+EP^Nf`HF50&ZckYE~Qp`eZRA*|JuS9;vG5d*?#A*0LOZkp%7f>Cyx{MU3vkMFi zET}*)31myrw0jT*y#{8Iq6D};qKzCU>JvvW$-7eR6E?-U1vaQ9JWlnM+(~*3Ly?9fFkFX;JHpeTyF9M;f@Z#h~p~v zgt1JL|L?2aTWkDh^#A-Izr5W~@1_1}(eLF4hTW5)gl*}j7bk~)D~C}!bM}uu9}Ybh zWl2qNO};c)|^Zc6!Twm*QPMIjpt|V0;PW z?eJHJA`AjBL_VX)lv=|tr}kMQWPO@&G2A1ey!iM~ESItv7urZaOWAytcqkjiQ3D%^ z{AbQm4OLc2TUAo5k1+XIx64?+b;qVlyKu*j_ijAh|C&zb+aDkPz%_TgfAmrK5m3`s zF(@hX-&R@R*O`^(GD2-hP`;)o!;_okpOiQjf|{dp%8IirLzMsD` z5Lr|iomFT*g~KAf?BtuAjX46|gm7WP`q`SM&WgQ&RY59t4(j!(=_ua@PfhNIL?Cf6 zwuT~bl}^X>e?#q0)taw2Z*Kf@%{O)*VN1rJ-#wX^KHFXVdq&6PmWH5@4=i zF@R0;WXKyZ>$1CY{A>L=Kj@KR_kl8S))Ye0ico*Xykb61M-?HQ*a2TbTcmbluP{*= zcf68sa31S!SY-4SJJ5Syf7T$pZrG`DXb-A8lALgjk6fL>bBKSt+oZ^!>s#gc-K^yB}iy$-MI2WNjp12Wk^RON@6KWM$mru`(h_4z?z?0>7ra~>5f@yu8X7g zv;w~q1xjClr{wR(Me*RGp*#xLQ&NMh4F?mj!;DpB(CrYL+iJm*&!;+PAY>&sD`031 zfZss^ygr16a*HrNAe4CFY|82gE-%d(9bzZ7TQf#p*}Wu9T7ku)Vk@>KKL@u;fP(}G zXD+!)wgnlxsE_0sp5(w9D#bVZQEYx`*j;^&!gr$PLJY=nQ7#rF5p-~t$ZAT+W&>M> z2yf&C%jYD@Bfl-+na~#WJ9uhsWs#vfRC+PNH~TPAvuQ5af61uc9Cj-o>q-`x_JHzq zyCr2K!EjlO<*HxDyQV{9Dr7C{%iIAonF!MeDm#`2)}LQjGfAf-ur@VO>TGcrUHGY}-pSOIs^#5x1-nO7FIF{8$Jy%zw&MifXJL932ycbjyb8i;g2R`LA1;~y%+jl7BOC)7|zL~ zk)7nNOwRSP4$iNW@47z4Aa3F62%uh)`tdW(Knl04-_eYa)UxNo=#YfJeGYIy4F zul9F}muL?mS*HMN#pO9WcZ#J~T+@1w^ls!*>_FsKX{m-&kxq9jgy{E2X_Q#g8J>|` zq`e#aFPIPBHr((7(wMLl0svNz5=6Y+qRAl}O^mo|pGf6TT@cF)%&9x(;}=)T1oMi# z!9p|k$rw?G5bI27{_l9Ld8F~f^vl3S>c?b8R^iCHj3>? zVFWfTL~02E0Qq&o9xZ@~4;N{@uHda3KSGdZ9l3K2TDhJFL+|va`fkW^d1&Y}CHh-* zBwdS*FXi;9kfB{Ry z>wHF$zlpdSnemj4T(o5NL}+G_$j%fptkWCsyD7-Vb3^;^l#702hMDBFbRpD#95>U}`T%8PwSNpipWPV7m(Nm?%CnpXnD0`5B7;#r0vNgZz#K*!d%-y^XpqZia zm()h6C7q)7MsSJ(8V-$4LwcsyWy@=2`L(%AF+VloQf9^-wa5w9S!QvD|L@0Zjo)t! z;s5*bH>dtTqHmV_(TL^b(4BE|(S9Z|%F;d;6%-+1oh;or05K;ftOHCYI|Xsc^U)~+ zSB_+THNtc_ix-v!wodP2{}~UHG109?IW?eIvg*l6^R>71|BhdveT zvUGT|uwtN~UNXQ=`a(gzpi%*zV%4F~B?82}!8I6gmrh_2qQR?~FpsA>5W!?O<04A{HnE7J=eUr_TU;7f&Md|%bY~9@ZvgD}4jD7v z~S(eBlxtcG!S&3NB|AyxHPk;6xI|(2r>XRshAKhK8DcS z7w9iWUWBNEHIxE6FlMUMfy9=pJ`viqxE&x*l+;zU!-fHgeV%`c=ZO#W6)%V5+3QNi zlbXY^DLWul;3TyRuWB}tGDeh#ZBb!xi*vXX)?7~Di*60-&1QYgDpo)t+g90}lK43* zl_~RnIn)(hH2pW~r)!N7{`1E_?*#;U`})!9>)4PdS0s`v#Co(N3x0!c%Q36=7xrIj z?ReFMOYQ?ZjWbz7M5+ujw#drQg|Z;SK`x~iAs0w)EPS13aq>xrl?FX#3y^|ZNNt<9 zZ13g8{uAORKlt3xZHiF~!Ik2q9GeB@>j{;*bQe3V?&VTZqNq~%Sk_PQu?i=w#OfPc zr@EM^9F8q4Q2Ok-s5lrFJcM`gz3In2YoLZc&)S_!2fVVEnBj)9!}WR-M8G1G z2=xfTa6o4`<)kPZCSnHU)D@w4X^LuJJEbQ`!?IS90`y%>rKIhV5)}USYr&sh2@7iw z9u~l9Qj4UDdI`!*T2;(dM*$!LZm^x4->;TNh8Qrn%ip)kf5IKG2;2lV>{SiKPQ!yQ zz7;f|O)d?as}=$|XQW(2mUSpm*w?22>uO)9HE(as*ZqDQg z!Wdez(G7#8(hxQDlx`|`2Av~?**q&Mu%Z-NrVWavHUZWu&Y8SSRwu(Jfc2s>hpcRg ztrglB3-AMqG=SjRpkg6ZksDxc$zml>!vl1Po&*@$xLuR@01?EkBY#tZyf79M(5-SK zJ{-fA7$J;L<_@&VO_3uYW#B5^E3)@HC;L+(w%*-C9}g8&LNBAJZsc+1^7}`hAQ6(a zi2Q^=A%~S&E;~f{v2S}CWT&^Qf5du0dq|VT0xap-5jx>zaF3K7Yp9`=7jjM!zdRv1 zBUxwk`G`{`fL-evtvNl@vrY8Oy}bwf?xeS|G{kVQOpN6j;B7=&#m*v5Pab-yzFFfi=4p^SQW_j0jxtcaXj8@;%-= z*>{`1%XbfbG@g&6Li08us9*Ye>3H$FuSsOO}m&CTq`NKXlD$0d*o08-l{U_n5CL=a{xM-=)#naRiEoa z6=cx%9+m52KUyfB8~O-+w6s3V4Cfe1JQ^hx4$2Y&Wfgy>SFX7HK?0)Uj?x?{dP5a{ z7hbhb_YavROb_kJ`I8{no6P3ymTl)VX=HYk&{BFUpks9zi6y3pkT0>W>Sw6BPAQPH z(!*4tXbz&};HKxW0n`R&yf@!>=f=xV4t+ScWiOy0;PXTh=AER;JH-Xi0Uo8-AGBsI zTi_R;U!@8iYSlea(cz`ONdXSgWl8~p&9E;rrqwJ%rRI8^de*4?#=nC|mbe}vXTnmt zs{Nrl@^^eF;3i1o%5?Ern4lBw`i2;>`|Ihya z_fhQM_)zWtt<6T&e@WJl_M^#UbLc~PZH4}Dp0+K8_7QIhk_?4QDpZi=fnK}sW|AAX z4&5|e*mlyu#6r_HAxX&RYEMeA`l4l0A~7fEL<*b4f!H?RAP$$w*FFRjv>Z{n2l&0c zNFI0U?17D2`i}_eSMDDA;NuplnXTmV(hwyemE-8~@HKKNb@(BKuew7Um%mtd;I}vD zmYE!(b?=jJtDGaiCJKrK0Dg;2`JhjtC!vf9^9v!-I%5G0*{aXS@Jzrshx?>7e zD&Aa_@}Q%TkK{1}@~J|i_S5tB)R&}KkS?UePg8w_2xF|uUX%4zH3uN0N_j7O!kHD^ zViOq?3g!)va2YIBim%Cfw|fqDAU=;3qL3s82X25|cSfsrhDoc*H}53(N6&UHq`48m zonSYF%R@Betbd0!%Y=N;N;8ij($#e_HA`@qA>A(`$jK%PWzFwUGJdGFdac|AT(Cxm z#e&;V0E+JD&Ym)G)FGwVx_-X(3nbLu)R#5&1vO+QYTR z-)xNW&%62oPuyq~JQ+cdZPWKP6w@^69FL^MRHK+$h|R*u(B2I0q;}6Z`AHlQ0v@w@ zK=-F|+PVoaxpx^-Wss@SCIW;U`J^F7=DN%~?1@|lNjpn4dLeMxe|f7k~I{8a|Z`d#O{kL9KW|eNK$#;mPAS`4;0ESDRZf60-~`F(3oeS*O@Le zF$q37lTfh?xiI-m<*cc?uX-EAf;UMPlOGxjC;!P#uvi9**E1j0i`WbA?V^&{rv?Ot z!c(?(bkVV$-6}f7LEZLtISQUbI6gpLf`iEEF6+9w$F5@a~Gc=*g%JzjeULh)4g(RX!yD82v56b z^8`Gjwi5CX3-?Z}k{i7GGNW4T4+93X!|n;Lh+=(cl3i1vE98QZ$BO6cT2rXH?HUxa z`{&ec2N+2uKkEdFNEu6ohrv~Gd`hQD!_>f1KO=n zSvl}nD4maJ3g96eOc6|5!0-%CpEOsom+nOyX14nwn#C>^tv+7xBR}%g%3@N1*Ot)c zcy_kFtge(v^%cJahlT3^#|e1u1V0jJR&W^ywi_F(lKZV^2iCUb96>uejy$p$zGON@3VE=xh-p1ac50E}1y(A_x@La$wz^HIL zK^=dHIGQ+I`f_|jraBMwyJ#HkTb^80}ca4wc@gDoj*&0b7Z_x_j&<&!k}i-5a=-K zEyRFWFhj{2N!k>HKKa|=&|WDP0WwLT`{`zl*>_=%iiAnSXdy#O_XwxRJqk!U0_TQM zZ_DX~@}ON`Ju);o;eMz8EBf4-W~|txoxe%L zJAW%pS;s61!HD1>KH)Z!In|o;njKB^E?ktKT*dLooh91c#iP&x7a4dF&TF=oF)7d? zGoPpkkgH5M4Ord*5ZqEs{#Q*F-5Bpr7#*G)iY&GYqcPi12|~d*G;2|>R%w@prYVB3 zwtimVX15#_%@S<1hXOrAb62SzLIoH?>XpxB+(giTPxbpX=jWpRNdnU7!-}varc&K_ z7kSwr5Lm-NCctUqAVVh{vHl#|D|&TdRS)~ zE!pXPsDGas>>Wez53at$#UK?rd`xFdEC3=b&v;D%Wvzis>QnG8lG;~KCB&M@=eTR-e8lR{<&}B#y{Fk3bwGCD0&Z<*2@YNwZc)eDi(bi?V z_KV3RaGh{)ik;7Zo2oq-q*<<~vvE`Z9`mi+hdfRF8w?~NtYRdSq zD3(FXUJ*Hx7qe7<@^XIT0|4WduF<0aHwMNR+#z`3KCObNSEVcrqf9Ov-OG(vC6sbK zAM;un&h#?TwH9)xyQ^d-;sVGQEYC6MDP5tbIZ&j;ULqxO$%ZU+3k>g#SA`QAEko}R z`%n7+YOQ&;F$(>!UM>Isus;Fw+~)uCiPp+=-V3&ktAmm>^6ErHw2vc|JR<^NW>eeT zc?909=aSH{K<1-4eLPB>uPgmotq{VtP7s-UowrHsjup9GQNbm}%`$*g!q>s*)Sa2z zN*WK83oLmvm`ep? z;{9C7*>PT6qW zKhYo6q5Z%^ty$p-6q|H$ z9iC#Uaw-QyMzQ8I0o&y20UZ|F6=S&YkOhtMxTp<0CM<|Zni_irVu(LTb6v%>NIrW` zj|&ycalRTo#yU#hv5VC)OdCQNi1JFuJEkPeg-Q4TG+%5~ZKhN~fbWVXS)Oz5;iWR3 z;Ft+Pn2;o{tL^x93{DrY%7sv{2w0YSUPQ1p+My`e5!M*FE`+allqs%bu%9wOL=i&KFjGs_DN~pZ*F4%jzAMMp2ZJlDNaUc0sF5gv4Nmzyx zxdFDhkYE=ls4d6-xiM|+@oSG8>wKjjE!OXEb!Q|8GR9zU7btUZ$J=;|zII0fF)noy z(7#fY&{ra?l!^XtsEyZ{{Wo6!zw3XherN5k^5->w=ve8AG8=cb&be)=A`Y`T4d;eVFoA)|q*YPRj|W)Y!=4 zNo`UmU+TxOnFm_02oJKwqqTtog}EaE66{sp0nXJvB5$L6LDj|bz7_uZG6$q{rhkVR zL2tUXFhy_+l~it?tft2i!m5*ex~8vNOGlUU%M`%(BzPt9DcBY5W0*hmVm~I%+}AoA zTK(ENX71mqxf)Zj8 zqV_+^24fDAf+&Wag}1at35`OL`BeXtmSKlmXEK*c{36deOn*Ly(6jj0j!_$u{7>*f zV<-y0WZps-C@7&9NAwXXKBcIIOM|Xvg55COFJvMZD2G$SRqblW32C~6IYJ5 z=FD0}Z>eLVEE^XXn;GKYO*J`q>y-d3zX>s%R4?y~F9d+$2Rp77V6SG-Xo?aF6V*VA zuDm8rq(!N83{LoTS+I~Ns$`6E{!iX7PK>0N!2T5~V{J%$)d(>xm=^e>fw?o7a;fk| zi7e?dEykw894@3p{~xP0M;bpt_m7{f@2mY!wT}h)_dehMn9qBm^);TgTv1$YIon{L zUsUF%yK5=(othLbs{apy2+|j;4HTC!kw`FarbPu|H7)@8m0)f*AI$BzZk{&knKy^u z8bx?I)sv}&Imp=b8-#_@C3|1DO+MYf)#Uo|*2`nym%RB1Fui|)TJLzvp;Rby)wmWg zOa)c$;CYHR$+jJsXVs564{x--YD-XX#3t*K)Qw&)rcoegh;k%nG(w_4&;={j!lawV zMEFE2N}7NR@K+X6i7wfYa}PLO=2oC_c4!*m;SU?vwO%@+ZuH#8VpV9c01EL4@ITyv z8gLfy2eD#3Fk78NxQ#{Ismu6SSEJ>zfP5m6;Q$LZPVryAMNHd1Em=3T^NX=gr(0i{ z%978DFA8r;KVeb0nD>I83Cvj>>M@0C5{y%Y_l)#R12(k{AdnPx=`A2G##QH3KgK;B zY`q9JW=U8cW76USF|4(~Oh}lJ-+{vdE~lVx3q$EOSr8@{1;SGDu(#O%uu$jnbFD8| z-ZG7@uFS~U81XBPg*73%jYD&*puj7ia(tQymR<5c>ppfzV}va(c4gKViWzbGXebbB z0jOnbMK+mnBU>7NVsww5qriPwf{+>jg*#+t;U zVhErJ)60pKxnMSXEsUhz*N$spw$NM=g_d4t7IMBHlfqtVJzt=}w7f1Nd^W%x#6(hW zRHBOOYN73_oYp_^M{t6;O4fv z@>(L)7uh;!Nl#JDv*Y0X6mf=ae2nBpcCeS0q#iG}v8Od79OWpLZp@ud3CgBbjCBZp za&WQBQ38^A=k*GOVi_QZB^t$Nm!NCwK!`#m8&^_&abV-AW>t_CZ?h+e0E}QOT`A&! zmurn5t^c$757jQe*Q9@q=M!LTi{)Oqu65jFflH0gnt~4;q{kiuRkf@d_=lgJ!Cf3-!I^R26MMz0=OG^2y zC7PTL!1TaEG==rh+35B?8|vGWt*14>G;3Wjhr(A1jg3)h1SZqMNBIS;SqHeB<$fBd zNgNQ8l(9iLW0Cu_r$bGV7iid##g7&wm7@v*dkoO>?06;^Y9^N2=tw~~5};iv%h2%h z12EL0TWc~3J=Bo(X5I@Sd+eyYf}*IXs4Ke<3>NkNE_%kWuW);USL8du4iYWM#Q+O( zt+&`uVB{;z_qAeRNmO;A97=oF1mL;c|LafI8sDk^xq7?yC$B-D-=mLu^}fb-)qCG+ z9h=HT9J>Jy5t@SHgvRh`inCnV7V?Fo4?PvFiQi-+OWPO1;&X%dUby7y9e72Qju#J+ z=eunF+39__AG5ypwVnc3b|?kG(&He41UMCAe^)+aTms5|iG#7wcjR123!FPV3Nu)( zdGr@6F32*5y9srZ2RkdjtMvA-e*p`GUa7FV)oS936j+qOy>~uP0CBj~OpBzAEsA4!NL(6Uu zwVxD!lJ=v=#X*Ypg_iP_N2egp!@5ICdMix1F8qu3v>Pw=J!S0M5466-u2~~ycx*rk zq4Q8BU}Fyi$I1*VB*m%jyj=38s*2hOmGVsrpl~DDLEjJ#ja^#!UUjW_21h4#^aLAH z_e6C*;K7NK4G%;H7-{C=&;?d`z%#3>nrBf>ht|Q)vzo;&&d=H;Pq~5JyBW1LCDWFrs0~l>aufH_HAQz zK@TAJ;sQ-IC%b*mr|V6&rYlb?)n#|3P#sAiq^L3|RXi3`Km*j(6Wi*cVZ}II-v~ zIkBHrdU>R%O+1W*! zlPP_Zt?@`XeYILl7Oh#jS^!{`m##9nUiu#k74TCV>U02mL;q*~_w|hr*Z$zUpZL9< zaC*1&J!Wm=?$+S~9xPblY@EDc$6Lf_GV8SDv2vF)F|?iOlJ9Hvy!biJ8-(x;N~ zBc#+BS1PR+W;+{u`Ze1fceNhdzBr{3$ZM3a7jM6EYC<-#`qB`Qhfi}*W&1%wXeVXa zcr<#&u^(EXPo6Nmoo!&XxxIRb*d&W&g=Kk{VBAl1Pp_QPNlO^VYe9IMIcr!VYnluj ze!4{yb>|=(d>e%m2}Md0(S{R_WU-Ogjxi+Q1jJh z2m?duIgv-0^a$BfJaE~aJK#0jORj@1Sj7}-p;~aEOo&XR{6*r&j?Utp-fNp4FLimL zb!aN@HFX}MTDawU>JBQ3#X0KSCVR6$45ITeI*q~!8$8cR=cNg4-kE2<_wc5Nf^Dp~CXcHHF{cSs5|_{T9Y>$c zR)3v&O7&=GfM$jxi-?RTJMD3znhx#kKqapn3vUa+s$yp4l+$u~Lw*pp7>P8B9w*|9 zb2ULzV%X_@ax;c?9c+Ce0UVM7aDpSi!}CW#lp<|SFTVx~2em_og=kiKMRI(2r&^b} ztN48Z9(d&r;1mvJ?lzlnAjbT$^Xz7f(A?X41R!O?0F`~A0=TT0baEp{Vax=)RiYjc z^2G@pOcIUwKlA?{Z~Vi?WA(pRU#tRa_Al^32&)3?>bWV=VV(%+H)_9Vi$QXa4rORw>R7 z*=L_tw%vAS3FJ^3jQTZM^%N*#q_Ym~$RM2#&wk2z&zlgJa3DxnXloaqfX07eZE|JP zvo(7!w)Sh;p^+l&b7G8DH*0n&79PAc=U5arZOT{S`7{%jgIB2zL?8G?xW-4qZ`Rp) zY17jxdk?f8(i^p;Y8x`;0Ht`c$Uc}tB#e}CxST|xjinA|IVQ!dyUiE?cewSSjb%$G z5HqF{ftAVSkffrl0b9*dTeKP{tV;0->HsnLkfzCa50WM9+8{oTQ<8r2`yEFBAigaZwG`!!&f#CGOoxx6UbshFjlCTO1=i4sUS1Fp3HC7zc*hh`oxlednhFm|W6Z_`8XHjcGEH#Q@lm#82P z4emf{k9!Ho_qBk;Fj8=;V?0^9{66xGlGsT20&S{xHFQz2AdWfwci9$X-HEw}l+T@R zf?t6#%Tg>MH6eR?oN8u2yy;PSSN65;n_>m}RtUV$zv?CE@#4&hBM=sG`b|!zutFA1 zX@#QNEle_Yh~i>jK*nO^?ft*ZSzf zTkOO!TiX3O(q%$mvlnFi<_R=clo~7XWt2F zSLjxYEHb=^a4dGolKda+Yul7bw9y`u|;Dd#cv_PUD}||3Ups?N|8YU;5{b%_Vc#i>-TSzyZKp zba*9sND=VRXFv{`lw?JyD7e!FcBa7lF~K#ofR5_tBjBE|hVknx5-SvoV%kDZ-+O?TAVW3Hi`w|tw$35h$kO^>bXz0taRDzojA{9^rQ4Xlkd#d0{|^)>nrMEKIn zDE#6GYFuCu?Lk^5iOMd3HEZ6!rs6I#j5-_b%~$O1xV6pjF{dQLg>Bs;!p2AKS zX}CQ%i-$j`cP}0$OPow5asccdJxussaThTP&Q7Qq`$7p7JAq~m>G+UM$Z~@nu{?N9 z$fe>Q20n30J|AGKiG02wvOmWBh<&Joxj)>rnPnJy!O!*jDCK+;WxUh-#AXbSdZ;xa z+UAB#{?u5Co}{5*Iam`3W|1cB6&)6HdVm~21!rHv4Wb{9E;yb*U7uNSRr1qng6 zzfZf2`;z>Bq}KR9>%UdMsrJabBK_X8o1O!8d2j13y)&(e4gaoEALRA*Z+Zq;%d4_{*q-X0b$hU{iQ6qUN*!j{rocSLxR; zM`qPf9zC_7l5xKVrcj3DNVLHcuyMi0^+d}*hq+ZM3_PPrLc#JBuv=Jub={+X_H26C z)|I+ja8*typEj%mCCxEgk`4B(vz1d0Vs6` zCi#8d7_U7Qx!p;#ivFMP!|mVrwfaxje(Dd~|9^bbvqF2fx9&8qT?5a6_ACKaq_EJr zG(SO{fT#}pn2N#=E=I$P$=(~ArzFWPUu?NUzoy_iBr7BaP`U#O^u9%9kbQVT(6J9P z4w;EzKELU;Zuhi*v%?)lF=u{>?d!ZG#9-k?X+ZeT`7+fA=v%L{1bt!-^g~bS`V?VBF`dF9d89aCAI5LTU0KdpiU zgvqWz86BfINwYOV-_Vn+hxwI4E8#}YJ-GlcNVv9BPL0~V8aIQQ`Su-~FO;CHcN&k2 zH2N@}iZfeXbF6&lneD8iv`~Yo1vEu{+Fg-XDE#4DJ*9AFbT&peJumdiBdt$~k&Dx@ zTco0Z>If9)m)K-Bs}Uf<^LSQH4#KdM>@sSxI|2hjfs{gLHW)-5x=Gdou5l0fTC48s z;YOywM|@45AO0KWW-`oz4VJo*tct20k+iEqb~#5qd^RWnWY!B5NyLcXd6f1TYu|py zzU2SkQ)>+B|8?!3*M6k7#~Q#lH$BC#cd2!|AcxH1nF4?x1k?;2BQvm^&}o6iN!dqF zaq7GDH=T&OVnz0X1jPayp~bEmyz!icr38NoR*_sxrRbdAjHz{xwmykRS@(U(HaPmA zWTU{3oQw1{4|GuA%)rC6L4LrxlM58KC`AN1;jSs}S2M3CWeVAc7^CUtT6?)5PR^W4 zZ`(^nhRfi%(LM0#QY~?Y(qKMp{10kloAmrbRD)WfwxD1jYN(czJ|~L1=JHSrGTl*) zM<=YLCmOuM@|2^WxC-=5$KSJ^-prK>x zz$%<|XOV-TFaW_qQ<*+>VS*H7AvC3OLEVG&fEfOv90p}|B_$}%1WiwV2VG^YP|pdY z2*$@G7H?Xeoue_yEoe*Jvy_xR&o{`BtM{IcZn z#>=fw9Aq1wLYUbxt-5olVg?}FQG>VJ&KqzQ--gT-PFBnGWa)`hw7QmJzPLD{dJE)o z)LjSgjveZ^jU~U9FG7 z*o@#pVWS0mb0LpgntnILF3`;1#tW?F$Ftv^oJz5VV_q3-8uA{i>JsTDx)7Hs%d!xbxFQz}@X%`%M5uss+fnKo z?$lf-F-}$G!rJudlDe)q9Lx89BHm7Zi<7sDwP3G0s|ez#Di1_LENEW_pQ%C#XXcR7 zjgyeId`**F^n#^H`Twu0Jy>i0872VzV>|#4zBA(Qo!In%w##?5KB6@y_D-522A4Tn zK!dL;IWfi8B1(sQq$r3@IRn5_)f;v^3l(pEU1qP@0~?l57I%Yt1p+0XIj&-x24!R_ z%l?E;0C#cjqU+v+Hnc4852R8&>9j;8xV;Fd$Vof7=~-@vI)B}*O^J#`3Gfq>I!b(Z}i4s6VYfkjcIywX|vz}Yz7_)bTtq>(n zJ2Yz4Mm-+7IlF>#$oMT~#fU>xk^)mL3*wyY|JBayW{gui-1@NgrvGOi#N()-&Dxbl zg8|nf(F6pWCEg}HmGS7j(sUMFmg0%psY7iW2R1!K?aEr~X1#;Y0aDb`3ti7McMV?d zw}qCS-76s)G;sIyoDyvX*y{G4j=C!RCp=T}5i2TKYJd)(?TTzR&>St;!L`{Hs=R$T zKp$5F+}ZfU_tlVC3e5m`Z9x$~enBr_Yk^rLSmz)e7i`y}wKchv8K-T4hK-VJ1c0W& za(2~DagLY(ZYNuqiA!v}DvJQMKwh=`Vyc z5R&3iYYU5zB03Ja=HZ3$%@Je-+$amQX{Ki}km6xU|G!jg-rxA0#{T-T+NF0!|KIgh z_zpdO4d3BSX^-$s`=XUQWk)c>hd9nuyf*YqDl<^Yr zI7xEEM6xGaA1of|O<8cbK{SfEmVx3~2;9mN-;1)PDq6xg+4Tt<2Oeh6UjPe_95GR4 zA(;k1{V}xY*#)hA8LhZ*D!{@$EwIyjaPz3OzNOZUbhNRKy1hmS;<1q}jZ-=vQqxdK zqBB6cICmx&(K?Y3c1$#s{ITNj5g?2Bs~wmLhLOLL=OUUrV1roMn(Vq`A5CT54bqxR$b&5?>IJ^L3|pWYZI!F7Ix&XB_7;4>cCZCod|+ z+>2#8!U1J!`>H|<#^P&JA&Yy;_@?w8%E=Ms+{0C)pfK)0@tn2umaSaG3)0kCOkLb(?tHyqqXLI<1aNHsQ=aa^R>TRd-PpV_Zyp@ z7}dMj8lDLgM~M|EWm>(@F6}eL)&w2Hi-QpnsmX4%Y{U2>Axf#@+=@IdlD5(V?B?q( z%3b0#3F$o0Tzk_)h&FC*4Hc9wDJW=K&6E3rq-iyyM5>vwV-Kf?x<-e_RQTc6FCU7QHYceIMl#77d zWWkj*mantD2cB1c6H2Z_+T%LBuReZ(xzwq^iH&PSI%GuITBM2bdg^|hc|1K6i`>J@%jOwcr|V3Cu(8#ZH}&+gU-j!7F{QpY_`ExXK~RE$>dNRkzC zmxC_!sF1z~&xV%pS-Hi(4xr%LJMo_;cQUM#RL~|Ur4}g(*1f%(9?!Ee+1fRxwTRVY zi+cs>?I;Aq3qlcqw*9o|E59$*CV+#17Yc3!)nlbbUA;947A){f_!c+<9d+T2Ac1ua zxxbtn#kEC;4Qt_@-p!jHW3zE*>-{GRVho}5e0WaMQbB*VrF5{cFLVO(J0a9rdAT|* zS58dfALYSnNF$SzelC1=3KLiqP`R8(LJwD&!9o9z)S5rloNfH~jXUZ;T>H*Djsm{C z8Ix=dv~Eb7;1yTx1G9uQaMMZY3A7{y!pD_)6)jix8MtB^5w|g?y|8)6uKu~!_14>z z7$GwtkN^>-i5T1i8~~g#BUCki77ycCxUlS@6}npD&dw#6TRp=-J9qz3<_z_7I%DB)skg^wReXZ25`TmNgK@Jx224O*85kfVy?Vnc z(n;;?@jEWxD0lK5W6RTY0gGooi17~bb`1>w$fiftY)rJ8CkTP#XjlGM_QN4%ik3rq z9u&g%fmK|Hp<(hc3*dGOnySDxDkw`U4JDL>GO=~#VDKDeJA6?3b7gTM6go#YJ^rS* zr_~Tq=I0kt!WgQYM=C->F-ckPKo3jY#VP(I@sTW<1WI25G{ zdA$^`lv;!HTF0NOWTaQj6uvY0>s~)Acsj)0)q*)XskyvUtRz8%6hl0M9=))!vist3 z`J@ouGJ=3xEWm8t?{t!7Sg(49$zTdG2)Bv(DZ(30Tx->WPDmga7}ZowyOeMWsPl@7 z;*yBU@fJ5^(exv5j(zGmuMnFuf@OpJLTVq-S=o%y3NP+{O)tfFRR|(^u!OY=bg)Ro znM6dz>E(Wi`H8XuqOGjb2oh`doKUm!D4GHh#%DJ@XrXs__xg0!V~clUgwf+ziJ-y% zWzJt*Rwe_qc!_p6xPufU!D@rF*qTfk6}8wHvEmgPO7j0dP;36_#&0*Cs{fbu6SW7f zN&VYD-1yk0hZ$VCfAMnc?`WUNfMTlL;UkXr>b9GZ3T8WtU7v_0kmP`(ruWar3{h8C}jNcCQIv zB83nlQQXsUghCoc&{pcU`6-I`oA+!IP$cQ1&H2Rc)x$ZfyI_$)m`d?@((imyE*AULQ2FY{51dl{D+sO>Ne&aE7A+D75P&vvboW%q7ItMkd-ubNI$ zOw(ApV%-=qnV2O}L_=IQmI=YFs=;K1=L#Jgj&11SxIzWEfSx4{Ae$rGDhJ~j9EJ9) zxHZU7uScfH-64DN)=jrQ-?(r0d4!8RJYpl*MA{03)pa!%`QvoymTco21#*f^oOn;U z5tXf_mU})!yd{aFB)1WdTwWvUf`Q0V0?x1s1Si!bcH3p)gh)EK=|1JXyhhPZ*^bz8E34D!Kj>zS;-OJk8#e!Gb$Lu{)ZU$277z&*w16#aBE>+G)@mH={os+H| zVnSR8!Zg*e{C077cm>mWNZqmA*-ye#FJkuT50O|zL) zeWTMcD5WPrTv!iCHqY(>Uy7_W+I3Q{`t|tM2fs3!cN0#>BtD ztWbqSiyUD3EBLQ*)tny8RYFYq7Mv$FBdMu@6u>^b2|^q$HW!p@)vDMm?qL+CQz#oy z%c0E)TVVCwi#j&m5;8fHan{(}2+|g@I{&4fYWYjH$ZJMp=qN`@I~S3$*2z+c#LImU z8-PrK;ZD5~2g$6OVLR6?767Zu0IJx8sJ8n#`uPRiitEd2!DF>*Xj4-->Kl7@pF8-T zzB~Y2&?!Xu*h+=QAxFW4R*swQg1>Ry?pF@J8Avap1v@08(TK)~H-+ruxCU{Pz_|q1 zuIMYa?$-(3fHfzqmbD9h8jxRD9+&-peeGMd<`az{sQ*s=K)uc%-@Bi0ZMrpn?;E=p zjs=QHQrQCqIV(vGDpeMxE8ziLa^y~yP$AdCUd;3g&ZCQnhJd#GZYo7jGaCDPZ9 z74&~$Z0CW^eX;D&(TVWFipI&ZLd!(QfJ8&R zv}jKvh~>mX`6Wf4nGmJan(+H}dSBdh|K^Q1cF%DHz89{=Yh1yY3n3)3umM7A(3jx^ zDB@h;_xy^Kp2Di&j?*%sQhsl!Sle1Mq<#e6VpKvT4-?L*B=OxkioM}MW=$#gE!}5+ zq~?E}-e))6BYLB|dp4ilH97|8R=MfGOT9hKcnl4_(C@j7xF0brz>@4gIhj&?tLoN} zrHqp_o(sB)S;}3I`9U~66Bh5|VO=Rh5&z#`Yu?oOZ2eEv{%LLhJ0*O0Z~*p8eTh!5sLTm?&3mn1(a%3G8H*rX-`;(96M0QqV;GE>FUq% zkgT+is`Dq7X0m)C7U)C>CpjJhA!|QE*W(@v6Egm&6g+Zqc5OztmiF$0he+DuGG#6< zfs{%Tg{TVUL)k{i9tAcI+f(o9>rlIG`q?z7b^@OY&O`Y-A%pHEDiE^D3=`o~Yv+<% zqR6C`dfSlX9h=cA<;dw z+Hcecr?YtkvkOgI*J&vM^)8&w`nZPHDBE#m)dv~P)_6sy_tK`@rd)nt_vBOt%C;3B zf<-JWpA&5M%Ty-Yke1Zp;Lub|h6(D?sPiDJj}wQXt$)ca_6h3l@I8Ro+YkYVPh>(E zk|shf@w0-){aVt4S59%JTs^7YWMK_J#{vx3S7{g-60-$6y@el&<|oH?e_^H!XdR>S z5UkaKptHF(c*V{oh!V{fd9w8wow9ZUj>zq!*^1U*jUr^RJQl5uWLuC*#_d)IOJA2Q zg3^Cq)#fIx*rUTGQS;Wir$qp(%DNJ76ibxNMJtT!Aotb!3je+SpEv)Y`8&|5o#I^P9~} z&9&xIbH3SazSw-Od8|3ze6;yU^MU5Q&AXa+G;eF((!7a*c{eocjX!AoKaGFX`1_6j zqw(vF|GDwk8^6@}ZyP_~_}RvP)%eNAf71Bz#*a4s-NshqPc$w!-e_EGyxKU|m}|V; zIN3PfINCVUIM_JQxW93CV_)O;#>X3b8aFm}H?FJyi~8@?f4lyh_1~!fo%-Ld|7!iO z)qk=6m+ODA{xkKzQ2&Yge^mcY{m<6_?RvldgZ2N9y?2X|^E~tXs=6i7Rb}}k%d)JI zMOwDiw8{2Cwq==?=@uobX^EmFHce|}SuQr4WQ%>Ey4j*=TcfRN$}_A|W_R``NU%we zxykNklLht=1bae)0LcP7yEltXFgYN>9)e7Q%?yyM026F}|Nry6-}hCsMQc3rcs*mn zE|FE=;eDU?IUfh@AGWvJ>+R+CV*ADR>GrejC)S! z_iVq1&+hg#pS|Jhe7<-18lSfhFZ21n;VXQ;f4Iu$2ZrbQ{NV5mpC1~2m(M$f-{JGa z!&7|TIeeDSj|`vU^R6Kc-}c-+e3Z|R4G;5q&+rhRzcR!TwCCR8gM5B`_&GlB8)}^& z9X`P4-r@axeqy+f&-;grjoI_b;oW?GYDfb4o=*?&;PZju2l$*BB4hUKA9nft%&^1f zXNP`TYC<)pO4m2HMvb2S4PK#yosJIN0FxOM^>%et7_{?)l0< zdwghckdJ^R0nS?Ww^R_&h#1z~|Eg z8s_XdG0@pPIS?G483+!~4s>?U4L;20$-(>iTp0BDJT+fj0r?t=Luki7! z+T{z{*mt$B7titWJ?-bE@AL6}?W3=4oOzy)#glxTJ;BG47Ju$>KF(|D7qsS=ALirY z7x`GxBA0bytIYP?vvwaJ>mb){|B98}ws|{eL;l~_ThmWW{m#_k_Pu|U8Q_B#ueoFG z_CvSqlk#jSWH$xMijC)b`l5`FbT^qZsz_Ej+GQ%cg$=2|6ip~s4+}7~I(TVAZI)tS zf3d}GR5-cNIbLU<9N9y}6y-=f5QQ@clw#jv*@jA*LIQCJ4<+PwStD_x3k19o8HCnE zoz2Iu-7Xd9+QYZl*Q^JJlR?7R7s+@J&Ki#l|FuSRTi-&!u~g-8OKwZF;YK?p?M>cK zjhD?~v*C%XK*N<%MerpcDcxL5qR{}3?CN)ZD?N=LUt@x=(|5 zl_?cS91g*n6<(3>56 z`C9aiJ%0=7tuL@C}y^igWi(ohvQvT6OMwa zQy|Yg`&P_&dA#?lb8s<)i3WxS!;R^g1{eQ|#0Q9Dmw;m1~CBBd((Aj^OAEIW#(Bs=c^Q1ntnPvuS9BXI+*t#I2 zPF3Lc4b(4H4~eIT(@2p}(&aGGITZ99o5^Zi;GurF6!_jjB0C~MATtA==6nY(kMupE zMupmZ>0nd?!v}A?#IFIxT zF(I8Kz62>Y97dJR5LD(9(>eF|UL@W@G5N{Qu>ry1)tAsnT05=*?Kj5SV+o!jq;<1;cMvO3ec6PF4y>GTmx5}fOx zdD8#iY)$<=vj6|{*5CYDlK;E(?7@|{JU(dqV$U;FjQqz5C_%?Fr(pn2SP!t2EXovb zjXe8_Lpiy$fvTPK^?VvHgdHLUGZ4i@3)6^-&Z;CITqZW$Jj?hd4m0r8)jdWfISLP| zMH^m9ovnmtW*Qf4(|MJU0O`;)3%Tr&_hWcB2k&{yBaVi*_f8(~Z&dBzNaAt$QTIYB z>NPDsXY5GV8u3FXN{SW1IHD&-pbxme$fFR3b0KEJj~_#D#GK48^=y0IS-i}cH{SBt zAwoCLos@S8F?-@B-d{PeMICa3UXfKGWdhPx(#n3rg1EaB04GqQe_0=}$w>Dh)CJov zt}rk41H*5=#4x7PIv$_@xR}EoBY4;zZK(?9_c+Z50%*&e%kt*4^tf-NQxt!V(q-- z32c~RKo+E(gM0o}5sFmhHkm>s(wV7@p#A$Uz2)ghgBN=zIL}fyd^%Jj9;507IWCJd zvV{zt2E*r-g5+5XSx_|2K_tibjk_x`g9bP|y!Wk`KJ-xUX`#Q;DDre2yfAmUAa^rC z0l`vOi>@hu?<&VAD$b-PPQmP!XkCauSsXHI89b*VFa=&c-MT<;f(OmZ?QINgY{7|1 zL5`Hn)iMcqtBd{lFK))BO*y63k5A+iFL;76XJ zORGa*L#&_5GIu2IuN|gh*FshKQq z2H^`Q6kVtTdX8w=Q&Z=H)ciPrlE$KsUn|slWHtt2qBCkIm~GOul>9gQg}2_TKyttL zq)dv^XGUub4TXjXE9>evdw}!6#79cOL&Ib7!``Rfr&I=9>NY`Aoi=)vB9&%VryLEHDnt!cvio9La8{Q8ds?4tO^>JVo%-|b-)q0z z`XBk{#2+G?;oRu=jx99Ea$P=+AV;W6{G7L^)@Ts(J|m}_J%W^q681P@N=hq{ z+7-+nh9Pf2QeiF{0^X!zRgoToQ2^{9TE^ zFgi6^04!{8-;dt%B%8rj?+Kl?u2K^OT3D1QEr$P!c$dyjtOSV}<+tq9X?Z1ftOQBH znr>N_FL-jTTN!zYd{{{4H(VLbjp%Xh0VTt^-o$8%Ot43Mgd??Jlh7L?Rc<G>S<+ z6Zci*C9U1C5h+Z(`5DN9Bc6wvs-}7QomPLAWa=t`7T<+CL6f|^t|ELk2fdJr7ZXrU z_8yxPv$O}1CCfD%ske&!8t*k+Nw|r9vAV_6mp7|$cIe;$SWE~V%l01L&UFfLZDYhe z`!?S4D3n3J=a~xv%}oCa%x^!lgdrR$PwVR%3|CUGs$>X|D4h#MxW@o2o>5m$6b+G; zlz{c-f$;y|*P6a(>NoKJ9c>XHX#5%8KmIj)z>oAE5!RX=TBc}Pw#3i=vgNl`y%jZW z@b?L!*HF9<$bVM^`fiH1pHaHSNU1=};q)uz)JBdm)2!XW09{tmDw&t4a>gS}u$KRf7;e_+qw;oif-{cr;qN7N!lj9cF3>|_Qve3dzz zNg~3#P*SB~c^St-U#VE1rJUAX5G(Y1tOl?n5=nrU9;8?gx%)vAJk?nzvm%#NLlnh8 zk24yh%9YgL@?cKqi7mh_1KAD=u3&YNxoQ9ZqZYCGQ`G+bqXzB&qWyI+KX$|I?b~~G zlilMb6G&79U>D@f!b4({ylS{HQ!x=V5m`&;c1( zM{rO23|9w(vY!BTTnBCDmRgWIg6b7I3m9pYv8Vz7vW34kc0canE4^7HSn4kJ1&1Vs zHaS)aX%-?4*ZL9Zy9@?eVXN^wzu650ax&%)QI43%?TLosdGI2f@GSO*o~F0P^-cD_xphV@ zMk^mnVTx9ibs>U2DwX0A%tp)E@Q)PEXLZ{i692fW-fsI*vJbMeWpAGQ^t6ELjk<B^I{fKF`W|&Y!M_nUla%i9jT9qUd9{zI3cW}`cnLD zKdXjh_UUoVM!CE9(4$qGS*s#5F|>slM#~$ETdQO($diZ$4^IH8EFlU5k(UF71G{l{ z@a?fDpHL(C6$>$f`;EYYNjkkw+M82itrBImRY+y4gY4D}0(sU@(KM-xB^X#zU65bO zj|dY8=7~2vg0Hrls$Y|B5*W=MkES{NRA{jNjr$l!Zp9H4m3fN^r=F|rX~clN#nvG0sp->6$w=w)S$r%Z$*D;@KYP|X@Z#0`aH@EJj0 zeo{a2mWY_Mn>W2YXCtV6S3X;Y=vbh5Tm>;ZAoKTqdjG3n4V>>18zl zSd80z* zW+!_Uw*UmbQuZ|8;<_)7G5WC4s9CkHXnEjnG1eS3^%0*mUP-c`vz}K1)P!&<{#-Pyqr1#n8h&N$K_1tZdoxfJY5{Nqz~~*2peh9*iW4`BPA2Kd zKBkJ3Uco>dUIR)Pld^V2So9PF9m|GXXAStmtD*E&R0z=?;W@zwH8M7IZb^St-&m7q z!=%9`6Q&5ON-?uhOSj`6lGVr}>NrKILo1IetT0uTKtQKvl53Q-+nmWJ)kkkD2Ps5+r$4(I#ThX3>CAUdYfR9t_LEZNz?N5&DT-{1Y_LgYS6 zC7tl%he`?vsMcA8uJV`8% z&h0eqSl(K_bYb?Z<2w|lTJIi4YNr@Yv@miikwrJkOT0zv=2|(QJu`N=eK6PkhLj6D zdFphbmy~k+ic@N{PyjD*!H+T81&=c@R>0}zbEz9h4uhX@9EXP&#||^Ud9rJdvt@Ln zd5?naI?DA$hGVM-u~At;%QH%Dk?$pF4AG7Y6?@6<4AkDerNd4#c>pw1m6<1L%CaGn zBTC+n?B9R|r~*+brc|S$N1+L6Z>LMfczEc{Uu50z6Z&`mJnLdps{RRr)QV+UUS3}% z3|S}o2zcSQmNFX5QEER;7AemI?X1L$^#!mTx@O__F>%bggR|ylXIm2}s=kc%!Uwg_ zD;w^a&A3@92a!VeD8U5N|81=oTGP)@{dZHJX8!L#~eZz%94rSh6zXy;w;>{f-ER_+p?;Fg)vAdFKkAD$zj1Wu+N*Fm-7Gz6K?Q>)b-~^Bc zEnZry?U0r_v9V>Rte1;azNn@zAd$b~buvj<0;j^H+4%a<;C*8cAR6A?`_!XawQ$Kx z#?Vt^9Z52L&8d%G^}joUZhXxHF>UokzNY`Q$D2?7qHMP}YNTX&yy?!u#5S8MePqhfDr5=YFGOiL%S)-~P z2a|fq0|5GEpg9mXDVTF8ulaprPxt|3_n&YNP{9oDWtqxy=2KG(JBTMOUp`Dh-&HrN zOlMK!Hzskb0~~x4C}4u&v>?C{B!xM7GY^L}NA-KSf9%;e!y~;<9OnUe>M)N2vN{yJ zm*)VZ&fm3)7SwK>#BsxL&N8XW#SU(pXwELqp@RV@xx$QEw?hTY!Z7ynOT{qE27UDN z&=9y)4z+~Pl)IYp|6*(U?y0}l{vq~1rAI%bKQE3wwP)~9Zy%?T_AP_lQ@M`iISdP2 z4E_WTSL??>Yc`^UUHovcWzz1T(_ogfg9Bqv#TlOJNmiEZpW%7}DgaTe`7qxP#8rCs zF&-F>Q57!0Z%}Ji_I(064RSdaY=YH>QUJ=xS7tV|>FImkZ>ShPE3MYnC=1lUJ9-)) z%(K*TB)!~augI(rZ|D^o=7EBL(7sb{)(kc`td+^AV<^qDJ&l>z z;d&DriTYk5`^03GHxTArEZIcH@hB4D14RTTxMl5PgJFQmRl*#Q&N9`vMiZzfA`4fAuq>{-4of2QQC3!Dsu~ z-mlbLMJ&5`%^KC{rpIHbg)kksdr^d7#bBGYLhiGjDURUN4ACSFBx733b?wPz-&RrmKP? zP*J2?^Vm#Y5a$l7iJPIN{+XARh#sB{JwnV-BXE+CoWj71vp5JQAbd3MMkiHjBu|5z z9>nPtMM1CvGapt(Fn;#CV~+$He7X0r1?jpF>!ux{a^9r%Y-$SqWtVJfy66yd*mB<+ z4!U-ZDW&@K*t2T}pY7c(GB=0_!`O*p=_wU3{FXXhfG${~QPj*dT4|zWQmM(zI8}GD zM;a54Jz{2fxc5;V*5)#afXkP+&>EAuX&{-RAj_z7yag4Iqrt(b@{+_r@|5~4nJ@Gc z=R%%Z{zGh3E%4`rT9S7-NjXa(T)$}-1_y5~(cGo8&i}ow^;@m!4@~|0?f-=e`C0z) z^ZYZodmICDKG(ZTAik0RY5(Y~7C0Ep)ZgeE1|X>r6hM^Lu$6bYu*I)Y1BBf#{^#^o z<(10oRtl3#f2vFdw~$k(s-!<`xyQyaGw0UcN9H!wp8PDFB;*)66J zgILtu50qXSdp6GCiQb(j%ZlPA>%tI(k^pq8ycI}9$SO`9{Eq}wUtZFTLvanZqmEao zHvCR&?1#sv>`Ywmeb_ImZpdmCO{4M+90&m7lN5Pb(+0F5SU5bmnJ5Gz8E#XO2xDL;J z<)xK?gqA|W3qqj*coQ%ehDXyLMLs$G#6xuSHDBc1cb?tsU}YS`UrzNtlF%vu!D7OERzXozaXA0)me#MgrXQR7FQ-1tUw%G+ zwmakJblN|DfA53GJi$(WC&)*3lT`KKZK-H4t*V?{0db%J=y#XpV%*_5(ec-lq>MeX ziNZa^{cga_HOVo+961bt=wA6qgkf$>s57FJzfU4Jhv8@1>7Qp@^fT^rYqf%Pxch6I z<8AqS5du`VtU=#;n`+G^2(wpr*!}Dhcd&ke3Z9LNZz=G9J(G8E29^fW{*uMl%BIjTy3Dx?6wnGR8 zI>`9H9|6uHj(L^9r02Zq_%`5dWD597z4Z!LOT&fXbLlxcZC#e{i%O`mN90fm^?vA7 zF!t!FfG^ast5X%L;}Tj|c=H`jBLHtD80VOQ^i3!3v&a-Y`!}7 zD4XGG?|pOZ!$zMu=q6UZj|_c44Ufn;hUM{~tWUQ-9;a1x125H{Tvg|lRa~x=_aX?q zE+0htqyzF`Hr>L~tnsT@DjDNCgJMcJy;QEGod5NgThoK7e?Ikk`^nZ{{`oL~pY+zj zedDK%lZSe@pRml4eGhA#ztOP+tt8XLPhf|F#s#wH{)Hqo>$`^EI#XJT-zRvmQ(xdH zb_~jeRXh%nyJN0yLxS&37a}oKiG2j0k86nppWizN240kz;WSGfxepAwwS+>rLrG3Z z2_k|LeMt!S+J308!dfLOR(235me0fKjncu>YP;FFN9-4#DPk3CD^4?RPoSMv!eAd- z5j#^q=fPe#sS_nXvTWFan9F5AC@oE){)o0joOyYZXJ6g|2MljTc@Yt80CX~3 zAW^IVbsSybudD~&t&K#uPCZ>1O<+qwKjW!-4KI^B)0|PA0a$bBN8@i923x&Ot`7Sv zRtImcR~IX4w!#Y$R1F=`k!UKG3{W$CuW$twI^GR`RA3U##OSzo(R?)G|M#?}{?*hY z^q;$@L5G9$W6#yuezNzT8gQMnHU{Noy4cmipjJ*Q&{PNIlnEA+vR*WU zHAW5SE9Ce3=CTGLUc8?`dI~K88z>3;59}XI;5n&_6AaMUURTMbK6PhYl;g3f&g5mRL1|l z)tdg~)L)u3&#D1DTN5&q*G??z)a!M|c*c5BA?i2Gof-rN5HrK7$^=pwZ zynXCJN!RY`O$()F)Fw*S6i>nFGDjjG9+TZWy(qzv7tdR@&kirfAyY%}R1p@}#JDZI zqDs|(a7gc#DX4%e!HMsqwpg1H75jILi#Db}HKW?vr1jHPFLQ!}VFfC4Z?CZMyIL{SEHaev|7H>1=}3zo|I3GbAc z6I}%)20%=(pqY}u*rBn;Ep>)>^mdGvnMqEtpS!D|xkD41WJ#jB#DdV zIh>s%djj7O;`TYj0S8_>aG|#DBG`!02U@aF;*RhwV3XwngYS&Lsda20?5%tvTs+*SJqNx6gHH(;(>g5z7pO3V68HMr% z#h&VCEKw;2Zsvb{b4etUJt)9c5eB1yBFJV-T2ILTe=Yyt)%NeT{`F5o`M)st3|h7IoKl9B+aD|pqIIR(^`Vv;qaWW%%FVm zsIoX!$+m zyDKUQdA>46oY6yyBx)HK!G~C`7Sjyo4el6gidB1fe|MSnOb$iC0jN81KyGay-&Kdm z_*-JONf~ndoF}pXEuf2u8XXYs!*NwZZtrf;NxDqDfMQ&f8iZX65aj=hAy=om7ohct z#4wLGD})dJqydP&5uO-cLj0&+8(XFUL!`UFBgFdgS1a|zc2}i=MRY!YfrT-^&P7G$ zR3+!2+!^faw9yckDF`A)k4QVaoVHF)UjZ8K7P6g`psqXd;51))0s>b0`DZ6WLE6f~LP99dhm zI!82s%*$((#GH{jW$pPC=#67oPp=*`JGg!9kzB*Oy60?DAYl}Z;$^ZlxK^*VwVyl3 ztniyE;G<4BugAgGI%I%lYgYew^+08>D)YniD45sDyYW_a6ZzrT)4K+n-6h}t=#nfP zXcvO=p1h!4t%!Zr9GcRUgR|I1R)giZ$ySUv4n~O&nG;CWPJqk~T4Rsc8ob^;8$i^r zB2YQ=JwpK)=G6(yNZ>6lwwUCXbtVf{Gw^piMhDNT7-8{*Iw-Q_9 zw`#FEvf!2IJ>;m!MDdS2E(THwA$@{8;9Ki!eN3^_A|S81dJcp{CtMA#%D^-~hp}CP zEZmOa7CwuF;qFdPw~+AdY!6&Wgx~-O6lMO;@3yA?C4IEV?N7IU_ZLQ*-+ZCp8h=>~ zYVg5sf5BE;CTT~*d_a)_eL)5xFQ0&qAOqw|;$D*uuH-@-RbTSM=DOdPjTOv8bY-E; zePZn9*%De-`$%-H#{ZC)swg8m9H#f)CED}Dqjpm7fu*tMdJUfFz9hm)$z^2cu;ZPg z8tlLz+9t2yiWC=G7Ax9C{AToVusZ&tcC&rD`@M=n;b#qsl0UB!YmJRGOj9s4oRdXm zNd0DQFQq!K8yu3 zarrgbF=y7#ri|5%(776|3D_wW!j^Jjeeu&HhhnB5_iKR_gKGHp>+VTIVhK(jN zRK(5d@l%R<3GXlwwbJJr9V6r|T3mA9FD|hr2ONS&)UByvRZ0J!Y)$=7Q_r<;Z~dLt zj@~~avcZSO9!WOb*Zr=nTf7$VGG1K7Jqt-jjzfB#+1!4u&jJq$92;CF>jK^*aUKXN z)Cgaz?v716p@iL}b!GX$kJV^xsTKK@&Dm$h9w#=qxBG%bzfG~^rPU+`FTpu~%)NA* zCL5zxjd+g$*w9H259h|SMwB-CXL2%J~eg?*4iz?gAC& zo&rx)w`_q0T&wg^s7EUjo%F#;wF-(6aaL=^kCes}f`Nu^a!|+$bVjcHA+q4y4yHJw z=`k)ZkS1NKp;3hM@*pI-SO`MeOd>?T{3NX7bj;t}b$j`k*q5~*L#>c?6-LSeJD#|I zc5v(Xpny2s(|vy4{&80Hd>#MNLKOPP3GkRpi7zv$ig=jo4s1D0)=$UF)7%dlhxMok zXJRcy3#l?8XNUX7p80j{aQ8bpO~-KAx-y}JNn+Dczr7$E#c1L{!rpR%Iv5?G)2y(* ziTkdh>PHIB1}*HBw-a=DgX|?ev3f^21?7y><4Md=$_n05B7))mSU_&L2)n-PG1w?W zK7}wisq%GR62J?@;Cq1*?6S=Tt#dInn5S=O-CVLs|G%}pzcu~QslP}C(C_irU;fz* z0)wxQKWnt!>Yf%h!TTz2Q(#uX7FY(SK%BQuhM%L=A>HJokwp}-VnM*nme!R^01iEV z{wmGh^@!5so}*REGprS6r&0>1^mF|#ZzU;W^x{a9Fnm0AXjuTcDI9t}izt|Y5d7x& zGphS}qx)@EPee}*O~6M5lro8qBs_cKbe}a;qir1>m|Yz27X$`Jx~CSxz!E7PTT7;L z1)ra!gHmi~8`ytB4Hr~!$_s>%B+AH4Pdz0VaNUvO&|qpDW7XPSw|AW&+h$^CSm37n zM0l)?vKTf&7oD;keGhXgVO!smImZLht3#VQ&^NJTA(S1B7Qr?vVcx{BN z93u%KXwAxqiFZv&D4~SJk6V{YXtIbLmhDs^>|~$SLEQ>r&5u86s<^%TOmmUw_dkG7 zBRrVd)m&$#YOa25?-2#a%n7uceP18nFB-klJ)tL+a}J61s1q3?;;@*VNaGdpKmyH! zN5`MgQnuUOrx$7ho!3jO3DtTtqzH!qyQD>kFq87ZJ#-BMv&Po&t3%gOizSYQA_8wq zR%8YtSBMf>CECcQ^m$k9s~m~9(*O$8Aqz7}N(KLfC4y}cR{0$A{`MK3i{0bR!#znQ zK8@80#nJV)AaUT8@q#nlVH@y3`nO9tE2n?h?s_8vy|DC)6OmZO>`UXle(Ry`Qxm7y zut6l5ORBd|wLVFEiRE#M0%2K*yi>20=9R(H4~_5BE6}2!IB(8~?DKXfm-?^T2+3rjmiWO*l)-ZhBKhAA-zymJPzm&E0EYsbzl zeU4cJ;Z@d&j5}x91U>`1P`N|XGTrrn9cYXV7^>F_sr+jRQ6QRd!iorXzX<~ygo-eP z4xFo;CzTylpb-fn>xekMIKR?M%KCrnw^jeQh5zpmfBm=Y4=pP_fMxhV_o#!=2;vnr zb1tKdXDmv904PXa7{p|%@S&VC7xoWqSpoS33zTNj&rcu{5>g%t{677&#p#D&s#-BM z-6o~6>+JEdhnWoS>OO8?|F?_hs$g`cx=2{_u(QLHh@2h6)a3Yc zfsRT(T+8!0FWnKH`73 zwm;sQ{#(;`P5pNJU-H*q{;42fO>o^{|9m8!?=5t^-C-X_}M5&lia(@T_ z;q@*K+~^~o7`Cwmx`KR^a#;7;B&SW|;zI zV4`Z=)|4f+oQ%*OgQ8YeQx&Te)|v-r=AkZBK9KwTX+u@1DX>aA2}(aU_RPh>`@3I- zd%m)z=`EgjrCW6PK-^iLLG(pl2-HPXhAf25Mg0H%*7SF${`S-Z?f<0ppSA9<&_Dh% zxNYny1;bAFA#uXOLT@fg0A|UKB^^<1Hy?mUHSW*vr0-X4TsJhIQ)=i0J{P&cG}y5R z5o|AazcQ!urt^}G{Y6ThKGfCAH=LKi)*-Y_})$ISbdcB zH{v*k9pkiDq$6T02%6#rMy}dfk75~$d4vulAFx>y>|l;Wbt9UiJVc2lHf1c7d=dqh z5|h`*o@GEkqc3aYaSgeU{6)$xn2$8JM$CA-&5|Z~%rXn*b1AbKo>z^#T3pi@)XRn(S-A3KolH ztuap=x?Q5Bu7d6OFWD`x+`wc(!{^kLWNRBM2$SDRsUxSyNRs=-lw4Fl%C7q!86R>4b-l{;J=03UTyvIdS!kE}V*8lWd)4i!fOv(OA zi_yvN>Yu^0V-Hc-zOVa*;NKxPP$Z+pme!(dGPAg~B{fA^btlPmuzDY%Lxvd9*#W00 zJ`IOh^h?a`0|(e~twk!Imx`#gl<`40!8d@NwgOnI4L~&PI>c* zW4JRl^67DmYPh@m`HCE&x;#)Lmpwp*w?QSC717?gIH5)2P@HRr4i7#*p0>PsvHLka zQM&cpYz^^7!jjT=Asj9>wzB~?R{etFXHulhe+d0)Oy6>ABbj=4>TE7xD?25)}6cfBIw2&R1cw7##F$t zpO?A^1=xh0%n8GIUS@{`%6a=h1z0$4W+&n8_eBM;$N(8=BFV|AA(!#U*-`_|62DS$ z6Dc!gijsx%bi^`@NI2~Sbs#H54JQ)|NC|BH3#E!!kTp1`hUnSc04^-KIhLJ-K{$P3 zixe0gr1XqR{{M~EbZ6=Z?fsrIOOmX34ShM#*M}D+8M7BhZ}@>HyPx&3rxp#a07hA4bjWljZYthDH?J&UmHbx;^E|{WIhqH-#qM;Qh@Oip(H5g(NH2K zl&sb2d;0{m>}IoXn-PpVU5~r$^}FGB#JR|=H;PdtcXVfVhkI(goGR;-DjyFmAgd3k zzEGGwadDML!_`jfuPS4>3^uv3v2_VO1S4s4PI$sBXb9m z&%p_jA616~xUaHGW4VoM02S}zOwvq{6Ih6%f+BcfV6Pg)iG5eczdF13abXjl=jfv> zK2YaOt@;4dttBk;87R^hJKX6BP6i(J|JnchueYZD>C`vcPqhB}yH2j}?gIxO9(g9q z@KfDSpQ!u1?OFz$I`4u6e;MyYw3lhh(e6fQUMl}x^y?9s7nW9-M+xs2rw=_CV9QKS zGiEpWE$s350kG#&LS{?LGVTYS?J?h>Y@_E=gSuTXz~i34-B z;9E!(o}lE^HO73dhJiULJ$2ZL5M15o{k&GiHdMyRnxDLyr{RU(z{zDbqj1Y?Fv>c- zU`TZ9%w@_MtDFF*H3spiX|o4slZ!)@#yj|~37 zNGQCzWc~k^_8qOMe=v2V{Xg@UU;Y_9KJpNX?LFO3yes&FX3O*)!T*Q~(?yY@QO-|U zfc39cOUl5i`iF8`S}T+H%=$p}Fysy57;MI$LZ1D;vm=kH7(CwH7d^2V(ud<3BWzTr1U9=CAqYWF^lDq|yDO`x`6eaJ4u z+>Cr(jv*stHEUDL-8_lA^bwRB5En)seJNsxZAEg-TVMCJh6(lc#B-n<;_93Y!646aZVH`UyNpZ77o~ zeGt*p*=$ZBq@e%Sf7+V*-S&Uo{yu;CxA)KB3nS027{1i~_<|9|qj@Arv$#cdI2c!o zdN*uKM56P&fjK6T=j#OZMHr~KLPOt5q~#Q`rzfyBw7JG}xu%W_rVra%9L~I|1=sgR z9!N2GxO;C<_2$;Ob2y5yjRT7|(wZz&=P+a3vVb z4nI8d)H4cOe&s}1`<2CtRo%2z`(Q9wlhSLf{NK9=i!?~mD7dY|UnW?~p*$}%izD-uX^SI>=8*!96i z{9}LzC<@Ji8;ePxT*9y&-aqmLwQEPZ_sscOWp{ERs#nKBxB9_w7dzlp*z4heq=_EO zwr9CqCgnscZ8nYxDpe;y#cnOAwIfebW@z(s32 zOt~@iA3=`8E3$TvC(5u_UWqyytdju6!p>^&JGz!Vf~0|z4ssQ?if7e5?bLZpzm( zFMu+m)sULy01FMhN;OCS0z#Jw9MPL+{R&jTni~w*IrKb$G*g;_aV?%>WkMTVvis?En%b4;6lZjtB$e7T8p<2lgSZAU>y&tLgAl!{{um<0EOtWU|2R&mc-<8{hTo`ahqeu|EAg}TT|cT*7R2SQAgU-#eSY9J z(h$A`-FAV5V`lYXUaz+n?SQhtJ3 z4LX95Dc~+0A6zpblY`$Z zAfyFQLlz00=E`nB>|NEWR z)IV@bda)#ZAJ~ zq+6??8q$l7)$(0C%U!y;_TmvnmA*YVyw5rIK`)iEArLVn@9GDU7mg zg!C9@Ir5|wB6xQkFX0gx8}ix7{Us6Rg7{<%-cqS&4gFWAf_#!}veqg&gIOaVgYn;| zwxYyJvhwsyE&!7gN3&Qu1)LwSTRf73QNpUqrfU_z#2Zk~V%Fj3Mjo7U?S<}#fR5uA zZnx3waT3dlqgzWAO#$eN?-3UaNuZro%6K!|!Y+UdrY7oRueKhw?EV6)YIRV?^ zjtYPynW7PR$1bR2$XIB0M}Z-`onjN{(ljkGkD)9 zrrO-y{h&NKkv#O%_j>jrxK_Z>ok$svoIdEMq+_f(0w5f6{_p0dxba|a6k}}O=zhRl z3Z@YVBQhcYBzD5#R)aExC}w-OiJUqkmDla(ZE-}36X;Q=>T!Xv3{;9P^*rnBl_ipO zqMeB(GUQ9~Q>X{dm8fG2Gi4C<>Jk>teC83dt;P)0;n9&N-@MuBzQ1Dq;-pdE(Ns#c z{j%~(s#^UQ(~rn&sJ)=SfHx7Ufx_k1NX?hE530d zpmv0AH8Gq3oxke+_We;z%z32yzJ>JG)M+g`sj8f$$r>i=io8JNZZw)yc#yg~aRU@j zPA}y@`G1d0&9$d)$o;b@e__;@Rk{6O_jaB{$U*p`=Gw8Y{-6Gka%185z}d=~Ju3 zLZlLqXP3|SVOg(@Je7x@YQ2hES+jwNBFPMjiAW<{LMSiqbhQ64h8O^H%E@!+jahPQ zTtI!l?o)NMnF9;S`jt%L%!y&a=CFWqc@brJ6fK5xm&X<39e62a^PO-NR!(`W`s6v8 z1NE651c~u#f+Ux>R{M)rX9tHzo)I+sVYj_6$1R_bm8T zIM_f#(SXo_({$JW2DR2iSS)D~$L!#~kq6-n4|VrU?oMG3G-I&HtghFdde7o0gbRNrORQ8Ez_0 zFT1b(?n+55st!;kKLJe@ZX7J5Xu&!_?J@j#T2bi-Q*xOPnAM20o7Rb$DA-v~2!HR$ zLwT;<*}ZjM`hdKXQvPbes^@QRA%}M~{4i|Sp*a1e%p@)bxbVWl#B0Gzn|dG%^2 zYIjIp=dkMuYiere&a3*@RW`!4Wn0J*nrGYm3|JH}=xiI~5!~8NpiwFUdYGg#Gb`=+ z)MJ}Eme$p9{F&QoHRzF)aN7Uhy3m?>W9k9^^E3OieQ4w^>_6V@yf&$BX}x6Dts!zb zGZ18M<>2u?6__OI2rgQGP%;J4rDM0Q(~ZHVWz^1X^v}2r3h)=FCCM~UrP4Lx*(>>2 zqE#UkME-J}AW;!mV1o!mA`vmkzY1X%QxT1FBmMc8*PR;9v2VGXZV{?Gn1bZBx5cX? zl^BnQdrr&yi%A*(l2q1X(&&`Vm~2ZJBBRtzyxDcOjM4t#LFklJjo|kSwSvEE5}#sQ zT0R(sDW2%b9d@$kkUEiQpqV;@vFgG>RNPxNiqPX`PmQ8W{R5pVCmd*lVsdr#EtFe_ zX9iP1{maq8z|)Tp{D#}v?9{T2w=#H!uaDeg{>>|$SLbwK5uqTN9!<5sg2gi<1#+HS zRH_Tg+f5YXU#w-UFs(I@J9G987sU-=2{@PRL)TIAGunlE?qDgE`3M3@MSoy|EF(`` zim2-@S2uda5wvxNr}=K}?}IA=d18Ko3Dwb!fM4{aTsK$3{uY`^DTM*uesSbkVb}WI=s6Am1bS^aebx|OA5qJHz{^9)@99OBMMs4$EoXw6iYhn> zDY%ieV;+pIWytN!*hxOm4eEbUnHWW8zOCXBwBUjl*KfePLBps;t@W0g2l(a9sTY)# z!G6T~&XQDe(X6uBv#?@ZMxFXWM8-cn@)R>N2Bx)H6RZ^=Ko|T#0If`BMstw^WgRBO1au=nNEa`uqS zKvxuQN)fIZcCwsmS1!BH!~m^`IEoqtNCCqP7R!s0|HFeE!I2}@tlx|RNmOzVK)TH#Ch$3j4fnQ!$69VBf zuk#uDg(xKv16Z$Jx%`?ctW$#~Bu98ibj(gk1!k@(6fahL8q5D%+wIo$kEZ@=`~Pfz zlfVAi`t$YCDSGa7u6B1cykDq%ywdw?ai|{kbqD;}!KX(WQ#UnyqSG2Yt-3M%2R<%y zXl1EQIGf@;g39VtAUTV8VTJwqyO7-&du&br4V2s2R6-~P-Ep#}VK^GEC$DaaNFO=8 zWe&;g;OiqzwwoGmc3OWfUIuERX;-=Ckc_b-%Zu=C;IGG{5gA7jPs|SCn9aDr-O~CxoddWrm@(I^GxBx)xmNI@P7*LKE|l5R3-qPmeqz zZu{=e8&VbIKM5bINCK@62uSbkqu81q(P_`q+f+m&-74IIGcZ{>0W!}PDkZ1sF@BRS z=H``hlCdjt)7tJoTs z6#ZZk;ZhXXoOed0v)UX;{@>dAOl$h&)Z^`cr}cOF>)ZSp+&+qlYj<^CmuF={Sn)14 z2xTM`I15A$rV%g3#)`lf6HEXZ%cqr$dqRb_2kh$aDqT#TL%5XPYX*!!QHwEEY&<>k z*fkL1M?VQ61Y4a2|95~esDwDNFd+)}l#-(ORG>Sw##%2cuYyM87^9oQX#8061{@}d zyWO_mkD=nK6Wfp>^h$Mls}Usq@1d)$F4(I$4sy9P6Ke zhxeilMp}`*`vJ|Y$Rn^Qn~3hDn-@w;E|Z`*NZO|mN+37~nA&=gP|={%sJ8c(}oboc zmk;4)ASKnlrV%Ca`(zuY0BSOXX@d!EKKEyfH^`zIAK=v%39EZh%}w-RWjYbUYboW< zq!maj0WVvndFvSs9HR}}{*lL;4Nr8gJ`KSVw+xu1xu<_x5#L-DJY?9(*%3A#-YH{8 z^p)*n!4UAtYNT*h++fiqEaKck3vJFcspJ zQB2snqq7w&4FU_x2f|8YC+xpxHr6kZgOSW2zB|5kq5rbxmlYDC`gM~8I-Nczg-Z?! zy&wa?=jX~@Z5w<)>rB~3)_HO%eyxzqVpAfP(CLcX4`_I!x_tG+m(Q`>+)@k6D@5_p z6nD>9h8<5CmOMj~dA~A>$yv*t%as|vk8nQNB}orE$%01FQX|??D1TRtd{&CUj(!k) z(`*S5PGl5cX_IiP8>UUvsj9LqV(Kr_O7d+5b(>})BhS(rzS7ymk>mVZi(#KCsd_U? z{;fmWMb3%_QWRp8tM6^9@mdOPD_o8$-1qn*94jnbKR5~zF`(Kwu2DH3-+y&|8IOMj zN+|<#qDH|B65_$qre}w@k36O7+FhNE1t6PVb1s@YgAT!Xhj2GJO+j!oqZAhL!03JG zEt@A5*HEBQT9iCg%T5E+7}<#TK*IW!JEe6swk5nX01#ay<=5{@802Q`@L0z|q8&&-Ie<>Y?B5890Fm_nz1W(1wf(u)i*FrT6gnk8FVOJ6Y1;Ud=UPWgrNy=t*Jk|Bw*F2Hsv+FC$CM8#MXRWQ6Anqm$LUxYX zCf|vXiwKjgM;`Q|lwm8LiOS8ixBah>HbGmbBW?^G_YxNRgX3$+X z)ONcCMNz)++?I25Z?Ym#V~RapoK`2kX3sJ)ZutHjWFuBzZa+f|La9j2?=+iIs3!tb zo(CIG%Xi`xntqmC(n;gysKG}!n71ZCWtp^8gZh<41;9nRs>v2JxE66dDsBgq0y7V~ zDS-|ulkAEeTj@v}oBOtJ8)?p1`^TNmdQG#I2=w)lnv?lKO+A8DA(X>a8|%`)uc*_A z&#|-!*c(w8^cR2;F%}#r?gu12LLjo{Q3OTPKWj+DHJdBqQS6^b@OQO`4IHk$M^)he z42qT5CBXr4?}Y6FotwjlEr33;jv}z)C+eS39ICz)h_CDo!9L{G(9pm&_3UhRC^B>p z?|m+M80m=Y4n=_@O37?h7RZ;VBgxy%IE#73*F0CR-DElvmLa;9aX09{{c>x1&(tma z=a+weW)K*BWE9iv_IK76IEgcKVKveO7I8Q*Ep=uM#0}eaQHF}9Ro4Ue{IyUa z2vXK+PoCBP!qSY;!diQ+=w|M4SfcJBX}9b(c#B8>S&~-IgJ6V}a)~+iLz&A40W$eD z57?UkcRjBSgSCsxmt^@CwHpq8dHoVE%nBJvP`rkrEQ(o&&|6u-TC>ew!n=||&Pb`7 zj?WIZMll)gd}rlpt21Gyi*ri|xwVj{na|Y~$;GD_=Oz0eDJ}9PQz8Zu0eqwd>_s)kkrA=WB9Hp-%rb>KH!|45^}y0*DA-Z3vAOO z+H}n_vNQGnztWoi9{oT6p!HXNX6XONeK3;O^Xrhj7iD8kxF3XHiZzD;NkI+-)O$oJbwsj{PLnl>rhkhpIe2C9BH1=@AN#a41dk-2Mq@08XoFvAj4UMQXFJXD_1z4j!GZx0*rRL zM5?)0dWqa%<`D3mJNEdy)YANA!94an$#9lNF=X$F&a%!D4@{$US7Tlm$y*Mfr4;uV zH4D3Flbb)6_>wBgZ|S{b3C?*7>07ck)Z^siqZpI-aOZ*^uBS9ye6d5I!?UCZkYl(+Hxh`n z=#F5rH6fu*4ttE%RE37JRudTJAUmJQjavH6;(xcasQ>$K+W&6rAGSV#3o0h+Ep^T> zs0o7}pAf0Etz*S%(2FHw@sO^( zMI74oDrqusX=8ifED|ivY(iBIM;H=Uv@tCfW`D-$A%oof-a=(l(o;9-Ke5HW-8O3Sk*PMR%^ zyrkoZiP*Q@tTiaqDj-tI3=f1<<+EuFXB$2|^2EVw>z%WLmC!w;EZ3^8(chruqfrE9 zzl|#?KvbJMl~>7YW9QQy{P!>?QUT#*8e8!{P}_ z(BllbTQ@naMQ0K0u5j0;S|E-iQ9vYJstk!r=)j6Z7?s_CLb&XO1p7=ukI+u;dE@=X zky3HdATB*RsL$GSC@4$?+Tl&xl}oio9LwMx`D<^{}!}=LVB{ z$Go)wwRIE$b!O?+I>5IFtKmPM8Mw5pcbMs54ZsFCz|YAI}*k(d2G z9Tc-cwQgHE?gDR&92`T0D6A)10a&8&+PqhU6FLz3H%KXbqX>*rbzWLD@VYCUU&Nvz zbho%7@+h`2JABV5rVifTdEsen)8giqYwMz2@6|V(#7>*v6mvuEv;6q95I~)XI++>Z zLX~=BH>5CXl@_Oxq|NSxN}6QJN=~LbsKsQ+E>iVuR@O{!!i1sCL37D=0`O9{DGh|| zMo;R~INdWV6n8*z6&@blMeiA$A04zS;>nIDForO$wU(XKCEhHDx;*5sEy`>&c0gun z&FLKKETK%E0|*Sxr5B%71Mx$v4F#H5dHhgZ?PA#zTgF@=k{Ph#)(K@K1hCz(!lp*7 zHe$_%_^G@ZOpQFAaM=&|F(%R@*C**L?6KxclM_&Pva?BL44+SLhkbQFVeJDr#OZp#4M;VK7+ zCz%$#5_2S8mvP}jr0}Zd8mr*@8N5eNl~(j;(2B(_R-Bl{HYFx<#&#+acI8E@4(lOO z?827IEJNL22f~Wg2`gHg9X>bm#M?Lbb-q{c#Fki+3FvLnczkOUi9-L$x7$sYOCOgl zNNg}}kHudpO1yxX8j_w`XxwtilXx(pN(oe6L7 z%lR%tTGoa%12Cg$#;3NPVEm(7^UsVt2Y374&Wk{GCu@`D6^Sl##-kJl`HMv%q~f@j zl+qQF9N8r{J;8WuAt!v$$j=l)`lsrS!jhC;kjA&$EvWzB1klO6kzbVU*Y4orH|5K# zVUBTZ_W#}5db2hCB=Y~iXg|VV|K$JRbcorSOP%j1wMfU-aFR_h=YnV6zkI&Lx)f|G z@Ti;=R<8mAjv0Z-rBjkY5agpYHejnaT6UFO)X8gohIv_|T=&({XC$l!%bn8;g6hO6 zRT@BssCPND#uC$yuP4Oe?Z_{gXyrRQI5qMp&g};}-DwomhYs+B~Qt zY^je#iA&96cJS3v4F6p1oD#DXXW{krp(+z0zF-Jm4C)T!Ti-JID3~;!D0xib047LY z+;mlPC7o7qz=t`7pLivk{1ml~_eBvv_Bz2A6{&ux-B@HiAP&10=W?xnVdRma!w9#r%K7F&u#1Ui;?0e*s-f~t@XGG1E0(6&VE_68EKA`xdjQeN=aCehaX6+P&K@=lrd|I#4DPQty!rY?t~fuWZq5-xO?Q$q}M*z zc^2&mPcu5yzXl6Nk|?7^4jStFQX=X0 zI30F4A#Y2n>(ZL^KVyQBS=YqOD}oJapGTBP3D!m+G>ps`DF~b`D)Vx?xRkMeAYbjf zvT8A2WKK2dUoXUGF05i6Y4%A{&+)WMaXkUvgk z9x7d7!#c_OxNv+YD=Co(q8lzkbY^qKY;6+iNO65E(>9%RCcPi`hn%`AE36yO0lEXC z-E%;fuEL4rj~OhFJacsWL}xyZD3kfb#3N9cN~oMZQjD~oD|k*g^-=-;)A{TV;w520 zsP5{?qAUnIeak~ZgU6|@2z)#kX~V?JnAp5Qe^r`^hSOJt0(H${%|-;r-TeQzw*EzH zdY$=yPqqIZfBn<;2W2_NVn5J1QH#M#WWd>tgFc^)B^%*l$yl*&QALD6MJt>Kep#Ki zZ{nI=9B;l&!iHPHJ~0!rQ|ZSSltL&+(HVYJi`c*~<@%{mj#N(n8rniZVC17zY^ZyZ zFlPtv9eGst@XpTDa_Z`#H$<0xC!z5hyz6+`l9UtzuP> z4!<|T!!C0;*MjoQQ-Rm4j0h(7PEHAZ3OZV~EYAUg~>ls{)mH#^#0}!iH znlyP!_bFMXwv0n1SCa$Yi?d}yI1<&zPr*$*$IPGC1dQw5B{jP|G0tS-5%T^~@9bIK zg9kcKHCs5ImxaUANq1;+WSdAiIuP209~*GtI1YvzD$*f}XgoQ4cvt7iIq-F;<)}%k&H}cfoYlk~eXrVfXx955?TLG&Y97`xi*0{VO*26Ml?zrw%2x6DyQv}vT z;%P&wR0X9~LXUwfRL?R8dO)mi96Vcw*RfQL2(?=Zu<7y%2@C&Ju9kH?6)44JCNhh# zC`GXUR*uCwL6uWFu9_7Ds1xUgkBvM6_|028N9Tp=QsD%tbLwMcSWD|}&T2tw(p}Q< z6#e_+?0XOstmhu+V~j44ioO+PY^*fY*ZxR(uddshMBsQ6TJ;;B4`R7jv0)vM}F#`Ky=dqxjhLQ;fX(sMibnnnJ8^m6*o5tr;U(pXVfrTmsjmr5paU}c_9zgy=E^hIvF*o zr>NJ~3|<%4qnNMENb%w^^fQ33Yl;ub$T{14Bur^tQb`+rGVTH1?ZNju35hY$7S%|%jc z4i7#zy2ERHq4Uj!33QGojTmIS1&v?D9JMdM%D3Rm>X|vB9^v~+p9&ed)Ac$Q6$lP3 zhH{%*IH?8fo$=XgI4DNkgt7seF!jNsApIa@hWbX%4&Fcdke7T<=kP9yKa!GQkMo`7 z0uKtm0Thh!W??072!*KOO+&}PAL}$S4n?Wv(i>tcd_fctaf^!6KSSD^nZn@S(FeVx zhdSQ?iZ{A%cE%fdK|1b@#5!d!G`*!03L8t6c4es^1xr3gR>FJmxzQ;wpQQkxku1rx z;*GPzJ4PPme(nCwBXgiiXu>7>BjGItDG zQwbToc83ia^#6QodYcNMh4ymm`FDx_hwmME==`-iJ6~U5i(!e#VXJcPT$eTXN^e<1 zN|ZFZYIlp2_Nu#`c2W3$?wm7AThSprN0(F`y|zfWqeOHF52%+Ew<8BGU50_y{-d)? zXAo*>n3p9*_J0jR5sp&QTmm9jHQtt`4LoZM_P~uxHVw;hz{W5y5*1L3H0vUp^CQo( z-+r=lXhtL0s0L`!iRl0+?&Azh%dDHw;^ZWE%^{)%Rtow!Tm`kKEMP38z?XllEndP23}2fpmtPwxFAF zvXWzdcJ}J%y;|{Lz4P#VbqcVgsOVt|w>x<`1h6Pmehs97v>+x|o50M_jC9sMzchmc zezXE27&iFOSlC{4nun4%Jkjau=IjqfJ-_>v&Q}Gkc!G*Q>ZFsJ-D%N+mwHtRRLN-K z)I%lFzRNG^hl(Gvp8=emhxE^BvUtk>&$p&mrv6;}51D`b`J2=K;JHy(tKHt$c?h#l zfvYrn5pHr=LtZzJ(N4(xLM;MKgk-^aXUclzlX2U`lieFw77($FpCWX-G3t0%{mxf( z2AM`j(HZoVZQvn5R>|XqaHVB))!jngxkxUeV_^n3366)qC~n0;q<=PbKXz1;Gyj;N zvcm0P2(A`4oQdDX;IDYLbfB{<7;|$B;kX`I6UgL^eIZbODlhvdS+UKrt{&;QWdr!1Otcd=0t;66=E?Hq;Uup6=kj zK^x&k{2n&u=RxoF>DUe6rJ$Bbw9+rggD7YW3)#G!{eSOiP2WGYzx@Us0Pm^jpW>xkz1)X8U&P^P zaMw#^zhp2q;-gl^v#oS+_LSG~&d%S)@5sNhjDfb5H?z{9#g zWJOJ>evi?eh0&-VBkh4CwxMdDAdwmA?+Pz96l=~Fgea{`8}zT7kx!4Ny@O+&&(DGF zE(S~y5Me_^OK2o088OXQ>&@8@>a4rGr~sEzlEF-$O6dia!!?}|>rEnk1BWCZ66fI{ zb~)3bj=rbaVn9grnJ)S<<93S5!o2%5h+1IQQRom*C#3fXhZXF z)-KZMXRFlJ=|aYDxOF*~YMqc;J&gd~SHca)(n@Edgqw{L zm9S9;3fuI18>zismbyP)lJyk+(fUB7VW8adRF(%HKZ5~w9ys93@Lh=BD?66en+<M@Wt4DxD8xY1)mRcMyl!urx|m9B)X0xy+|!~z>a z#4y$_wJ9-(On&mCcGPOjY&HVsX#SGeu{f~|z6hnDGS^}KL_yr1&}RzmXRsiL-sq(q zlMT#ZX>lqg$-->JD*^d-G+wyl`j32+_jmRec0xBUg_94$+Sr@`Brb6^{vFS6q8Q7W zz%c*2N~Yws@=jHJNrU5HYNCI6umv)1koE%W5r8kQEnPAFPqp{7rhb!;Kf8anr?0=J z)A{jq=Tpxh4KKm7q?tMBjq|@x#*D(sYoyT&q~Qp5(HSgVFfZa**|-oHS$#E`mEXWG zsh14n!9SVl5Pfwn6xE1Js+p_tfYVYw1Ygf&v~0hBl+weY8pTur<&vH)4-?oiS;A{2 zb;ub2fu-Dus^J&PJxYwAepoES|GwdtSd0_T7ChJx_e#N|9W9+eE4 z1yuk^iBwd@n@AYv|F-XLO&^`Qpa1;wPl3Sjw(GB2u5~*1p9lwam4^mdT9w5&PTe_f zFW9y)DUob4k{04qVgou_;!4P+@f{Oy$?B-TkTF+h{E_m5JJk9RRewTy_(3T1nG;xR zs-Vj%ww`o5(PM*_WqYmTa^pcM1`;P#y^Ancx{xri1Q|;$p>LWU-gkY=kT}%&#EqJ? z$P|^qa!1rK^DWl7372FZ%NdrDJKH;Rmx&P2n|ED%KC9N?mg7ZjVhN>DK83!Q zlgXk}FVW#{?+gr?F=VBBq$%rCN=v2mnlWoI$gz+0_RVFOL$K8>iVmN3o>ucY+1*@_ z%=FLfUO3_CVwsasv;%{$T)!+34VOFn;4I(=J3p<}h~?UFpK$HgmbiGL8B~GObIC_^ z9t-mFC(GxtV))?!;RP5xa!+gQF=jE_gNYO#M7$JRMBh5dq+_WWOEy6WAA{xpEjazB ze!u;{wf^^?brxYQZJbjeP=NH)ciCknQI!2=CoDivhr3s=Z)nvxukKq2 zofCNx6X0RA3}=HVcMORY#ZKZdUj*isvpTL^4P^zu#M$a`94hHzFg&bIBgT=OI#z!(6Gu=8+NO^;K*whS%wZRHj$Ei(Hu!R;>p=G-C0S)@ug8c37{e&&hAn z;LCB^<`I>SrJ&yguZXh<1Ie!vuy)}Rc(Wp3y3UJ@FqJCQnWwb^ZiXYVv_3=eO#1(4 zt?7HGo^1bK>z@_z_Y3{s;Rmj-`;_nP{K}0IODJ5k!w@nMU{>Gb{g1FFV78|+gKyHz z1$lLaqw%l|$Y)BYK^$xzGhGUaf-pdK>GKX^BY{2f^^$nCZ2U@$sh^FQ2hSFD&?#JD zB|2j5FU-Jwvf%a6FIYAlmkULr97q=3Jh~+Mz>ZwyiTs0?uCECsgF~Hr002s-(#{-* z3oNukg8-4ylfB9ir?}kW(TA)##HGCYsNE1Kr3c^!)qsxct9k|9oG zcbVOz6G(5t?XLy%DtwhTvDWAtYrm=e?IN$rbsFh!)}4wTT#JX2Vw)^1J1PLz`VvKF zUb?>G9Ubc2-JsP-D$qfcU?Z&4IuJa1CDl8=($b|f>bRICdvz^^1IV!S8bKS@h5(g? zfSe|Vb9sxx5Q#;^dtZ{B3-p!qPwZpO{oOpRAI``GZ>bC#ipr2pU2`oHP_ zGd(l4$Y1_c{~5k={h|)^&6heKJt+YwkG9|+t=%(&3~RzgH2M*;}m?w73C5& z{w;2sZhM`4x&ozpqic~JK(q%ZLfp`xa}!qB+=WY6MDPr_lF1UqV3URpacf-}#^pKJ zQ>qRTaPpF*Xs|j-nYCmf(pGLHE<2WFRe}rp6*bu9#%OeE{n-f4xPV&%R+rGh|a zP(KNE6mi@%?QiI(y0wLFRLgYjbEg+iSS0UPX5I8W$j^bPv;{G zJ{Z5=r?a!&o-88kyTLNF3L9k0s~9l&u!Gsq|L6JzgQeZMlSjh*J-*u}m*E=6xsNz}4NQ?8dO>TpJ3OW-Jr9?e26JV*3Ol1H?L@ zV5rmez!i`Kl^j?%E_7=>E2kWksEXIC_$+d!cy%`XpB>(P{handZT%fb+-FQI7P6;R zyrb2;3L|lOg%Mb*tbnML`aYXCPMEk)SSvzGv0^2MD-f!MCEObO zh_~?M$p}iglH)qPw`uq~m{g~a#Vdh}cVs=8#TuILh~n3;F9|r?FLXXMpBbNF#3{S0 zI+abJaCxgw%vFBMceE34ZT*Z@A$CHlH9CG}SqZwNKqdWDWnsQa>X&|4lb_WFR&EDX zCPWtpMg%`{8UIW;vtA*E($i6ursVHB%e%3g>)cN2soBtUjH3(~U zQ^)u=FpzoCa3ZYyW%gPqm)<={dmQ)b&Ll`vaX1oUmtDVGYv-HX;9wa0@VW z`w2~_uuhoGr-EIoj=zLc!d#N}#z&8z{og43gw~Z( zS^C3s@Wb!prc!JWYEw3sgK@aof-b4)!0WTl3;0mdP zR&A_|%r~}CDc0p8-aetP4-Q=K8=+2h-WQfC?^!JkZv6hq-njy?R2i8Msg{(5uGs61 zKvAec7~v4209q-qau(?Y!G6n)>;ulj{t&`A32mszQSXBgj#WUM9lUt``#$`KI=7#e zZ<6yi351o6h|SxYCAypID@&_j0h+F7W@~MAiv>dZZtYZjN_B#}u2ThvdM%LqqMhJ$ z-MoC*f&G1Zj{^TdyrtE{_HhPs+0@=yM!-Tdo@)~gzl)oO@b$zF2hUx9$s63)dG8$W zFDi&YLkDpaUv4C8dg41OCHk`X{BFAwoYZ0Y#lmT8%EM5lNZLM^Pi%h%gbJtdGe5KiQi4H&c7st4#kq z`P0*X-Z}Wp_3s&TpY8M>l{J&PT6U|@b_qzvkl3B9+xx7`c?|%a0)M$0(5cix_EjLI zF4JsC6VP|gq_=8^AYNMMY>c*6EyQ7rbhFFXU({j;Cpul7s$Y}>rr5!Z+EpQzNTeR3 zh$m7i>V@?+i0&pD!$fJ=N3MU@klEkqENDkw3kWYthU*lahK;?twBmF_lUyKg=?av? zz8${f88Lp`ljc|JVF#NRJZ4kzx-R6u5X;*V0E zV>2RHiiZG-)CT2m@L?4Z-=}y&EVr8ZQCuK9LV>3LY21EO|DyeO|CD(D2vVH6{=83h zu5+6(01gBSOYsXdXt@7y7hMc}QN`EM!2s7yGu~s=MM%Z3*1Ju6oE?}hUMi$BCgKRR z?qJ}jeOCF|NK|}PG>_iSNnL;_t5GGJ{c}qcV^+@diJDE3YGa73SeFQBmdu-&e*Spp z)&(R*9eO~&t@9O1$@u@z-n+ordEWJ%?|F};GtR}1b2GU(9@$AeN~Gi5949u3BHOY@ zaTGhzIL2{8a-Met*CJ^StkSMkCuXhJ=#&_%qg=^Io3k|NQS)M2w^UNf~Isz^0D1J~~g(_Cp7mW}Z+P{`$ie*Q9Z zs2Dn-_XX5znh6xp^qF}o0LEUg076zEL*=QfWZ=||4qcr2P2ul#PL}B$GE_?vR=N~T zTD>?3Go`_jP~1CFg${4WSyXk_ZSpKJy73rwY$1z*Y(tV0-GBbZ-BwCoaY;F&c9RPR z0j4bySr8*bL3so6QY-%{*%oF)kFz*)i3ViDVxhi$%R&f`Dij!RPw{YaAGdF4vic>1 zOD?$$LZ8hwIE_=85)Vz))*3?i1X$r{$CIJ^F&W+M-@Bo~>E!=UxMxotA*^C33LaZS z1W^H&&09bxOR(vngV9zebTVbT3IAb zWUvJ7)*yD-5s>uUMjcfC>Sfd<>9bDi29ZUdx9B;Y_Z~+rU}}+vzY4bz9wes(ECxjb z?N@B8LBX+WJ3hH!Wn~R9$a>a4w&AJn!?lU&FeSfWC^wYyi+E!H?C0RnL^_59842+m z4oIq9f>5aaHDoJ%QEW|a0|1%Qp#fYbQs?nopkqYm5r^qralE;%>hy&-SA3iY(vtwC z(pVH62|CXwQP((U+f|RwRR>&{a=;;PSIel7b8(ltuA{b884gJsM3m4K7FW33NkUw` zh#No!FfnT@4F}VqIdP9M*OuddjQ(mrhzH;b{_*9XF9isEeB(}uoc^f^cVEjclJ!w@&|;{vB-Jn|ewxC0X`aF)atcnUB|&723AGx?5#k|_1fAV@r)A#$ ziQAfuInlaPU76TK)lfmYumlwR#yD&rq@$~`ASny7vgAl`P^-Goj?&7bTKOfDmW2Kw z9Ry1&To<(uEpoRk@Ofiw)hY%%&rjJT|6tR61KViiEIhDr&~Tog*sn~;XxNoKzh(wF zNqD-tAe|B?7nx}X@kU$IV^q79;N9xQ{cRiX(3=O_CvHt;6XHtt53&W`(@0*9xv&3NJ2vtfo$Px;oae`7X1inYyvjrV8RSGA`^U|1b z3Tl^TRj|M_)FG#bzNf?^V}%MM)AzB7EuV$-Tq8b5>yzrKhQOAOt1c#YP3444PL=kN z(YC>4vaV3CwL5s}#sNd?`iXrFR7|1VV=e-%palVKT3RfJ_f^>VwqA0{f$@IfT-zzd$ClszqUNo`W1SBF8z6O zfc_mDw`()&&zso0Psjyh748cv)hVl@Ub5n(hPWArrJ`9-q;m(`VRGQP^0`rqY|bDg zAjG8c>S`rXaHTtQbFJ8`8OK%+=9qwc0Cn@*5t7o3q0h~P-oJd~Hf?8c)x=x1opCWu zsr{Vo^DL+7^Ith;dDIps7d;)kk$V*COqIgk3g=omo~;||lq-6wptt)I8~cqfrzhO! zDz6DA6)ik$I6CNxQzCyND8*1h+!<648-3IrLeMl!G6Z% z@anQ7KwO)u2*Nwk%%V`@fRzxqvb>>~8gMKzX8?(OR?~4d$0K}I2>d7(q6g8+#w|X$ z-o#D&xTOnFp2C0c!#3nCvYUHt0QSAcf}wSdbEadhd3B>B$@tq6upA^ zpZAxSwLV4$VDD+=0R2lgZZ?fvHsNMa#z!FHd<>3XZcVxd(XqwKoFu(M#l|Q>Skv?x zGn#1`$hlSSP;iEdGkV!v81oQxg%|k+$g^($85{e&n9C>Ld_a<4SSSyT%I|cweE_-( zU(s%*)GbsbMmHanT+1K<8Nay0#b!A9gro!4Lo;xU`+U@9-Z&bOS9Q)?Jgnm6%QoKT z)x2clO*8gx1bYT6tB8@8D1Q`7%obguP}z+zJz*ty@16q8<3<9qeoO-3n#0oJI<9VG z{F8=>2tZMA43ZjQc6IOF*lQfTZsLZ7FpkigO36-93P9!wU{?$x)usiqwI!z2oFwoD z%wS#(6t`=vO90&y+}C$bym4yAhbZEa165!(&`rR1QS0AZ7Wz7A!0gE234K?WE~e^9 zv?mYi9No;pIjVe9FwrJ+RGu%Q|6pTs&FcRB9gvU zx!djU*ofKdmrT5VMk+r4g|1P7&+>uuMlVqNSZfDPS5JthUO`&z+t+4~>6t0^H6`JY zCXQBX5j8=er`>Ic`{c~V3?W6h+ke4E%v8U2LXDKhv=UrJNV+Jf7lJIPfoawoGFxe? zldF`>*!2YlMMh+RC+Y@8D|OZTj0^j;gBmUpt(YB4JY#47O&cDXK3JT1E!_h`pw*p+ zEv#5Aat6>s)BGd!Fu{`khg2z$w?MRUe%2FtAg0Q#1p*$2fi95KZXHh1cytBIEQN;U${b2D$R2fIV+zx_k#)<4C!{~ro_LLDmmua64LNBS5Fz$}N>nEU zWO95F6cWdL5#F*`_B)sNB=2D^JyZ?=F4PaNLA8?IwhzhU4>RomA!p^ODYNf-*nM_N%@)9AGM~RR@who=wYPmc?kXB?uqMej?6a; zwM@ds2G|coVLd0IXEo*J@G`V26_H>N*bJVcLvpgVqTxNk952ZA5xzAwP~`wrt$#$N z^tNyl8nKL?6>oF{CH!2s;W_Ao`zGAf>hb0!q<7D+YHqOA2BETblpK?AYZUZa+Mvnn zqAi%YB5~c8A5Ng+#L6EY{&HVod#hKG&!@0r+hVHtth7Yp!Q5C|9| zgkaqv9N`%RuaK7~$puNoFg4SXZbTK)de-RTxVp+obo;#xPxfBlJF&asFw-f6M~tCL z4@j98etk4wTtrj^rOL*THfL~nbhK>Vxl;jD5qNe7)uLRN?It9OHPJ&m@)FaUolXFE zIwHm@f#YlgU$H1w$`*(}!E5i!kuQnpYzRBuTM0MT;P#AGfK(C07ibK0IxxM)m#XY#un$>GD{^M@YMqtz z#O5>=Lq9zfKsB=t!IlsdKNT&!e&*2YG#xk{79 z_?K1c!P%A;cM^=Z1($q7leeNGu(w;8%0N*UjfJ91tr(%od@BP$5td{G(ByoR7QS`2 z!Ur}yS-d|#F(okDNd^P1`ramYSPlM=4?xp;#m#e(2?6l-0CLi}mfUz!-toJ7vXX)I z_{Y;~xw(@1|I+{G-tykow-ui$au1;Xr5o4TIrXB6Yqg}R3}I0bU=r=Qo93JD)GI}k zqzM&P#FLVXd1U(}2F>P(k(mT{lQ~Z?2b9tQ_Wv`A2}6^-|VadvQTSMUzA7k0*H`PB5eGj^v-T$Y-6tj%*DE z8V}EeD+~Bo^pqf zRa=*E##wGmU z64}yY$*>TtI07>c!De^=nvIxgf6K%*albd2J4J+VdI|#tEnynpw4+O@NwQ*5;N$8^ zHC+#dptm%;hp93rPVkAVP2B1cr4wBTq`jaERJeW;20ASudje6gYH~^bzxks58sh(d z*!pNOKP~{Cw!a3KZoEqPaOOo5S1X({IR%OIQul12C%1Kl;i(==KD9K(h$}dy%wpjO z(GtpDHoClM?nI+E)Z>)Y3MbN(X5J~^wcaw1=6p2GzCMO(u7P?ug%51?v2O}=aPtsJ zrmU8N$2oP(v>NhF<8@mLM{{0g>l^q|n9ZK(}jSCrE;eyeLsx9^uA{CG* zwY(y4{QqFJU{&41CNV^DG$l@kl9ZobZdz%WBA`oQK-5yVO12=9b)(5G-ESQ(}Gqga6B+2TO=>2)&~S-Iy@uIbr#e z?cigV4eC3p1~1lRwBeeAcl+vx#H~Qt zd|Z;||7}O?Bmdvi`h4qQ{`sZ))4ylq6+)EtS5I7dp#z8`hqGNIA#1lxcvDHv4HFM+ zXmOLs;Yg;8f(_Vh#AQR%^sUV8wRsEptaQ1wE|>{yLU-8P9MaTxB%0IZ2oVq3;~co3 zI^%glb#s$9D=F)A_x_ESn-v_Ic&SLBR`dKK)uKbTq}0S~{9&G6X>Noch!|45*=*Dd zqI%<|&J!QlxW;e0Y2qap!trLO5}GN-{o0t=Y^9$E&(RtoYy6dwd>AmXKOYA3$&DDf z{LzUQYePBYSv$(?TEGovxIx+X!!pod=H}E`DY-P1w3VQl!aMeD+t(jj^6}FB_8wldvD0krmWdY$1ePiWQfdd9D}QhaQ2j}bV>Fx%Jv0UBHB~~% zzYC;uQ<#DIAstN2CXf(j8HtyTRwc2E8igt>xTybsM{#{oey04&)}OY%qxF3Lc%q;F zeH$_8`SlZDaUllXS&iP!2-!65%rH40MuODc*A;~!*X>Vic>43;*%Mbx4#@yDO*^}(Ly_u}Q_Ux^{3Zq|3E4o%FbK@i~iQ|H3_Hasty z?5`JwsA9e7kWVTgPx``9tH9_ai-$#fDRol)gw%?rd(+01+u^8d6EC=Vid#IfsbSp2hl0ne3GW44Ttd(Jxw5Oj>WH;(kGVPySjsS zZM;P9d+4f(=i$VC7wMa)SC8-9mC7M#%B@nn-sFc;v{rm7C0`0T^2bj22qgkS4&@$u zkvDE>Euy*T?uNMt3T2c5`4sMu8DXX%#XX)?S%(opjz$H$lwsgZtH{ZWi((1fuWF%e zd%FLZ9$Iz(k~3SLVtmtcgmmoX%>Aaej7VoCv0nQYRaT1s?I_L^?GLqE<+rpxTl^M( z{009U+3=|G{w)*Fov~#A`3mMo@DA(>CFa5nhT#K5L27luh8rkR);D72no?^!N2T6h zwc%mi>w6|HzqjeFrx~~7?`bYcod{TcQCWbF`7oLrLYBQxCnx3Q2#2#Cn7iaVajB`B z`D#&P@SzYGlG=qQfN{%V> z72qSb*i5BTw_63E#5b4c)*i5$E?lORmC&BG*IfF=6ApNAJ_vh65*rdD zW`eK`;8SY56{|Me48EZ^AE)6=8ljZQ$A&T#s{|>n<3rE@eey{Bp+!#j$c85*gBF)v z$X@P|VGqqZeh?y2?5#d=%LeZQpvzICQ!=R z7}Lz@6Hh$X)hX*R&Q0kP7nT3pi-*boZEq{z*E-BUzX(6jrso7ho6qveo;-fqxm%p# z>It>YGnGVw43W{umY~_SQzgvkI9g}MyCZ&$5ZUXiITCVWs#5wu@bAbiOi6r!4xuq5 zyYE7q&W;!KPEv5f@wSBG8ajD=hE@jSN~Q0p_;|C`d)V`Z*XJjm>Ag+YiU9s)B(o$4 z+E94p#PV^aYvg1-O2}M5qEMS~hLR2KQz*S^T!$Vbez|%-)j3Qq2w7xK!^fUQpm#ya@mbkq}BI=}s|FofZoJBCb#$J;;%4JJfQ4KGmGV z?^Qi*ZU6>E`7?Kf+ucdzKr^FyzBi zuEjTHz)cTsc+hWuabm}<&Y6+-D=T9a1ZEtNjgGRpC$+D{AYnnF6x3a1H`oKoa+P!R|EyWQQvPB=N z2pSx@>!~+uyh0QMiLoPLwk`pkQdMS5TQ^OVnFdTheap{7Db6GM{Y_ z3wVZGLqWydCe~jH|DL2T6g5m@!z%zXsF9iYOw12ex@$$W` zztg&*cHj<%`X4a&;yOABzqntL3-luJNd`045;=_9T(P;swu! z`w5loqJBQ@wKw}AZPa(60U{z8h{SSAF zUkf!1|cpbAX}ONGIDUwU&olV5Z?WX_PUTlhsC+VVb;=;Y%$e^NSzc*sfQs-`^>I zvGEF)6FrMT0xhto5G62`w$1{^;s)$f-nRgsE~c#{3Kp!3##`l2O_1s6a8U)gd9eTx8^E$sf@ssZUjctbR z_D=B&Gmf8Djqy$_P>Ep{K+^LX(EpbAqq;044xT(Fxx%rPe8g;pVZZWbREjB+UPu@~d(_ld5KV_* z$CTpwM*Rv`tIDOS5sa}R2DiuQ5zbm!-8sLZakORsV5j&wojt@#1*}}gO_ey_TnAeG zRy24;#KJKwe%R0ur!n*{Ft0zjp#iXE|E5myvv;U;1~dhkoEadywV6^Fj75dyJecg2 zS(Wa_hc-iNXBSYIB&fcuih*wbnH%_F0Q2RY;XoSf7YV51 zVU~`#6r;5Opvl^A1PT;=OG`u_hYfG_F&Cr`x81psMjn>^{hi_;3Ev>7yEc!v))|F( zkQ7g8JAqjBhz=K1!Zke*JE2|lRS5OSO=; z4vFO1d3vP5!)1T5Q~Xp-N=92h5ESMARjaAQi4vRV{iXmMi{Omj60wR{i}cs{>~j0mj@q%+{|_0D1!h}+{`E8 z_0o_UnUz44Mh&G9ylB5#4a~05y|W=Ed8BNcI*SUjdS0jaUwJmB6Q>|9pvFG2I~_y_A0z_W3a(5| z>`Y8pBx}i}8|>GRpr$d8;KaMZjIfZM)hI86LaEF{}Ys1uP-M;5QnD_PQ)8f*pi;cP9sd|`k&&8qP?5?pWXZ;eg>D1H2tnT zb7iOaaqB_1f#8dwHy*f<r20_jVl03n_C6(Zi9xP}jywFix0R2E*mvOj?C#y6LsAN8E?bm#X zvraWinMJa=WkS*zxQkSdN-{FBov~^@g~k8s65?p5o;WIF7&6`rfz|F^>8Fm@?g1Q$i2}YzZdJ0#oJN7?Tlj`TiONe8^8e?H_Iq2O zY3=(Bec0h}6S$k}kxV2;87S-Fk$TE1@FH7-yn zt1w8@{epeUb3ZoHM6t4epi}(M`{m}~Ot5%!4M2tbdHhA{SaIigf@_im;1T*u1l*m{ zrMG6oc>UEQ&Cx0!epjdXK~c2yYLM5Ol>cQ}9Lkx!h{}Y@)CE^toWpPmA zX)3)`tKnr3wHpAKx#)T13m>C@J*9d92S+@()2!f0-G`PTT~E*A1b59RpNYehC=i`kVWFA0(zj%lIxR2(PaK1 zKtfSU`TsW*?T?jbTJI@7QQYvPMER{F%`PkZ?N0FnGsiJCPNDR~01g4X5w6%8QYrJP zKQlU}=d9yg{2%XJVP%{)26s~VF>h|1oI*!cj~M%3-PnYG%e&a)O*lR(qqMR`fn`!E zqv}SWV115GMfV^E42un9&B!gDXtw~CgLTs3=@aARbvS?BIMGKQHcTzFliPp*{^}eP zi9DBP@8DGB0=-y$ZF^j6gUa6?zaHq^@F-nky*C0*7- z)(HYWo=1cF8EKU+O26=^WQyqUQ<>bm=hS^tJGEAklml$;7y+e{wWuEWj7> z89~eDFz*EoXpy039(41{@DlJh=`$zR^> z7o%BI(MLPQ_iNuQQ7pDbF1sgU?s={-sr{R=0FGu;2y+jjX{WbCk-5zpoir-~225EJ zR+yC&L|HZcw~I@Q_A|>Hic8PDVx$RPy3Gi4|#+EKlJA<}Pwrk$!JJ z02{~}9zcp?z>Vty77GjsPrFZ`e;$XXZ?C6SeV$6-FA;?0gXt%G3u1_(%xR-SxDaq1 zh2NCo^=X)m2}bJ_x-B8Y^5YKW9m8!Q*=S7*ytozuC3Mjv*k)AX6HpT-1A^&LqAJDt z8Zse|%(?I{aj)bzf&3I4XG&SdZR6o8(_*9bl_>J~d?o#acL5cx6_UaB4Mu=9Lr72a zNF*pDcQtAbYmD2BqwE~_F$m1VltTxbR%)VUaF|gZ^S{GUuT7)nXM1{8<|)= z$Ry3SB( zmA9E}%qI`#q0OQH|4>o>dg~8buPJ`$NlE`N>O~(Neb7u3C*1cOlGY@W6J1&Lfro(a z#=D9x{B4zB1M@gMKWq1FV)?aNcH`vDMs`MQtV$0!;oPMFrqqquo&C3tGzG34;D!2I zLX!IZ;!0+;elesLNUZ>K%I!f|$bdjz^JW<2ku_sb#sRGiQzZ(I0{NBL<4zeBqR49< zrh&0tol9m3dNezHN#|2RsrPLWX@MqrZ{l@bTpy$Mh zI=4NlC$QjzKr2g`50$PGpt*pSp`Fd#Yw)s>rn{AAZs-)>wJ(xWkPCNC9$d@mPl+`W z4qfsL_YnC=;*>>5i?gUHGBgGKsq0*6VJmSyR#2?sVAm09+H|v~@k(qDHV^m{i^g6UA=FB;EB>7a)!AU7t z#xJcd_mGNUMjWmvQxUd$JiIS&WS56aHR($NXPW8;d+YY+Mw(_(DTg*a%S^3+I*lKUs_`b^ILI??~Nv-ukCh?f2Qm?;fr15p-@Y+uC z9l&gs{`Qq>=9~|T8W$AW)@*VFYB>0Kk+#PTRR+sJm=NC^^#q4xGLz5+i;_L5*E`=> z0iDx8`NqjZ&Vx2>OCX_$9*ta1#N++6Y2+1#9==2(WO6OY#;F0C*0u`IMmXD$HKv-; zc*S@#qc(%{CFfDzw_|(U#2^LZr18P(5~>|nUiJ|zWd5cELhKHl3)&2exJIf3&am`x z>@D>S6CE0Av)Y){S-tzggTyDA+6BN$e!o-6IN+q?065YVI5AqXz`GTSz`r3oo|i!7 z+q;CyN^XF1tc1I=CUKJ)GHD!JN-`~#3HU6C12j5t+R+n52=QjVc1GJIeSy^( zR<1a_(4n+iQrci+R5-?izCl`QdvLTs#3@u{aW)(k)X|ihnv<9 zk2Dvo9OA$F7Qxi`7x{5Q#|D>BWrAkdsO0 zwTm{u2S}7g=N1Gr^2$KS_=b|Nv++q%Hi88xy@_Xr!Vs{Z2`#uE~=va|mNL-fKgfhnspE z)yD$vG%_4H&0CylFLPCIRz85(N<6?9^2BOS0elY86H5emWx?g)!BW|IyHpkgmZ+k7 zChr-0Dt}D#;Cmni&Q8n+2D+jICYJ*r$e%1Gg~dv+#b_uv>87RAZZGq zvFtf`EY#NU-eQjC%K})CLhx>Fn9C#GY5mwp!)D4M;kvKi$IGnnCdsDMD_(UG z>1BZ@nIuu9q6K1LVMq8#1XlOKt|ZqX@md~<)BN=0ArkI$)aZZ{Iga%{lA3VMjB5@S z1Rz=@ujAjJJeX2+>sz!ba;&jB=9erZLEQ*BMYr+BrKBE8)Yu>h z14D{0vHuEYxBEptXaPg0<7DaT6TBs1W2FdTB}OPd3<-BZiN@gSkp}0KXV721t~zQP zNqE|$5YglwD@NH;O_TCx2|o2HN1gYp!}YY!Df zES9jI60!1I_yU}+fXt)eMb*24x5Of9+f-K*8{%DLk?reIc!`FfVa$uDlJ4SGV|UnU z-N{=?h1T+8VTnJ;M<8rTU81SRqjSvM(LG{~$^XBt^{qwwRQZ$TUjFgrpD!5*^sgFe z0&Y2=gyuYS0mhkpbeyUnbvz>wC#FwZf3_QREdnkI4b7cQnnTECBo#)#BNmG32*HFL z1jlkcfv`H%Nr_BJ%UVpiWQ}`xiWo-s<0Fm6E&FSo;#}<&l9oYX2aITsTOtMhRD;li zmi=KCdl9qBA{qqF7lZt{Z_;g6TM-G3`F*4@Ye1~im>-j6R;xKKI1`ow@h7H0FG{y-uyqTEmW=TY zq5XT`pvFmL$I1~GL9$exrOw7yfUi#RzlV$R56gXDoZ5ep+xiV7O`$Cxp6?WA_p#T8 zSdQonJk-KKd@=Z)xZE+slR$Hf7!ZAu8}_49ak$os!W4-yq(Le6E(6a#Yy!(+A7{`<*B;;4-*A`D4kACC{=R(x*hi<4PP72UEobZ~FMwOO> z`#QxV)zPssTKXut+G@Ijo2yWS9H=@Ic#hDKHLmtjKVjdYlW<$qu$Qg5;8?U?b$9eC z$q5UgTUIoZY+;Bzna1!+I-1w5NqL{ZMb_5I?w~W$bkp+8C7t4%wPt-|KysBCYT`hX zQ@Ul2_!?KJr++XNoNimNlqn@(C^scL3t^JfO`=uOoRWecGzuJ~NOB4I9*?@mbk>l{o7T9A!M`xCqc;SNAm802)y=ZN*@)+GuI2|?7x zA$gEP|Nm|Be_NyCx4&dW-=EJyNKZXzT;0_vzNwZJTjhiqIfWK#FeB1T_)~cYjhmb> ztY0la5TZv{NIOlME`M_K=`04IMJFKJ(@>|SW{YrkdGFx;Bh7;?AG*F%d}DU=Z7LYV zvS=h$@q%nTF7}SmFCYRwsm{k|Ok9v;_GFJc#V=IDNSA=Hur0#bZeu13V~`CryYNzh zBdL*9jNWO0V40TU`-sycXmgqsmp|@vLJH^mtr$1&l}N=eCtVoStzw!NJun?Z1=RV~ zmAPXoQ5C5u|5w9ho&WpSMe_)0fJDGR>$M4&MSRV_wPT&AWPn4HO0!Q%*P$4J8ez5S z(zyrKc`7sq@TI=O$UfA_ z#-1sVQreaLk@3fJo|}w}bt*_R8Z7$XUR+g_Kh*l^R}5y@IdnRrA_7!)O6pB)rc3_GPd5DCijd((BKx#i}U zs$Q%HOdV) zB$5cF3-CFBntgrNOt=%gBww%h0P+YKr@cgW+7ze0D{||V1GV?7NBjLkBG6we1+~iY zA|botAqx7+Y{zL)`?G}$fkF|64F8SOqC^pp^#fS{-TuPpRv!_S51&eiowEvsFwIoL z1FI`Nyl}lEL?kVT*)rknz&hAf9ki~h&a0CP2a5~~H2c|Q0%W00@eZdr*rk<)b>#y( zD4`_^lYllv6)O-F(tY@XfHU5K>NEr;)GyI;oo7(jH4NT9(wxllq02hOM%r&92xu{0 zEgEP>4u=*UX=-{gTo2ujo(9SpO&MuWx*=`~lor*zf_tp8#FjyNX)4%OD(ef!hMwxS zZ3qO1>qN%3DTJ7C;-jI42^P9`i(|<=E<_Eo1w>w>I{e676|W^uIZtXRQDsQcm}d1L zQJW7^TBd=ZC2AH>I^K*#`dBqhstcY?`M>uU?N`zN^B0SMUEJR!|EKle%Oj0{Ec^F% ziZePlppen!9;gYtAYTN>h^!=JTce2sG?xo3#D!rRXif@rpyEY9OllF+sdNJ2Lg(RF zSsxl{He;&dM8jWAcO!u}ewzW>{_=jK*I&78VN47)y+RZq^;FjB0 z37*rLO&=JqfUB6g&Zj<{dbQv#j#w!s#AQ8;2TFe?y?KHO=^TBiTfbHrhRgzLK8H0m{^?A#6V~H zzU122Y>GEYH-J|VH%g4lWv{>l<`z9?VJN|S7SiKvV&VMVoJO2;he5Cy_-g1+tDai& ze_MeC?JLDkq`F5;kt13zC20U(-f0y>kAtL zXDindLit9}Zd~J4mg}A1bkhV7N6R?@kOUUW92SV?g1`1gA>a8YsFl;AyZzN~f+^DE`Pf3J)r^;Vz1Jv)Yi%-H@hwf4~JowGO5X7&w&jvCSuWs4Bi??nCOa zR~Sz7^=g=iJ_3VG4gxD*(H={;n^yf9-c#SXg|t2fC5nmqef{f48k}4X<~qeg1gU_u zVKgR?ay-R9LWEh~{c281emGms9)H&4+@R+#i{v^VA8?hghCE55xSL_BanXm=(W0fzH=&-BxRY!jr?;K zac{-Rz@QnCN`i5`bPtVU_|i3<2ja%iPE*Y*m7cTnR9HfOacs>*kp$;Z%BASp9{e7J^EuQ_3C)+jh%Vs>~TneLe=puDaOdT1pO7j^O$=k!H51}mxf_CD{kL;O9 zgHGqK|4HUD)>Sbkah3FjbNVp zvf^6Yd*{d#g!(V(tgyG-aZ}|45(&?K7p@$r#o z^3XKu!#WdfJ0p~7gUgbh)W~wI^fPcMTWM;~YBy~%ZSbhTO=Pm5TVTencDtij2^sI} zoR~ppwaIE%i|Vs=eDU7Omp9j49mP19Bc0_6f+_f&)`ZvKE|gfbMz%>rgGo`E3$u%C z8GWV`Yr2@>6np|3Qc4Cp({AavCU5WR+9G*Pz<9Uh|3vZWqJ4L}!~*z**4MOl7XOqF zz6d`zk35y8-|8&gBY%M%@%U7*Sf#bqmb?tL?&Lc`CHvBdxX50dCC7Sh@4?ZvM#L*R zi+~Y_Ex}HU#aaPQ&XrL1Llr}1;y;&~j33bM_l;iZ=Uv}f5X$gK+!fBDvI4KZ)_gqW z#Toi^->ewM2~Gl&Qk=9;(Zfst0b>2Xl+ z+=?RNFq~)=deidg`D zImx1SN`}$M6-nLx%_9#IS#Ndb_nm+=Mt4$M0NF;)L1a^nt^m zwcrxZ<@(wJ(8*6NU{m`L_a8fbg60ir2hCZUHFijjI8)?F2ycQSca1!%q<>}Sc&*V( z6-hvBrh^_v$qnQ11N9UMM6>Mz9@o}sMLrAZ# zNII~%kvyyrYU*UGv_?n)hL!w$I3Q)MnG-?)b>zhgm6m*Dd1Yno1j^WIn*Y}?+F#S& zUS3J}|NdVv;U67M8f_1Bjsh5xt)Lq+^EmR;w1#r_2_X$UYFWucKU!2+EB2?&%g5aO zNDz((H6tB^>qegHapu0xoQuV+AqgBg=O6WnI}s@e```s9Q$L{eu9{n`F<+`6vEK`+ zT%pEl)+%dnnhrjYwt=#Zo^43oMgk+;BM}6$+DuUHjjSzY{g-Fv(jr^eIK|PSwHgHK zEQFQpPG#34WYrg3n@v3{*io}-v7GLegGdsSH`2RzcI4?J)KSbHfJ3HsnzBf(5}Cjy zSaKku>0%R;PI&KBq`76yMD!fblUgF?#0mI?*3`sN7M7xmNJ;s$d%==883|vLkCG{? zChSp~z+*TA6;YUI#y8D3Y1^w>?dFZ{eQP|UB76g z`oM0eFySKUQLaj*btIYN2oJM|gh+Y-i6o(CfYo|^WEuE%5PPF>!G#e=UPa{K%_C1l z87y}`FeA*$bz3}4&$l!3L?o9=&yA#6#VPofbx02)qP;*^KP=4TK9$b{9xCpj{lRC5 z%HW%j?;(1!N#3RY4Q8K9o7bPakJTAZiixcpnUmAn-p>A|Bab{8yr}clIz|J4bs#&? zM4WeR$ARMEnZcW_a-kNYg`=v&)RMbAjQLG#uiMI*ZCp?~d`$L?`zYfeeC@~sO9mh4 zykB{$08!&i>H`iHJ%B`7e2(%d+cCw@xR{TnQ6Ld??oNvJKBNZ;n$=u0pN5oPg$Fng zbVt@A)`+@=faeeU4>M+a11obMq9X&ZQM%;GD&fZRwcmns!pxTTr|A<(IdwK!$y23$=4%IrKqr0I4fg_fhrlvHQ- z$&n|M^gr5pZ}P2#O`*(NtEAG=!=ezfrZ8tETX~R7Np;mg3&%`B0;usp0!>oCIgCwq zY-5ju2Sy$s^3Z(eUSWBI%S28Y$LXxHs>^FQoWmxq(x6)2YNqXqOH%}5k|W#Dsp~F@ z-RN`xenpjy0IZh&<0g(n1hYI+(-yn^4~#s(Wc{YjSIuy})$qqZ_*Hc4C8B-#&{K4W z9oTiy806`0M|akUptV9CsYoMhd$BKklBolXVUTK-tNBf!RedJ9BA;<1c(Pw%*F=7- zh69(iXZmj!-&~ZV)@lCtCGpemjXaNKeQ)QUeLy87G^!DpNL(5McWWDQM5H^RzyvH* zUQ;pJRUI)*u#wXfzc@^U&=Ux2^FD3#Q*d7^oBxIaY+m95g zVvRzr&4pLQ*PNA~r_gl=&m4K&%bDkO-t&&NmHSO&NX(G=E6XrP`)p8?m4!G(CJjw6 zFxirCL0@XDRozeAi$ZR&R)-eNjmAJj#3yZEeLRRz@epG?bWBPB{(uzBRuYBg5+(5q ze%V-(Rl>~9sCS_IO;x3um z+?6-xgQI8L5PeJMP=0*s64G?=BEr;bOugofzS2Nw0^^)gge4Y`4aY1>a3G#$h4>sX z+q;A;V;;}0&b##vF{{*1T`13^l6%z8(TJ|vV3r<3X$*ERj3c#ez$rZ=KE!iyOG`?p z{OGK7TfvXqrAiF1GCPTYyVNDercXdN-MdFIo9EijyF`nis%g&>P)!JfC#c34&FiFU zD0x-N0J9HB>?t8Q{d{REXu040=*VMw`m3G0wBdKq^?q{d-HF2~C!8k&1aHo4M?lb0 zZ)7|eloSi0*MTy;Ftx+7AqJ;z!{R4yq$h)8H zdJFT%VP#K;Z4e;gHRd^6nFirXVhaoS&&*mXy8#b!)cU-*a0(TKqo${38B* zaHO$1t^Qj&?|fW!KBU-F(&ziPj5>P9db@Kl-XSmAA||TaVG0qB&oN$MaW1v^0I`Xc z(o`ZBc!8D5(^Oh_5Spw8iGxbD$Z1VzcRK8ylQ&1LhJ0{nrHP<8H3j7)-ld0H%Sp;9 zgsT386&X9095DuqtzF%FMmr3tS9jh~n@kqcSp!X0he)Z`ic zf+eshR1hEQTBw~K5F;AX|JZ@$c%=a5KwT{Z`wxz`dB=;Lw~Iux_G5#3TKPa27$UMN zZz$aP`>urtfeE!u4y(1>qbanldZY|zMP6|PWucA-E;}rbM{pJ!74Y~|$Lz8(ifbBV ztrnJ3|B&RmULieT85)figv}(AEgV#6c(QtP!|E5sV0jwX1Rzm7nJh!Xmb0I3hsM2uB~mM&*e-k(24)F zUK4%=OemV2RE56afK@&PmK4rrR*cKtv*vA(W(`~NK^N&DEW?}%$LZty<$%!`c{H8! z;UacaSb^?eHquxl2KaTR1x9JUXsrtDhW99um8@NTL|th$D@uOFyOScxeHYFt%nCvB ztjLDmGMPv#uBl`2{E?;~wa&bvb34QkzKofAmQawkFo5)z09~Tf~ zv6mDI=S~mjFm%XFQMy5N^hPKrP8B*k+!*f4EkbU;C3z3SsE@0W#DwHEO)1k(S~)LzinW6of*qz7K}|@B+0z6^ZXRi3Q;X>5ZScv= z^QV!y^S{e~SiYVbfaT)gQ)B@}F?iWfW20JU-rU)Lu*Wv7 zJSA}_gFAts1`lTm0;7yc;<0DpaE(P^w|iFW2tad8!6-rF3K{3{c_>!dnw-0lYp)(< z%k_zP_dAz+$FY?|BNbNF$`hVbsEb#Mn@cgRAG$T)trnaY<*v|RYns=P(zO70_pYIa zTeS!_->M)tC86|%kUSJ1#D6NMFYiyTU?V+vnCv*QBpO4ee210p(okcpTBKFqQjzDF z84Pj)diWaza&(!Z_SNHg+JVMFNo#gF{gaGtcl*y8YSdF}@Pf|GJp0Kg2$h1-aED4< ziz+OH^2mDjX>l=&YgpECVF$s7A`8*0K; zYw+>T-Y*K9NFrJcBvGlYP;;NqnZyx-!8B>vjc%fD`r|{5&uTG@=B-~CA-g=)HW;CO zwZ^FEKUO6dcvF6P6_q&jw_sQI_)uf8T4b`lWoyK3Qhf+(kmo7U`@4o31*O!!uY5dm zZ`d0m>qtha=$n^Y+nw}Uy8TxTHA<;9cvt5paM+r?Sh5>$;Mv#}P^sM!0UWqZ=pSPY z?hB()n+dK>t~e?@1x={V)t0y+{BTM-8r{sh{dDjA;%f%=NUM<}p_W!nGSd`ya9%y}re|&L% z29raLR%)GjOXtnv;ovmN8DLN%AlTtpk&cPnNixBJSWeABxb+rhrIk%{xrq}ojNc~t zuDMCmj&SPAxvSQY>I+7fGHac3+M)7+f^MA1JLTDeEo0fm$p$AteT}Shx4_gz*t18J zTFLWqy+)+l|K_+uLyOaoavG@<;n`O!v~*r^8SjoOwLQHUQPgBQ+34%`Up&-^qt@WH zoi~vm{gfQV5{vQ_gEHC}W$aGGpT?As;5C@|{5a_gmCy*FY6qcFOOwE_9TciXiw?T) z8-7@e>tElwfd+8N$-Q$|jB<23N~I<>2M_aBa%LdDcpc|1M-@~f5lUACB@QT^b}%#4 z*re7&ujsr{6kI=y<;1##Dvg;&KIaRDZfTpV^AC!wUE{>T+T_N>;@#SfvZt5WJ%1|a zjHN|~Ip>swCFhe;)>-gnR=`u;E<%`rPN@iQF1oh?Vi$sLqd&SKuYWHt*LWjklWt+F zYlmeY5NpB&i_!CtyxdJJi1)Cbd9)?|rIYh+bDBlEP6j18B;pqA3ESJik!HG8s-pG( zcJZHz_RjJ=|NE=(vwqD`ld)P4-_p5$pG-Qz20Oa0&=;)W{Zwq)Jm*i*%-+d6?dBfq z(Y(cVw*I1?ENf(;bww2%#EzVhXi3inr#MT`IG7Pu=;YMh$3C6PqPAPvZbYqaMv?BF z(VQP?t0WivMM{t{xgqF{5#Ej-H*;jB<$(Jt0@z^KEj-*=kp-4eb%{sK#$5&qPC?@$xjGS(s`vp?p)V zO89~y=k1|piM0mp&KuqnXb-FOlXGsru7g|G^B001xs0rFgrFLBnax*u1*j3@jgBhs z32+&!c}@z{4z(yMq zjCxvNAD&Ywj)JcCL_^hT|DAU!L|LYZV2Z6EK42a8Qn@rLdd&-88VxFVM63<|=B4Bt z@)==p#EiLQ8OZ;)y*N^|_mtn$`jgh%_~+C8)4zA9(Oj+d>pHKKY-fRyWRp-_f8WMv zIM^9+eXLO%a9dqE&d4S=2U2#^DlijBqi;?B2<#yHl|SB3Z*Kq(LgbK#DKHw*6Ia@qh;T&xjle z?x=y0h8ST;xfIl86rA8etb)}9uZea^-To_w8YtEp9O%5}!h;E3H|-GiYG?{R7+7NL zi>;<{gGeg=fX)|tG(%|)8d71m)9oy;ohzW0dPxNG9RRJ28Zn}$u@_#T zSz6)+*J-7>tch28q+UpSdV(g(L)ilcdS1f|X@f$zvD_CLIC>tdZC} zOdXaMWXUWQ9w#p}$5(ifFL;cxTsELmP2xG#%~enf6djwyrEOuvP%5h)e!MXfkGOHs zpPoFfF4FS+)9h?aUza$KnM&~7-GgI8&D&}{bVujadM2C2$WVM(&SwX))pbH!O&-hu zHq;hr^ms(o?FGY9imqlqi%V%84HT{vniCjV;)$uRH{%q%8=bPhOddm4kZFva69R@}qlh?g*hrX105; ze3ghA-4fZ6cHML4m8@Ov_OBey3eblXNjl#Dcd|0dYZ7sXaQG zlf4InjFHz|na zQB9*OHoHTj5o5Cy2!D>ISIM6QO8OD^swT7j(FhQ%_3*yVE2r(Z6MqWz#j$N|*?M0Dbt13E zu+%pfj@|VMp<0XSI_t5{vguCl2Mf?b+;uliISDZcFy)>LrpU%kH&i6BtEqS;#JI^_ zWR7F`EUpu_du7)v6Cve30TaH| zyy#^`@dz9;V#RS(2Aq8qe$kDZ?S;jL>rl%BelPI zd)v>n|G546_V2WRv;A+|zt;X2?SIn#h4w#c|3v#o+dtI)+wJdZpKpJ2`|H}H_MrW- z_5D}TED@$&DMKUjW$`Q7EWmyea-Sbn;E zxcr*(Bjw3*sXSgDF5g$ar@X6tdwFa5*78l|>&o5bE6d69#pUzMOUsF}#0vS(t>0_? zR_oujew~gaztZ}p)@NJ)p!N4!KhpYv*7vo(v-Pd5v#oDveX4b)^@-MlOe|k$9c{m@ zePjC#?QVOjeRcb#?H9J6(|!gSO@CVcQTYeue=Prf`ET0a(fS~L4Q5*JY)!XrZhd9z zjjh+TUe$Ve>t(I4Xg#;}tf$Ddp7d?oK6id6F#cEPU(D}cJO2WH|LgP5;rFkfzl7ia z$9WE7+rK&A;`hHj_c4C|pXVOn_ivm#!S8=}Zh_zb*Ex=9+rK||nBV{Rx%cz?H_zR} z@BeV_-TeNobMNH$|8wr`{Qi&UZsYfFpQC%jw%6Tg4w-1Yqay>nd_yyF~Gp0@q| zxk-Nir*l{E`}60X$L~KpcPYRB^SKVc|I0b8i^U+uE`&o~^gx}A8^fG>5`Y15je%aZN@cTJuKg{pT&whyC z&ppfAw?FUf`}qC*vv>3R1!vj&_7|SLo!?iSeH*{O;_Qw5e$m-Cu(r>B3!C2dbLV-^ zwx2(*z5K#?LG>5U3#z|#UQqqV=Rd&jUp{{?zyIm^8GirD`FHU9pPlEK+kW*t&)oLU z&kK_O;ye>_xBYMDS@X7kd44y)|M&CS?~Bjw;rC0d5xo^MvtRQpESx$fZ%g+My?XNhi{l4-nkl%joBjCXHsYkThpZ%5{ z{Cn3Ug21aD5d>cS$Sl8iKLY%>?|DR!>^?%~&i3mb*~{9cu=JyTX{9wrc?=9LxA^^LK?*&v}ia+bEp~mjD9{ObG6*ej? zB=HU-epT}r1M3h{NDc||R2Tybj>;H9?c>8WVV91*a)r#m94DO;A60Iem3b82dW?;{ zJMy(A27I$d)2dCp8cxltLYc)JIi9@5czkM^q8Bt2QCsLJyr>TFX$$41ReFz^_Y`5k zU1H((mvMDvC5LiK>59}i>%dKe0eO-%=Gi$_N+4Klbmd8@yb>{~CDSjo*_|f!4UN}X zACe4dnq1w}Lk$jW^_M#@uL9UlQfopgL*d~gqsLNn-nTv+Z>{R%u$Nky&x>7h*FNM~lJ|UtC*>ciKPm{Su&7fO}Ig2Ui|J_CVhVt7` z1CS(t)}O(1hWBYNXRhq*JR~;@u(hR&rfA<9Ky+MFDL;hJj*z{GVySewAT8&E3Q?dv z{zoLF2S|JY)|0-Fb8mhrECEmH<$Lo0n?7`+zzYtk#s$V`tTGhG==r2Qs zRou0aLT0Gj@s{((#vgYQO9RMnVk>@LMXHsSHF*8-y@tt=4vCJyL_ExI$Uq5i2E*!B zmK<{c6k*@S!%(1OL8)yKw+R?XJ+DwG+cAhcd}fz3*y=~@+%xWK(#n<}Ri*d(iU!MwI!ZLq6z)mB9R4vz;7xbVbBb7E%d#2d&IF!vEBAjw4|`jX@|5Piw+6vEw&b^0MDoC}shz5G&$MvR3u zKS|!QcX01ebNX5jUDtWZmPEqg=naQ@j3_18?mj<)7R(gHa#}-5s?CkLg6-NXN0&Qr zPP|5`nZm^6wh($@R*JmTGc>;R6=+Q>%TSb*qj`jpc)XpXpaAzr15rZSR4hRYJdzs_ zn?T-PKsgabeq2ft5pQO#9_*ku;vCA1(}TnTmlBSSlg7YS<9^P}?4%04ClZ2) zPf`Of{5py&Rp}&KGB-b||Nfyyu(Z~1=)8C<;O3WP8Bwbl=nH1#?8zrd`KL+5J58dC zhA1>rnFS@7=C0U;taHIQ-wXtiWoqJ@lbRzv&0c zL@5YfeKDK+Z2(4LUG)XXp4pIzrQ5%BsIfFHnp1p5^_tr?6bE%Q^$IgpNCI0znIJ(# zyvHg07lC%2t`zYsjD@PY7RKTLJ@(CUH06|cCK9orcdD7L?a6RhpgBc*sDU$zpk2We zbGEun6Deo)NK@-kNH92Em;{vK3$J)5&$C+!Pn8&o=}}=0{m~f2A!W`vy@-8DoP?H2 z2iQ4y%TR+}T4zpmUKlxRpg#!j>?^=Zw!M6)P`&(_csuuvv1t1qQ(YA7VkMFcEB+A; z7&Bv0NP!1xnfyaZV6?&%k2x;OX^PkzKHC%U0Xh*{4N?mqNHL^9UbWo16=`Khx>m(A zd9gKHNiRUA*ENHmx3aebh7OR^z(O5dVwDofTE16jj=#KiqxoL&X~>zy%7cucm?IOM zb#wdPgF{VQY4xw@yg-WpbP-$5Sc7;(Vrf(Hw3OM6mW=(&>)a%d6p)m2Zu#FS-dwbQ zp#9Nyr+hn}wyXK`iGTWYL(NcWt>4~x{)~*TSU1>4uQ!iLNw zBI%#beVzu1D<`P|QQF->Q_gEU&lAW)mq302;X5b8`ivRkCK4Ygojg^Li}({#G)1PA zPp__)Tq5mY{a%2m$@z8Y?`i#c>1l~$p^P0v%?**i|GCo{C~z7uE(>ilD~1fs9ix}G z>Szuldo_^TDwLSSX5%Ymu(5O)VIt-O!GhGq9<1Y+sT9N)#K26t2@N%1qeVBN%Z(6u z3dJTP*t@Sff+qq^AF4vxWMsNXrzkQDg_0YtAYnsvZZfnMO;ri*mW_UmyLO;;Cta@bxqgX!%MPQ<$gf#n$`BvRK zhZKy3XSaXj@HV}B@Zrw0s&_k&MA#9m#qUAc=8XUg6$(Ytix55{0Hrd?&=+4ipj2v> z#XTg~9AN4+lP!aJm^n5>TquwdxI8l@lJeZ(2?TKTJXv$nGJQ?FH?GuaQQp`kA4u~) zln?YwaSIg0oRbQXB*zjnUykFS;5h2fPRb@^k)XFY5)mEk1%6d;ClO7s96g-NvCKt` z)kV+%f4VJzx2haf)y0L#Hgn5?XLtU{H?o}W$A=mu(xRr}8ENTOpe6HNT}2W{jB?(^ zoCZ=FGkdcl62w=#ON9^YPHR-4*;*B~(CT%8em;MaI@kzuu|*^Rx#&ATF}%g^Ki#=R z?`O?{RB6Fb!YY#1fh{*B(^Ol1C-gQbv{f~R(qEDn$~vql*jMDxe0Yqd{WQE0-IK$c zy@0vS#0)3l_8qbltR8@EqAKxZzHy8s?a{l9wanwFK0B^+Yiru6!opK>+KVaw?kWkx`8GK;uOotw3n9v=m9r z^#9U=NZG$CU(_15o>6>9L99YQ{bKkwFYM!;9a}Dpfo?dG4IM$PXmQoVOye5o=RG^s=eLz@l}NrAg9GRYeX64>X{G!M>7 zTu4KREi|D3aFXG8G$HZL25*WU;1H>35$^|-xPlUdE_>wU&i-468Yj{kobHrUX7l)P z8mYEk6w4X6eAzXsb=4acw?BTE)Q1-;wVzFDH%-;Sj$k0x!x07C%UA??668$J%$*D* zn6L{xeZk>0Pz>)iw5T;v@@c^k_*W%sc3(Wa3Hf@-^__jVCfR3?%^uW_4^d+=*fzY8 z?UiS!TK`+q@DvNO$}MZpVJPgdp{njbDTm4Of=7kJ=RrEv!p!YG3T;R`BZrDPjE9w1 z<(z4}+};*%Q(DzYJp5vi;7$}PLQ6;y1)kVDNQ6I{OZ|q51&3=xiiCAup#zF0Yj+E0 zbsrTy1_lVxc|f8xF1^k68w{;6KvnjCyEtC7A8h>w|NC?M8JrrvUhsa1JkD>wCnw#L znSn@`PAc9Bx01^meDQE98Eb(CStw@RJv7RkCTpUrrY$u}jMEoj)!2>6;s%@YxFG~h zIIse&dnXTs-d<@@Pk2iegHce%C`jBz>8tkdMQH&U;>zEOlQk}%TxQ#%3eS^w9@}o9 zV8PmW5a=dd!I@z)Tb&(-$gx+^usn?_miSsE97@WhYW42=<-^wr^bgbI_S^1M&wUWB zHiR%2-J4MGs59xK?J4YaoWAqWr7-ZTi;@nNa_~4c^j}uMBTp+onHHHSy!w+5>%$ZF zt0;^8iAg~@K87YE8&B4^lOa!Jk|HYTUOWh#1H9Tk!9;4_y#Ho0%=9QnQnjE@U=RW0sQA%-?7Ds z%#0m}6u9anY|f%)pFE}7xFg0mBT}BHbjp^en=FMMklLwg=g!sA!SKA^>`kKoXBV$6 z+MjJd+Fl`(=Th=zPL%hyKHvJ<;=dH%&sXC=r=HESTmAdX9f&_st)P2rBS~KQot%p! z+Z=G_+}QDsAT>XcdJ>FRN~?%0Jursk$Yo>75NGJ#;kH%%=at(54cZ3rZPnkQeF*Iw z(afg@pv4~TJFE4>rpnd7+`Xm-SC!kOYD#g;?}C*!IPeonxE{ARt{*$7+R9B1MG*yl z7&Um&SqOB~(1hE`bdN=wsyoWI#L(Ds<|gl^RCq2icCdxH-k9*?aV`8Vf`=Z900Dii zy~{49$kEIa$ z@9Mr_Lc;get;GxD_bF^V)J3G%-ya@&pUhyeikF!oI%hV{| zh&85B#xk5D$~|@khG&@S-N_x>dH22@hg5Q*q+^y~=emMbdiL(G+^#E53&T)jdg{)B zf(ix!9JR&E$FW;uS4{STg`&dapFvnKk&Uj`81a@E3)>XH+1%R9J z?H#3oue4&V=3pAu-Q82_qWenbASrhmy$O0xOv{o#J^Y@Slk=t)mtV4)#Fj=5>+R`! zJ@}$868-NeURJdCmY=5k-+Jpx{-}QXA056~Txy-#&hNIkIdoj4inBxQBkMFsxSro; zU}%9!IiEKE$Po5fBheUKkd#U6L81u?~M`ong zlNh@|yG&C%;kM-2OkP2-*YYGv+_=Scp7y3Yf-f1A$QNXXED?;e^D(@xdt~?~?Y@tB z_Fe46LQD!Z-Wk%X^+Fy`XAaM+z+kXtta* zhLMST=@qiW;3{L!0lyX&_SDaTv@GUPuyXXR`Iyi)fS)|Npsz$0x1=aZxBr&m8@yz? z8T<{MTO}0hBlmgp1i3M*-pd?X^5(A6U~P4gc-;{U1h6Z`ePuO@Ud#*Ock*ch2cClk zGvlYxO^lUjRq(i)KS+9TS;|E>mg}yDQK`%c`A&3`pK-Rpbc_J4izG(izA`k`uRoB3m zyo}~aY&xdyU}3mN`*?^j?PJ;p9z~iVJ2xw|fKzl*mb3`?J0*i(1%cCiqAa2IXfD+{$wb^-4QK}P2V38O6waw2F+ffC5(c&P;t z+#I0@a-_@|k?6{taEeqAIao#GROx}ffOV=TOkBmi*PEnc1pHwsI1y_l1f!cdU2api zvn=Wp;4e7%K>e6qkO*Kb{$CcKFUo(;ufHNc>+c`#7LG~UJZcem3z5IwNbM|HG^;Bs z5&^xi+I^C>baZM7G7-rPetWcPLUJV)1iR_2BDOhEIJ;FJxwVr55wE}rXNHxjBJmF~ z^puB_?9}u?W~1Y6gd)8dgvSB{$)piQpbBKb4p_svWshuTquWT2?kn#EO}_5u)ybB zw^${YnNNYwzAxYvN+5 zDNGOwrj|7a^xP=bZ}=`GAM*EEGUyQrgwwE;=uN7Fb@ty>_#Ssn&M_1N?^9ADvfPmV zzpZ#rf%pH$;-~n>)ADnC_$tA_PeAw);18nRmX%Vpb@;N*8zIbsdMn4)oG7)2n%PP@ zhn&pDBv8Xe7}jjkN2M5n=$vGCur@@KMPN|U`pq-e(>PG+!5UI8y)qRNBK&UjE8T*0{-N}t5ASP1HA$|yxKv(Gc*uW`wTNlgIUNxYUzfK?WAfZZRFWBQJevFx^46pjs znyo;9!eUkF1-ZcYvl<7O>TLF?P!rH_8_T$?Qea&84j{X5%8lLZ9CXGyw+TR$jUd~9p2(hJHn5*&z+*bRGgY3#E~;hZp31ylOqeg zQ;FJHoA$K)Agnf+rku*mlk%bKcAjLB!DW#s35Mt%yHe^%RG!R5RQd=tzMO)diw6L^ zNaULcAAEB7O0$g9JBq(<@o*q4f{K$i;h$}cI9)8^AQ1%X@p%&OI2({0 zVu?~DQ<@I6K791M2ahNY7^>Y7YEMj5oK}oOe4Ov>i%4OLfV_wi4fa`Ge+s_b%g4+Ey7Og+lQ~v5j?zoNAV4k zris2>0*!beb`f;eq&t6_!XDFN1=CHIt3Api4)rQwFX&1i9G&w=n@LO8Gdo9qoU}LTIp)}~NFpzsG=yjk!LjEMCE3i1cwwz4q<+0pFzmj1yhw$bAhDyZtJRWK z$bzNiI9C#bK%L~0Yt!sOt#c}tAA!PHpM@q57kb-q5t-|-{FwjUZgqMH63h}S# zPyha*rstOHIN`p2e{yiqgMbvjVEn=Bje((#C&WCq+*4(i@bxbgGjX6si!0$IOvxc+ zIfO1u2nkDcjO+JrA710_5()UaG4gX>j(i93H|Ys-xI3;b@;JwV^;&|N; zcQQOGz8Q#TwS7DQfyx%}%L<2;Wzq5SNfetC6*W3rzpjS?_oXPLlvw3Svv2EvHw~}W zR?gs}{%fo|;z{>FbJY&0wDh`RS(1wG9{}kft@Gd6!ov3p%iMFKw~z|$=tC1rn#EDz zh9|uT;MVo^bSg&S${N85>`1aKDcr-Hv>Q?1Fd_}dWPk}ik}$`OAu9#J?1Z)ZQ=lGi zI03nfPtDTrng3grs1%6f>aa~Hmnd~yZ7Y*V##j|ooWw;7nI!+)#Xl>`Uv2#`|NHOZ zr~kg;P9rx72cMpi&Me%Ifq}O6z)1B~o6WstFj~WYLUXF@X(?P|95rCRT@^n8o^ zrinX_?VK5i6vfE))mte}zMZfyWhAh~F(XHj(1=+Xv$TBi?l6A3gDZ!V#&{I(uNCp~ z7KH~eZ)T+DSDIro3atu84!aY5kMvCBkQ22fYDOQfIVt2JJB71++t?*Dv-tKDsJ?oW zO#paGXfuZ?Sj5m&vs#v6L!Nw=9$i>0t3XLvP&BrOc(_0q?6wuj(_;4@pGO}N{=pHj zJWb(rVwq})B0G779p54U*bra7po6Sqq8^f1T53V@W&* z^=kR>YD|j`-BU+_TfnO_vPh+dXsmy$f2MnDa^TthFe{{6U3M%39S>CF>AXu(Xe)U7 zn*~cULI>T!%8-m6JRfvM7|oc^QTBW+g+TpYbly;35}pg=1M33epq-SR40m%jlr0lc z2JcEtMqUleawNV&PAM{Q zZ0{s>0hjIHd;(W2h9_Stv*;v%9PfU-C&hKC!;)PSz;i2vQbQr!>|z0iIljhVKi&Qv zLrv^1*Dv2u4CULR+i{iesEQEcr%};NnyI=pD~zqXk0p(TTqh+@hk|~953wF$C|t@l z$z_C=VD^G#VMRx3y$!)QHVze_`3e{;D+Gs5oZz_d@y(^`WqsO6Z;h)3EAD9I9!vgj zFMhZvf24eM>t|cf=AU2cKlcn@q66sT_Ip^4sfG*&Zep@%K+#jrAW8)+8J5a>4q49C zsapcQyctH{?Z0vOVm*351H*@!NtOcUzQe1_3k(6XpsO5Kc2T)R4qDRdERf`9OD*-# zQ7y#vQF!Q`ft9WnP9#k@{I=GQ9S_2W=OX8Sj8%?td}%gVSn@XGdczkPCb+uJ+y?*Y zSx6vmD=bkqYRYr0Q^0Ud3gc>9T?n#evKXP_{`R3JZkGdW_VvsYa@e6V_;Y@AqD(<9 z=TM`+qc`CjaB4TJ?&cGM9Jmu@wJRH%>tNi~)kI9A zSBwu8bt6@ARfWUFE}vjB2{E*Q?LlU z;kjq9wt|RB7qY5N57XCAqNhvv|NmRj{#5%}t%c&heW?@w)4aY94K=;I>{C?PwtKFH9hV)7(QSeAJ0}nR|LnbMklpur-*?X0Jx8J?nwBN%E(@S6 zaUlUK>Ox%zNe}=M+$9Nu6qbT0ik1%c0xYo?a2HDuq-6H&?m3iYCr+Ejb?Z85;-+z8 z$LX}4Hl4mm+nIJU?WCQ_G*eG!k{3<#!s&~?Xw#WC?My%4@ALfr|Nq%tkf22BVsl3V zchC7>e!u7U+^?y~Fq1w#L<&D}c@+Lv2m{ge7eowexG$msUwun?RkKkYfKkT+O%kffyF>>VhC5F-ntA`o^5nS*mX^VNt%_C!qkr#*7*QNdlXM zP`XtOjM=4 zEZ?1dy^V|Oycj~fN*g0FRUXXeKvB`)TImjP%*T(_MHgq>W#C{OX=LJTzWh#*gSNT7 zUoFg4Z?1DBgG)kCiTXL6#Sfq!Ok$K>c88US-h+w;(iCQfuJHxuvqoj`J6?+eAnBIQ-%jksEw%^(NZh%Cwq7WBy ztLr(IBkVKM;Znio{0ybg&LZR42`kW)m`GHAG}N2|+lJ_>u7gOF+ip*=-YADe;HzLM zutirK0WvGJK;Ny^Wrk_l?sO_oJVTIk?HcW*R5I*1t(UsVI9wB=&NU)#CH6!btuz21LAQD33fes`=3*b9vOupd&oh$TA2n^UEb+Ly z5*D)i*uqmV8!|xYZ;DYYXon!wGwM#*RH;V>q0Vlw_3|A$yPZqD7iQfTMe<*96!|uc zLWRmwBNMmrX-Y%~vZ@V^c#^)7BVTEf&ZTCb!nyv6@iVwBu;eb4yPIo)r<=<<1h+@$ zY`AC3y;m3Lsa{1K;5|I4Xsy&kT%xGef{R<&DVzuO3CAsUtFnV-;mXvN`Jtt8iLxXT zZL9geyRC^oH}RqNpR}L;Nn`*Ho_N_)wRi6B-4LS4mTS@tal z#Xx51G%1xt3PA-MY6Qo^;xYu3qOR_!KzZ}3#7uw=Fk%1N?E{c#p_ZH;2^3MXaHZ4S z#%@`N7{O|KfbVl)j;hxFgXzIrU%p*fwS9Z)T73KKysj|t^!c1Q+uT|gh zXSmD4$SI*~tY?yseN%aLNnmZT2yT{V0Vjgft0{Q9=VcdA>^|MQuKLC#A~ql(R$eZ^ zJ?@Yi_doGS^OSLr+j%`BDa4nscllGH+e&F}tP3;+57T>EU4VyNHDA(dI1G4Yh9C`s zYq&&C5!=$C+E1fv6rfBOsp4{Bq@%WLP`D@|x+jZmWBAOYIs_1l3}vybbs1CtYlqY-<4{pfm>8meC3CPpEt$oXt3!vubFegf0 z^>;J#GExND+t|fa{yHJXEiDh~|J7}MtVP|=)9tfl0MW~I{|`yyM2bMr0jE zfjBIK-@rq3PyPO|4rfCe0Oc*#05--7CBjnYvB|{Di=Uw)1j$i|C-~I`&dOvoo^}8S zQixB!v?>>x;24RkOnWE{XeAM7Ascdq+ z(!Pe7m8tqGWj2r@9vLmItkbGMT>c)tNqWIS7m6VQ^>VMci9WK*vi3604b*@qdsi)0 zi@H)7D*@clT5vO6B`uP*sILwi?mO!;`;4){+M+VL(F{#`GMfpZ0|#iBQqs$|HkZ}w zTX~Fj2C$7_WYV%=Vy3-35;1?tJCj5`PX^-HT+b=huk1rc>|ApF-(>4|S`(el2iia2 zm!F;=V)8MT{d{lzq-o#E8#tY^%lf}7;YEYdHzUr6zcYZVZ>++VdobXx?7gUdWLn=% z-7$S>0Lx05I>!=k-2?T4z$wT=eI}Pa4hgogex1IARC(3RQm%b*oe)u|ptrwF?mj&B zJ-sy+gc?_-hDfVRstL5$2bz0>u5Uh96-q`PBWrUhQxcr(kaEVMNf3pT=b%GSM7X9X zC_G!*OHm)M;X9aT>gD{)9TUUp-fF-f7QIROJ%$oEwz9Yi{#qTlkv2#05l0-@nH{d? zMLby=u{tPYUnT5h#1*jRik(4mobgV+PVT}~z-n^@XFZI1tVmuJE|P@ek!1xL=)~h! zFMyY*s7fiNp*PynnjXCHGNm8T{Ik84uSqeixZ4;j_1%mz5v=Rh%jh9Uz<9EnPzOhb ziRv-2>O#T|UBs-#3kyb4HTp$eN1h_S{cvyDs07bvdoo#6 z8?e{=W13(QAtrIHW$~fbMJI?Yiu1rY+=>MH*l#qs!VFzTy4+O@S|g^Of$K=EqqA^d zAbEujz8@QX&nGt9yNpY0eJMQvHE$5r8aVtC_^k9I$w~eN^K*O$pwgSrfb5-DF<{_% zT*${}W&&yaf}^8ce0ggFr;b>d>oSD_Gr z63;q~c0~JCsZo7-!_+v#iCI!q-LQrQ$EI1VbR#$6#pxkXUbj;MX(TU7EgWD4~*=)T_BMz$oO2Ba~!i zq%0>{H1E=&s|Jg)Ugx5Gl5rTH7|f5z8%AE7?kzkSj70J3sPciKRFP^kc)e4rS40Yg zEwk8It^D%wW?hi#%JQnc4ofs1TwFoTI3NpYQyvQOhy|NTib|9>QO~xrt|DP9Ae3}o zqU&aGM3F5RJ!+yKP03NMbl_Zmqbhl4e&h+~FQ4ws^Pt)~k=7+-vf>q!7n8Z6m|e+K zG{fCqa@a<59P-62I{1OKA^4QSdR)A#d5OSzt zuBw5(xi%|*g@nNVzKlROFs~BqH1kMs8e;e=d9deLvEVEvg9qx@(`>;gRkWiSL z8dvbnu?RqFUg`h;4G%o*S@oEE`t4Ezzj$?ha~W1eK`EGGHgxN;7+mR)M;%a6|W-l>4+L9h8nk;6=TSB6wEg60?6!BFTB*B4-XBjjW(XD91 z`oI3zT9{759v`d<;+bzM~@ux6Em$H(7Y;RfNYcBF?S^a&wtsUYp#y zCZ=zTixV?nS&{3srn!AXwRIiA`zz}~9CI@E^WCd<5Qx-s*JtIY4~ve-(k5$O9$ho^ z&-K17H=KpDQ@3PzT)==w&!T~$n4p$sj-#iM=MzN>`qYWUv|hn!;nE2B3vOTDOAwMR zR*el(aMMMNPmq#_a8S1MRDeVFL^|7~QEQo*7*R%- zNJ6jnr?xX2?<~&D?l!0(VD1(qXhMC-hohD!sUB> zOKKI&1-e)%3ag?&r7}vnAkkAVaqBkSazvN8s_0sHx*N#BH$?lQ4qN6xrtrujokC;e zR@n$;1jUryw-{qN)FsJ%PNd#QA;lJADV(P5Lvbp~xSSko`QK^%qgLmG{PU;fXZ!SM zQ^-1;=>5v0us)w3kgcuGC)DK~CN7UHIy+Lkp^i(aj{DAZd^%d$=Vh>z7mH%wN_s0*ntp@Ic`Z(o zRCS?N-XI8-FSIjLNT}ha_Hw^tpF!$E?x?o0OaxFtnGOC@cmk~*GkoPK!|+&K47I8~ zmN5&%goyQQ@>OvD@^FN$_GS>uxN`4!vT+u##g&j4F&18^rA}(AQbA;d1Fa&N3c&lZKTqLHQOw9 zKU8&*g;_?@x{Y&UNyM?vjMlX7o!fd(?pe15l@||@9}sI3uE2I!TBz0;Gg2SyDy4wk zA;#;p3_&D!F?ADH4I;pbu-e|>ZzpZ!Sf)el=Nt$4Xw5TE%lS28Fj)ZC1 z*Pmf*;FMoXF9C36&K3s%959YJCnKP(piJ@En{l^1x}t@UHTw0FTt7=@RDW^B_dcM$ zjD7$5(E1vASbE?inn~n_cQywAoZfZBGKbg0fR~`$bj_UG+Q7Fe_nvU?=(k4C_+9t* zp3u9#2~Q$_Iv@Pd`sJ`V9)g93PRIiK@RkGKCbctO9Z3L-@{33Q-*2`$|D^qU?IW$< ze4|MJ7N0wK_egD-+jOV<=v<{JULZp~)I_q^GKgfnX!OFCz`cyK&8CnTt3o5G7S&c9 zpP!6r$}oPwX(v_{nF<^p3(;A@f}*CckG|-0xYGOOa~g;j!dsq43OrQZFX^Nxor7{> zHlnMr;gX^h@pAT9pxV||#{n{)la9h{GY9=fFozV)?azWVzRHL|AIyA+)QH<#k0z#HY z$*spLt$^^LuuZ~aS1xIW_mpspaH3HF*=nqW@1XF*R1G0}YfwlTft6Sehh|k~D3yC) zZ<+rb3T(}#7e7BYx{X1{F0_%r zw<=*RF1z>fL=J6D>_9x6_TdFz<{d*Yr5aYC(+amQILV3IOb3nU;a(6QR6awtM&*&q zawApL-bfRbC(}d|TaEtz-PXHXo&TpZ(f*Cr|HZHT48Ap57Tk93?ac}0xNBUs6{XE? z_WSd}>H72Q7mt%gffkg^O}7Le4c+b0by${Id-?oOm4p%(aG$6a^u$ss7h&(*kEs%( z^ZfeCI>#JuT)&wA-CVpHOg79r9mh#5VxlmO&=``;HD^$yB1hAMUmRW5{d{JlwO6LNokf3M7Bue% zi7&>(ApJz;=V<81p&NXuuW?$W?Ome@w@vJnT5jgiXLO$;Aimi~NOT~DjAFA4c{#a8 znt}#Uc=e1?b}v4Yd+nOs?u-_-?cH|oG3hZ?4HE5*vfmC^Zf9K#j<`QlLfNbJA|Ag_ z2QMKduP*h7RZ?^lm;w*j20wf?xB?ERM3!!>!XDdI(w~@PP~~M2`EWQUo@uhjYSM=4 ztwzQ?>rTXRaT*0viyQX;bs_tS-Oj)4oM?~u_0Pc1;O(OY)Bl~lb5agwLjbuIyu0aY zkSUzmTtkxi0+d+7gwWB!!AN8VJQtP@ITE^58d}xwR^)4=S4WWeTooEm5AGe!dp(zW zkBZ+Kjd;x_9r^S+Ik@#G#V0%q(?h^j@6S2&zr>Qm)d$&X2IqBo8jMU-$;0SI6i$Ub z)v5sq?D8cIrn4SZAu6~-y;06jo(i+T(^Nk_y6A=XduOXxH5R_g+>`N@H{QNy^`U)v z>#e8&Qs^l35)>0$EJjx|uC(RnCyv^tMsotA1zikqa`h z1mlP>0J!T8jIip|DESU|&mw6?hIv<2B5_g>?B4MI6svom+rA~wd|X89PyCS zgZ|o$`$GSJuGLxZ+(z}^b3Y#9A9P0)ts{Tm(tBtM%iFw&&NVuVvSUFwgo0&lpEIJb z^$YzC&>sJX)2`6`L_`tayu@g!=x68lDbE6NECmU;2Hu!^D8REA2ZHuqmR&_!CAQ4Y?IRDm zAHApdAjR|d9XfwmJmxy0bb}Zij=yWFw*kXL>xwZQ{=K1yly+n|QXE;7@29D>08G*_tKnIHqj!f&d7O}jBr(w(8 z)r1BB(bkh3iJ88w^dOK-nu5ZX29J31*wsKL z*3L0Ln2(6PvP-czP|WL=K2&Py$~&DcnA`(gTv^yW8uWjqHSwQx{(k2Ztt)RJ{qKBf z^psGHi0~Pq7`k9q#FUtoQ@GhKifz>?T^b_>I<+47!Ht3aL(hPkx#@B`H!YNA&|>=b z8sqEa+d@b?c@w6tc6!2V9=qWRSLxFrhiI3B)sSwe9N`HQ;=)Gyr=|ham7buoTJw&% z^JASdQlq&u4k_?2(qDumaPaW(@-CB$Bm|2IuL1~YK`h5**aSLbK%@ooZc;t4by;pe zO$I%?Ahe4M=nMOe%&O?0#yXQDd#`u)@bE1Jv;h_wmP_@cUos0xYTFTIyp>yu)I~pj)Q`Ois@VhQRDhI!b zy}U}|&>-ePvF{WW_CUrIVml+zbX}`bq34_V0%tf(3DC7;d~nT3I5g zDz!vcLFR#XI!AkoV*np3iG(BEfYXD!M=}2W;og_)tIw^jEnj66BlPK9bG?ApbM#aT ziz4$XFKywfTE@XpU7?gI|(EPw+ zwodW6WF88)q#{7!iT1&jrzq?!wrj0`lPb|P92Pq6$Fi>0B?_HCob43j&0$4|1~VpbQd zr8410<;1O&dd2zWV}M7p?HWd<8%*QF&|&27_O|!u_dO~>txOR{??J8t_MYd8X^SN@ zu0)BoH%}f+1vq1>l2|YunC$`?7J6pP>(sNF#hK#FxlqI)isi73cx#&a1%<_R-Yks6csEUW_Rkcsyog*4g!skTMK z|11uDNMMip8Jd|H46nnvwJ8hvF~TbHKvk^daHDy?EfOX0xcmT-34FjSvc|~JiSGHy zJylM_;>C($Zl<-=eZ^_U7ZRG0QsM?R-(y9}W%Kv(07GDJ_54aYp=CzT{m@wkuK>+Mir?{e< zBdejNh9*A?`~RP}CLZtn)Ak><{`svC|IgetD7P~{*LLqS7nD;gT(;3%PS(MzsWH=6 zlC_zaQgqJZ%L;m|5h0k9nH(R6`St4V0SLQJg{cw!AQN}{v!e&iR~CDphUYdLPeLQ% zRjTdv4W+}v4!KaMX09UQCn;5t;@RR37J@*1k;!Xq!=w^LkX}%VD8a)?{e=g{NYaQc z?tjsJP=^95NJr&JC{<3;K)> zpf&Mz=$&Qhtp|c1o>~Ii9HxA{{2JRhdbk zmUiU@`!YPkukKYSO2jJ40(!V>W`w;s3QJ}2^vdXz>EemrvGdjqw30?CuM_by!B@pW z7AdQqIHy!HM$Ln@bo0Ko8MTM;(491N&-RYap%SG%>qS9Qmn><`5JJ)L7t;To>4ld} zSvwp7M5SXbraYI0e;&RZc^7?{0h+%OU{orUUrDGX~pYSwbzo#x9W4(VYWg|NHopvNG5L z`Q3G9*Xm)(|88rcHSwX&Zu_sbJN){({|w$gqLDW&@l(Bzb9%PNoP^ZR%qC-NV4hA% zNFa#rOUL47MYh!7M<5G>+0Ej^foo_)D;65cDV!AHka(uy8)4B)Y6ch(o*sN~^cAn} z-rkW%)!IYWs#hc{AL^IZ7Y9CLZ!OLn*szyb6b4vPWt@hRD8~%2!#fyPibnNX~M3u?? za3d?EYGw?b#nc~$U!y5uh{**Cn4L95Bv&SNosJ`jm0TSA<;o1I!JVTo`2_Cn-Hm>n ze)@yNvOObsz|stX;&jHv9cfuvYyrz{Nne{Wj{C;}SVPMrkp~5@tj3&(4)^By4!%7) zZqOg^eQd7Q>BP|apXY>`e^lW=YwU-X>WYEiM#JmlbbfI_X(a2&?pUVEwV<>lf zf|hgEE@*i_dEey1^CG=_2F4e$p>ZWuO-gd-o-^x}60jhC+}Rf&tG{syy&CB&eN{5_@(;hG~)!s5*U zvx=a?Jikp3I6OF~mdb4P1HE5D+)D8eMGBQgdSqFC)Ho^)sunHmTV_0sB!Wwa(`7ci z1`a-OAlMjQZZH*jWu9E@AI*tu&@yZfW_rML<|ODjlSt#jRa{LrsjLY2X~Tv{J%4&} z^l9yA@L=ylCqu-i_k(qCfIuQWt;RLIeIFVoyF-O|{PsqHG2Q%2k+ zEP&LutUm}_0u93Z8?N{e-hp&}tPw?M56r_qS@LkfK$F9AUs0RmBh4XiZ*TTK@LI9x zkegLo&lh%_17nLr+S*K}W5oiE`^95@Ji9%7LD-~tMEOEpky06Yy|jcDe22FTsEzlkkD5ZP{7RHeQlXfdn==LmHX{MH9T zHTMW@DJF@+R;2MTy;gjqA%f7w8x3}Yoy-x4Xwd8;-Cgu>%KDTaQN?&TCdq;Il?CYk zm{0S;-g}F==TmBq`)MALj&TaU3H`r+ zzxBB$G2iU>^^pc>w+B!4-ZLkmW4_4qvgIr?i2MabI+eYU=u`}nnungszHvm}lY0Ve z$iYgnEr3Rb?1x96&?~o(_ueh^4y#EaKw(p;GupcHtFmZ>&x7-Aa#6CHqFh#Lb`=Sa z);E@Vt|J$h6>peIH=ypUF6NMI$}1J1%zQe5(xR!yQd;hmDgP=nA7cQMwNuE1mo;l; z9XX@GL|@B?+=xd+iX&SsDgSaG8K}wO>3ye4V#mYZJ7P)-OFGtjS7_FTgH>RI4gZFo zRP_|)Mf2;I==IIUQ}~rs1#0)wgs!jQUWiW}iKKKE;FdW_e5HgWSGzV>7=8D4IwQ>w zZjai%cVgEx-I+cBVtnRx zT=e+X#@$d~N+b|?-1(R)%dA5?a>NNpSq&*?jEGGztQ+3TU+bcr=`@j8;wB>vCDYJw zU=VKhY@y_58Mle8IMiP-4?f(x!&Dz-HFmPh0xy)Iij7u5cRi-DNYMqrJ$YRj zkN2w>;DwQ9p{oDq&-{*rbU?g^bH z8odSI5?y(MrrY7su*H$4!peTS{fYF&!BiQgC*=U<&D7p?gIlh4eH5x>Bl}2ja)!~2i9>(R zz{~fx{Jw2;STEYWqxW`WkKbncIPi5GbG`{xcYpatp1dEDoSoO3Y;GaS&!tXAa3?zp zDv@!~hf=i#=S2fwqhg#6^3D*HitFG$n=G6N^@ZM*HxcVM5$Vs4G?=tK_;&AYz@pKt zr>PMm+~*i04yh&}l$I1sRkpg|oW)a#tf-P6Y^QQ!5u>iafv-mIm_38`=)->LOTC|K zl=wCVLfX`D5P^)B%>UszepMPI=D7xfwGQ-HT4j)^P1pt?DJ#Uk!og!r#5C#Lfdv&$ zWN_R_11sCx%e~uX;A&{Gb^9_n8$^hPTZR8nr7|Sy2m_T>q#VXLTBVob4e~vGl9&4Y zy%s@RjH5Iov*nR;nb+Xp5(aR38X%Pl(MSXVP`p{-GQYKHz^PcF%~I0L|ND(7D$EHl~Mvs8>&k}KldJ@Fu*oDuskO+l+(rKR0AbbyUsg%mRn0(a2 z);nPtEWO$f$WmLk$zL0v*!Z(3tV1T^yj2{#__d~y1e#CT9=z0>mR4z64V4;c?Vap!-AWIRo=<-#Su&Q9>MM2MMD_reOpqLThU z(fSvy&WPIofAM+|{!im`2Jantz~c6??sJ&sxU05utexPDG^)}$1aTEKMR7);55~6d z;0Tl&;#&?)-Q<>>GpQT!Z4w&uy*$V>1!(8FujTA_N1mp*v(?=qmCpU$Y6iJCSmzE>ONnu*5WX%WrLxlv;Ef|p9LZJ@LJD&-(;lgKO5&jrP`ajyVhcKwqdt3Rfp{y5 zROzb&eDnZ|B)^}z)h@zD0rKBGEuyCv`ZU8sH*$@OAZr^Ul3p*AmnrR`U;7xgpBs5} z;&8cp?PPh)7$u&g;U_Jwr1RKEewNOShY#JqtnFXZQKAF}g8?dPD;`zifX3?v2&*B5 zMMDb1AK-g~aI3oQLzh;vlR8O^s8hg77BxvooIRtZw0qV&IqFnf< zBwpcZN{yOXR@B!0KPFrMQ)}W&o!@Ey1%CNc@I#Zk7(BV!-K>xZgouPxlx{T4RQqcY zYpheIWvv~f3IHz&>naVNmK0cXGS^BK8i}-?jTu52M^R%^rW75^>PD4Ed@mudWSJ!r z5^glwy-jbwW#mDWyKn1m?8kVO1gP)Cxex$HEl2f~tPQ;k zsi@v7p*>a>)`b+6U4S?M9$CiFS0f*=FrHBfFfWt9Crw1Pzc`W4i=ey5N@hBO3Y@Qa} zQhX_UhVm@R1ZK7H4=X-=AS$ZfT#C3nJJrN7@h{O>TxX^cvrewU$%;=>w4JJaPd22~Fh{ zZf&duVkb=uCXI8!H(pe6PRs-<8(ZjQzogCmc!-FaJgbvFDjvY?IZ!B8V*Lq3-} zKmnCiJU)>f(I&ooK}w3)fQ1{BrjcwoOHEP91W8YZB8odW`9A8nWPVH|u?8r$qoE>K zM%hg?>w_^Usg>XZy*KC-)9N*}al3O(X%DN&vXhxzXG<>2z+jP^=Y9 zu$!)MKs?jj;W!5>Cf!(&D8T24ys}vJWP8fq3#b~HLK9%`?6csbVyJjyHaN>{>8MH* zi3{H@W*mbrkr}B`)%Nz+c|SKQhpEa+5rzl@_(*A9@xuCBJ-KKmJ{H=CY~q+fzi}uB zl}14)`#z<~F+n2rZFS9e^IC4tq&To5K;b0W$Y!42{pFEI5r6-I?lb44`$-(8o^<%o zBLa#vTcK2?(DFw;Fuy?=%gcA~ieZrF0hywU%UtQQ_%={xT-2W`EP|5s1+tXzeeo3=86RClWdbvf z)I6

- 1 + 0 Qt::ElideNone @@ -38,102 +38,128 @@ General - + - - - - - Theme - - - - - - - - Light - - - + + + + + + + + Theme + + + + + + + + Light + + + + + Sepia + + + + + Dark + + + + + + + + + + + + Language + + + + + + + + + + + + + + Audio output device + + + + + + + + + + + + + + Features + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Daily verse + + + + + + + Missing recitation warning + + + + + + + + + + + + + - Sepia + Import - - + + + + - Dark + Export - - - - - - - - - - - Language - - - - - - - - - - - - - - Audio output device - - - - - - - - - - - - - - Features - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Daily verse - - - - - - - Missing recitation warning - - - - +
+
+
+
diff --git a/src/interfaces/userdataexporter.h b/src/interfaces/userdataexporter.h index f9e7a710..509aafa6 100644 --- a/src/interfaces/userdataexporter.h +++ b/src/interfaces/userdataexporter.h @@ -19,7 +19,7 @@ class UserDataExporter : public QObject virtual bool save() = 0; signals: - void error(Error err); + void error(Error err, QString msg); }; #endif // USERDATAEXPORTER_H diff --git a/src/interfaces/userdataimporter.h b/src/interfaces/userdataimporter.h index 4a177d6c..8d95c623 100644 --- a/src/interfaces/userdataimporter.h +++ b/src/interfaces/userdataimporter.h @@ -19,9 +19,10 @@ class UserDataImporter : public QObject virtual void importKhatmah() = 0; virtual void importThoughts() = 0; virtual void setFile(QString path) = 0; + virtual bool fileContains(QString key) = 0; virtual bool read() = 0; signals: - void error(Error err); + void error(Error err, QString msg); }; #endif // USERDATAIMPORTER_H diff --git a/src/utils/jsondataexporter.cpp b/src/utils/jsondataexporter.cpp index 58844fa0..6e415b96 100644 --- a/src/utils/jsondataexporter.cpp +++ b/src/utils/jsondataexporter.cpp @@ -81,7 +81,8 @@ JsonDataExporter::save() QFile jsonFile(m_file.absoluteFilePath()); if (!jsonFile.open(QIODevice::WriteOnly)) { qWarning() << "Failed to open JSON file for writing"; - emit UserDataExporter::error(IOError); + emit UserDataExporter::error( + IOError, "Failed to open json file: " + m_file.absoluteFilePath()); return false; } diff --git a/src/utils/jsondataimporter.cpp b/src/utils/jsondataimporter.cpp index 2909d8f6..76dbbd43 100644 --- a/src/utils/jsondataimporter.cpp +++ b/src/utils/jsondataimporter.cpp @@ -45,11 +45,11 @@ bool JsonDataImporter::validArray(QString key) { if (!m_fileObj.contains(key)) { - emit UserDataImporter::error(MissingKeyError); + emit UserDataImporter::error(MissingKeyError, "Missing key: " + key); return false; } if (!m_fileObj.value(key).isArray()) { - emit UserDataImporter::error(InvalidValueError); + emit UserDataImporter::error(InvalidValueError, "Invalid array: " + key); return false; } return true; @@ -60,7 +60,7 @@ JsonDataImporter::validVerse(const QJsonObject& obj) { if (!obj.contains("page") || !obj.contains("surah") || !obj.contains("number")) { - emit UserDataImporter::error(MissingKeyError); + emit UserDataImporter::error(MissingKeyError, "Missing verse key"); return false; } @@ -68,7 +68,7 @@ JsonDataImporter::validVerse(const QJsonObject& obj) number = obj.value("number").toInt(); if (page < 1 || page > 604 || surah < 1 || surah > 114 || number < 1 || number > 286) { - emit UserDataImporter::error(InvalidValueError); + emit UserDataImporter::error(InvalidValueError, "Invalid verse values"); return false; } @@ -79,13 +79,13 @@ bool JsonDataImporter::validKhatmah(const QJsonObject& obj) { if (!obj.contains("name") || !obj.contains("verse")) { - emit UserDataImporter::error(MissingKeyError); + emit UserDataImporter::error(MissingKeyError, "Missing khatmah keys"); return false; } if (!obj.value("name").isString() || !validVerse(obj.value("verse").toObject())) { - emit UserDataImporter::error(InvalidValueError); + emit UserDataImporter::error(InvalidValueError, "Invalid khatmah values"); return false; } @@ -96,13 +96,13 @@ bool JsonDataImporter::validThought(const QJsonObject& obj) { if (!obj.contains("text") || !obj.contains("verse")) { - emit UserDataImporter::error(MissingKeyError); + emit UserDataImporter::error(MissingKeyError, "Missing thought keys"); return false; } if (!obj.value("text").isString() || !validVerse(obj.value("verse").toObject())) { - emit UserDataImporter::error(InvalidValueError); + emit UserDataImporter::error(InvalidValueError, "Invalid thought values"); return false; } @@ -151,16 +151,17 @@ JsonDataImporter::thoughtFromJson(const QJsonObject& obj) void JsonDataImporter::setFile(QString path) { - m_filepath = path; + m_file.setFile(path); } bool JsonDataImporter::read() { - QFile jsonFile(m_filepath); + QFile jsonFile(m_file.absoluteFilePath()); if (!jsonFile.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open json file during import"; - emit UserDataImporter::error(IOError); + emit UserDataImporter::error( + IOError, "Failed to open json file: " + m_file.absoluteFilePath()); return false; } @@ -168,7 +169,8 @@ JsonDataImporter::read() QJsonDocument document = QJsonDocument::fromJson(jsonFile.readAll(), err); if (document.isNull()) { qWarning() << "Failed to parse json file"; - emit UserDataImporter::error(ParseError); + emit UserDataImporter::error( + ParseError, "Failed to parse json file: " + m_file.absoluteFilePath()); if (err) qWarning() << "Error string:" << err->errorString(); return false; @@ -177,3 +179,9 @@ JsonDataImporter::read() m_fileObj = document.object(); return true; } + +bool +JsonDataImporter::fileContains(QString key) +{ + return m_fileObj.contains(key); +} diff --git a/src/utils/jsondataimporter.h b/src/utils/jsondataimporter.h index 737f7331..434a1ed7 100644 --- a/src/utils/jsondataimporter.h +++ b/src/utils/jsondataimporter.h @@ -11,11 +11,12 @@ class JsonDataImporter : public UserDataImporter { public: JsonDataImporter(); - void importBookmarks(); - void importKhatmah(); - void importThoughts(); - void setFile(QString path); - bool read(); + void importBookmarks() override; + void importKhatmah() override; + void importThoughts() override; + void setFile(QString path) override; + bool fileContains(QString key) override; + bool read() override; private: QSharedPointer m_bookmarksDb = BookmarksDb::current(); @@ -26,7 +27,7 @@ class JsonDataImporter : public UserDataImporter Verse verseFromJson(const QJsonObject& obj); QPair khatmahFromJson(const QJsonObject& obj); QPair thoughtFromJson(const QJsonObject& obj); - QString m_filepath; + QFileInfo m_file; QJsonObject m_fileObj; }; From f3c272c88a60277680c7b8d1db98333e71a959b6 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Wed, 28 Feb 2024 23:12:29 +0200 Subject: [PATCH 32/51] refactor: add notificationsender interface and create implementations for each type --- CMakeLists.txt | 9 ++ src/database/bookmarksdb.cpp | 11 ++- src/database/bookmarksdb.h | 8 +- src/dialogs/copydialog.cpp | 11 ++- src/dialogs/copydialog.h | 13 ++- src/downloader/jobmanager.cpp | 9 ++ src/downloader/jobmanager.h | 3 + src/interfaces/notificationsender.h | 27 ++++++ src/notifiers/bookmarksnotifier.cpp | 20 ++++ src/notifiers/bookmarksnotifier.h | 14 +++ src/notifiers/copynotifier.cpp | 13 +++ src/notifiers/copynotifier.h | 13 +++ src/notifiers/jobnotifier.cpp | 20 ++++ src/notifiers/jobnotifier.h | 16 ++++ src/notifiers/updatenotifier.cpp | 17 ++++ src/notifiers/updatenotifier.h | 14 +++ src/utils/shortcuthandler.cpp | 141 ++++++++-------------------- src/utils/shortcuthandler.h | 2 +- src/utils/versionchecker.cpp | 17 +++- src/utils/versionchecker.h | 13 +-- src/widgets/notificationpopup.cpp | 114 ++++++++-------------- src/widgets/notificationpopup.h | 67 ++++--------- 22 files changed, 319 insertions(+), 253 deletions(-) create mode 100644 src/interfaces/notificationsender.h create mode 100644 src/notifiers/bookmarksnotifier.cpp create mode 100644 src/notifiers/bookmarksnotifier.h create mode 100644 src/notifiers/copynotifier.cpp create mode 100644 src/notifiers/copynotifier.h create mode 100644 src/notifiers/jobnotifier.cpp create mode 100644 src/notifiers/jobnotifier.h create mode 100644 src/notifiers/updatenotifier.cpp create mode 100644 src/notifiers/updatenotifier.h diff --git a/CMakeLists.txt b/CMakeLists.txt index fd37e707..f26d7bb1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,15 @@ set(PROJECT_SOURCES src/interfaces/downloadjob.h src/interfaces/downloadtask.h src/interfaces/dbconnection.h + src/interfaces/notificationsender.h + src/notifiers/bookmarksnotifier.h + src/notifiers/bookmarksnotifier.cpp + src/notifiers/updatenotifier.h + src/notifiers/updatenotifier.cpp + src/notifiers/copynotifier.h + src/notifiers/copynotifier.cpp + src/notifiers/jobnotifier.h + src/notifiers/jobnotifier.cpp src/utils/settings.h src/utils/settings.cpp src/utils/shortcuthandler.h diff --git a/src/database/bookmarksdb.cpp b/src/database/bookmarksdb.cpp index a5905d44..b97ef4be 100644 --- a/src/database/bookmarksdb.cpp +++ b/src/database/bookmarksdb.cpp @@ -12,6 +12,7 @@ BookmarksDb::current() BookmarksDb::BookmarksDb() : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "BookmarksCon")) + , m_notifier(this) { BookmarksDb::open(); QSqlQuery dbQuery(*this); @@ -239,7 +240,7 @@ BookmarksDb::addBookmark(const Verse& verse, bool silent) commit(); if (!silent) - emit bookmarkAdded(); + m_notifier.notifyAdded(); return true; } @@ -259,7 +260,7 @@ BookmarksDb::removeBookmark(const Verse& verse, bool silent) } if (!silent) - emit bookmarkRemoved(); + m_notifier.notifyRemoved(); return true; } @@ -327,3 +328,9 @@ BookmarksDb::activeKhatmah() const { return m_activeKhatmah; } + +const BookmarksNotifier* +BookmarksDb::notifier() const +{ + return &m_notifier; +} diff --git a/src/database/bookmarksdb.h b/src/database/bookmarksdb.h index 6e0444cf..3e3d7618 100644 --- a/src/database/bookmarksdb.h +++ b/src/database/bookmarksdb.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -15,7 +16,6 @@ class BookmarksDb : public DbConnection , QSqlDatabase { - Q_OBJECT public: static QSharedPointer current(); BookmarksDb(); @@ -112,15 +112,13 @@ class BookmarksDb */ void setActiveKhatmah(const int id); int activeKhatmah() const; - -signals: - void bookmarkAdded(); - void bookmarkRemoved(); + const BookmarksNotifier* notifier() const; private: const QSharedPointer m_quranDb = QuranDb::current(); const QSharedPointer m_settings = Settings::settings; const QSharedPointer m_configDir = DirManager::configDir; + BookmarksNotifier m_notifier; /** * @brief integer id of the current active khatmah */ diff --git a/src/dialogs/copydialog.cpp b/src/dialogs/copydialog.cpp index c9960c37..e2c33e07 100644 --- a/src/dialogs/copydialog.cpp +++ b/src/dialogs/copydialog.cpp @@ -5,6 +5,7 @@ CopyDialog::CopyDialog(QWidget* parent) : QDialog(parent) , ui(new Ui::CopyDialog) , m_verseValidator(new QIntValidator(this)) + , m_notifier(this) { ui->setupUi(this); ui->cmbCopyFrom->setValidator(m_verseValidator); @@ -25,7 +26,7 @@ CopyDialog::copyVerseText(const Verse v) text += ' '; text += "[" + m_quranDb->surahNames().at(v.surah() - 1) + ":" + vNum + "]"; clip->setText(text); - emit verseCopied(); + m_notifier.copied(); } void @@ -51,7 +52,7 @@ CopyDialog::copyRange() final += "} [" + m_quranDb->surahNames().at(m_currVerse->surah() - 1) + "]"; clip->setText(final); - emit rangeCopied(); + m_notifier.copied(); } void @@ -74,6 +75,12 @@ CopyDialog::show() QDialog::show(); } +const CopyNotifier* +CopyDialog::notifier() const +{ + return &m_notifier; +} + void CopyDialog::closeEvent(QCloseEvent* event) { diff --git a/src/dialogs/copydialog.h b/src/dialogs/copydialog.h index be073789..ed3d0411 100644 --- a/src/dialogs/copydialog.h +++ b/src/dialogs/copydialog.h @@ -6,6 +6,7 @@ #include #include #include +#include #include namespace Ui { @@ -15,12 +16,12 @@ class CopyDialog; class CopyDialog : public QDialog { Q_OBJECT - public: explicit CopyDialog(QWidget* parent); ~CopyDialog(); void show(); + const CopyNotifier* notifier() const; public slots: /** @@ -29,20 +30,18 @@ public slots: */ void copyVerseText(const Verse v); -signals: - void verseCopied(); - void rangeCopied(); - protected: void closeEvent(QCloseEvent* event); +private slots: + void copyRange(); + private: Ui::CopyDialog* ui; QSharedPointer m_currVerse = Verse::current(); QSharedPointer m_quranDb = QuranDb::current(); - void copyRange(); - QPointer m_verseValidator; + CopyNotifier m_notifier; }; #endif // COPYDIALOG_H diff --git a/src/downloader/jobmanager.cpp b/src/downloader/jobmanager.cpp index 4810c707..865dcd89 100644 --- a/src/downloader/jobmanager.cpp +++ b/src/downloader/jobmanager.cpp @@ -3,6 +3,7 @@ JobManager::JobManager(QObject* parent) : QObject(parent) + , m_notifier(this) { } @@ -110,12 +111,14 @@ JobManager::handleProgressed() void JobManager::handleFailed() { + m_notifier.notifyFailed(m_active.data()); emit jobFailed(m_active); } void JobManager::handleCompleted() { + m_notifier.notifyCompleted(m_active.data()); emit jobCompleted(m_active); } @@ -136,3 +139,9 @@ JobManager::active() const { return m_active; } + +const JobNotifier* +JobManager::notifier() const +{ + return &m_notifier; +} diff --git a/src/downloader/jobmanager.h b/src/downloader/jobmanager.h index ec550c7d..1ee39a38 100644 --- a/src/downloader/jobmanager.h +++ b/src/downloader/jobmanager.h @@ -5,6 +5,7 @@ #include #include #include +#include class JobManager : public QObject { @@ -18,6 +19,7 @@ class JobManager : public QObject bool isOn() const; QSharedPointer active() const; + const JobNotifier* notifier() const; public slots: void processJobs(); @@ -41,6 +43,7 @@ private slots: void disconnectActive(); QQueue> m_queue; QSharedPointer m_active; + JobNotifier m_notifier; bool m_isOn = false; }; diff --git a/src/interfaces/notificationsender.h b/src/interfaces/notificationsender.h new file mode 100644 index 00000000..8f631ee8 --- /dev/null +++ b/src/interfaces/notificationsender.h @@ -0,0 +1,27 @@ +#ifndef NOTIFICATIONSENDER_H +#define NOTIFICATIONSENDER_H + +#include + +class NotificationSender : public QObject +{ + Q_OBJECT +public: + /** + * @brief The Type enum represents all possible action to notify the user of + */ + enum Type + { + info, ///< general information + success, ///< successful operation + fail, ///< failed operation + bookmarkAdd, ///< bookmark addition + bookmarkRemove, ///< bookmark removal + copiedText, ///< verse text copied + updateInfo ///< version information + }; +signals: + void notify(Type type, QString msg); +}; + +#endif // NOTIFICATIONSENDER_H diff --git a/src/notifiers/bookmarksnotifier.cpp b/src/notifiers/bookmarksnotifier.cpp new file mode 100644 index 00000000..8bb04297 --- /dev/null +++ b/src/notifiers/bookmarksnotifier.cpp @@ -0,0 +1,20 @@ +#include "bookmarksnotifier.h" + +BookmarksNotifier::BookmarksNotifier(QObject* parent) +{ + setParent(parent); +} + +void +BookmarksNotifier::notifyAdded() +{ + QString msg = tr("Verse added to bookmarks"); + emit notify(bookmarkAdd, msg); +} + +void +BookmarksNotifier::notifyRemoved() +{ + QString msg = tr("Verse removed from bookmarks"); + emit notify(bookmarkRemove, msg); +} diff --git a/src/notifiers/bookmarksnotifier.h b/src/notifiers/bookmarksnotifier.h new file mode 100644 index 00000000..d6b1ae95 --- /dev/null +++ b/src/notifiers/bookmarksnotifier.h @@ -0,0 +1,14 @@ +#ifndef BOOKMARKSNOTIFIER_H +#define BOOKMARKSNOTIFIER_H + +#include + +class BookmarksNotifier : public NotificationSender +{ +public: + BookmarksNotifier(QObject* parent); + void notifyAdded(); + void notifyRemoved(); +}; + +#endif // BOOKMARKSNOTIFIER_H diff --git a/src/notifiers/copynotifier.cpp b/src/notifiers/copynotifier.cpp new file mode 100644 index 00000000..333afab3 --- /dev/null +++ b/src/notifiers/copynotifier.cpp @@ -0,0 +1,13 @@ +#include "copynotifier.h" + +CopyNotifier::CopyNotifier(QObject* parent) +{ + setParent(parent); +} + +void +CopyNotifier::copied() +{ + QString msg = tr("Verse text copied to clipboard"); + emit notify(copiedText, msg); +} diff --git a/src/notifiers/copynotifier.h b/src/notifiers/copynotifier.h new file mode 100644 index 00000000..fca6191b --- /dev/null +++ b/src/notifiers/copynotifier.h @@ -0,0 +1,13 @@ +#ifndef COPYNOTIFIER_H +#define COPYNOTIFIER_H + +#include + +class CopyNotifier : public NotificationSender +{ +public: + CopyNotifier(QObject* parent); + void copied(); +}; + +#endif // COPYNOTIFIER_H diff --git a/src/notifiers/jobnotifier.cpp b/src/notifiers/jobnotifier.cpp new file mode 100644 index 00000000..fecdc905 --- /dev/null +++ b/src/notifiers/jobnotifier.cpp @@ -0,0 +1,20 @@ +#include "jobnotifier.h" + +JobNotifier::JobNotifier(QObject* parent) +{ + setParent(parent); +} + +void +JobNotifier::notifyCompleted(QPointer job) +{ + QString msg = tr("Download Completed") + ": " + job->name(); + emit notify(success, msg); +} + +void +JobNotifier::notifyFailed(QPointer job) +{ + QString msg = tr("Download Failed") + ": " + job->name(); + emit notify(fail, msg); +} diff --git a/src/notifiers/jobnotifier.h b/src/notifiers/jobnotifier.h new file mode 100644 index 00000000..5088891c --- /dev/null +++ b/src/notifiers/jobnotifier.h @@ -0,0 +1,16 @@ +#ifndef JOBNOTIFIER_H +#define JOBNOTIFIER_H + +#include +#include +#include + +class JobNotifier : public NotificationSender +{ +public: + JobNotifier(QObject* parent); + void notifyCompleted(QPointer job); + void notifyFailed(QPointer job); +}; + +#endif // JOBNOTIFIER_H diff --git a/src/notifiers/updatenotifier.cpp b/src/notifiers/updatenotifier.cpp new file mode 100644 index 00000000..e44f8789 --- /dev/null +++ b/src/notifiers/updatenotifier.cpp @@ -0,0 +1,17 @@ +#include "updatenotifier.h" + +UpdateNotifier::UpdateNotifier(QObject* parent) {} + +void +UpdateNotifier::notifyUpdate(QString version) +{ + QString msg = tr("Update available") + ": " + version; + emit notify(updateInfo, msg); +} + +void +UpdateNotifier::notifyLatest() +{ + QString msg = tr("You are running the latest version"); + emit notify(success, msg); +} diff --git a/src/notifiers/updatenotifier.h b/src/notifiers/updatenotifier.h new file mode 100644 index 00000000..5c6c92fe --- /dev/null +++ b/src/notifiers/updatenotifier.h @@ -0,0 +1,14 @@ +#ifndef UPDATENOTIFIER_H +#define UPDATENOTIFIER_H + +#include + +class UpdateNotifier : public NotificationSender +{ +public: + UpdateNotifier(QObject* parent); + void notifyUpdate(QString version); + void notifyLatest(); +}; + +#endif // UPDATENOTIFIER_H diff --git a/src/utils/shortcuthandler.cpp b/src/utils/shortcuthandler.cpp index 05b3fff0..c1f36f8d 100644 --- a/src/utils/shortcuthandler.cpp +++ b/src/utils/shortcuthandler.cpp @@ -8,6 +8,7 @@ #include #include #include +using std::make_pair; QMap ShortcutHandler::shortcutsDescription; @@ -71,109 +72,43 @@ ShortcutHandler::createShortcuts(QObject* context) void ShortcutHandler::setupConnections() { - connect(m_shortcuts.value("Quit"), &QShortcut::activated, this, []() { - emit QApplication::exit(); - }); - connect(m_shortcuts.value("TogglePlayerControls"), - &QShortcut::activated, - this, - &ShortcutHandler::togglePlayerControls); - connect(m_shortcuts.value("ToggleReaderView"), - &QShortcut::activated, - this, - &ShortcutHandler::toggleReaderView); - connect(m_shortcuts.value("ToggleMenubar"), - &QShortcut::activated, - this, - &ShortcutHandler::toggleMenubar); - connect(m_shortcuts.value("ToggleNavDock"), - &QShortcut::activated, - this, - &ShortcutHandler::toggleNavDock); - connect(m_shortcuts.value("TogglePlayback"), - &QShortcut::activated, - this, - &ShortcutHandler::togglePlayback); - connect(m_shortcuts.value("VolumeUp"), - &QShortcut::activated, - this, - &ShortcutHandler::incrementVolume); - connect(m_shortcuts.value("VolumeDown"), - &QShortcut::activated, - this, - &ShortcutHandler::decrementVolume); - connect(m_shortcuts.value("BookmarkCurrent"), - &QShortcut::activated, - this, - &ShortcutHandler::bookmarkCurrent); - connect(m_shortcuts.value("NextPage"), - &QShortcut::activated, - this, - &ShortcutHandler::nextPage); - connect(m_shortcuts.value("PrevPage"), - &QShortcut::activated, - this, - &ShortcutHandler::prevPage); - connect(m_shortcuts.value("NextVerse"), - &QShortcut::activated, - this, - &ShortcutHandler::nextVerse); - connect(m_shortcuts.value("PrevVerse"), - &QShortcut::activated, - this, - &ShortcutHandler::prevVerse); - connect(m_shortcuts.value("NextJuz"), - &QShortcut::activated, - this, - &ShortcutHandler::nextJuz); - connect(m_shortcuts.value("PrevJuz"), - &QShortcut::activated, - this, - &ShortcutHandler::prevJuz); - connect(m_shortcuts.value("NextSurah"), - &QShortcut::activated, - this, - &ShortcutHandler::nextSurah); - connect(m_shortcuts.value("PrevSurah"), - &QShortcut::activated, - this, - &ShortcutHandler::prevSurah); - connect(m_shortcuts.value("ZoomIn"), - &QShortcut::activated, - this, - &ShortcutHandler::zoomIn); - connect(m_shortcuts.value("ZoomOut"), - &QShortcut::activated, - this, - &ShortcutHandler::zoomOut); - connect(m_shortcuts.value("DownloaderDialog"), - &QShortcut::activated, - this, - &ShortcutHandler::openDownloads); - connect(m_shortcuts.value("BookmarksDialog"), - &QShortcut::activated, - this, - &ShortcutHandler::openBookmarks); - connect(m_shortcuts.value("KhatmahDialog"), - &QShortcut::activated, - this, - &ShortcutHandler::openKhatmah); - connect(m_shortcuts.value("SearchDialog"), - &QShortcut::activated, - this, - &ShortcutHandler::openSearch); - connect(m_shortcuts.value("SettingsDialog"), - &QShortcut::activated, - this, - &ShortcutHandler::openSettings); - connect(m_shortcuts.value("ContentDialog"), - &QShortcut::activated, - this, - &ShortcutHandler::openTafsir); - connect(m_shortcuts.value("CopyDialog"), - &QShortcut::activated, - this, - &ShortcutHandler::openAdvancedCopy); + connect(m_shortcuts.value("Quit"), + &QShortcut::activated, + qApp, + &QApplication::quit); + for (const auto& connection : { + make_pair("TogglePlayerControls", + &ShortcutHandler::togglePlayerControls), + make_pair("ToggleReaderView", &ShortcutHandler::toggleReaderView), + make_pair("ToggleMenubar", &ShortcutHandler::toggleMenubar), + make_pair("ToggleNavDock", &ShortcutHandler::toggleNavDock), + make_pair("TogglePlayback", &ShortcutHandler::togglePlayback), + make_pair("VolumeUp", &ShortcutHandler::incrementVolume), + make_pair("VolumeDown", &ShortcutHandler::decrementVolume), + make_pair("BookmarkCurrent", &ShortcutHandler::bookmarkCurrent), + make_pair("NextPage", &ShortcutHandler::nextPage), + make_pair("PrevPage", &ShortcutHandler::prevPage), + make_pair("NextJuz", &ShortcutHandler::nextJuz), + make_pair("PrevJuz", &ShortcutHandler::prevJuz), + make_pair("NextSurah", &ShortcutHandler::nextSurah), + make_pair("PrevSurah", &ShortcutHandler::prevSurah), + make_pair("NextVerse", &ShortcutHandler::nextVerse), + make_pair("PrevVerse", &ShortcutHandler::prevVerse), + make_pair("ZoomIn", &ShortcutHandler::zoomIn), + make_pair("ZoomOut", &ShortcutHandler::zoomOut), + make_pair("DownloaderDialog", &ShortcutHandler::openDownloads), + make_pair("BookmarksDialog", &ShortcutHandler::openBookmarks), + make_pair("KhatmahDialog", &ShortcutHandler::openKhatmah), + make_pair("SearchDialog", &ShortcutHandler::openSearch), + make_pair("SettingsDialog", &ShortcutHandler::openSettings), + make_pair("ContentDialog", &ShortcutHandler::openContent), + make_pair("CopyDialog", &ShortcutHandler::openAdvancedCopy), + }) { + connect(m_shortcuts.value(connection.first), + &QShortcut::activated, + this, + connection.second); + } } void diff --git a/src/utils/shortcuthandler.h b/src/utils/shortcuthandler.h index 7b6263a2..9245c0c6 100644 --- a/src/utils/shortcuthandler.h +++ b/src/utils/shortcuthandler.h @@ -64,7 +64,7 @@ public slots: void openDownloads(); void openSearch(); void openSettings(); - void openTafsir(); + void openContent(); void openAdvancedCopy(); private: diff --git a/src/utils/versionchecker.cpp b/src/utils/versionchecker.cpp index 41608b75..18ee6df0 100644 --- a/src/utils/versionchecker.cpp +++ b/src/utils/versionchecker.cpp @@ -5,6 +5,7 @@ VersionChecker::VersionChecker(QObject* parent) : QObject(parent) + , m_notifier(this) , m_updateTool(QApplication::applicationDirPath() + QDir::separator() + "QCMaintenanceTool.exe") { @@ -54,8 +55,14 @@ VersionChecker::handleReply(QPointer reply) { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (status == 200) - emit versionFound(reply->readAll().trimmed()); + if (status != 200) + return; + + QString version(reply->readAll().trimmed()); + if (qApp->applicationVersion() == version) + m_notifier.notifyLatest(); + else + m_notifier.notifyUpdate(version); } void @@ -70,3 +77,9 @@ VersionChecker::toolExists() { return QFileInfo::exists(m_updateTool); } + +const UpdateNotifier* +VersionChecker::notifier() const +{ + return &m_notifier; +} diff --git a/src/utils/versionchecker.h b/src/utils/versionchecker.h index 10750b2f..2a03430b 100644 --- a/src/utils/versionchecker.h +++ b/src/utils/versionchecker.h @@ -7,30 +7,31 @@ #include #include #include +#include +#include class VersionChecker : public QObject { Q_OBJECT public: explicit VersionChecker(QObject* parent = nullptr); + const UpdateNotifier* notifier() const; public slots: void checkUpdates(); -signals: - void versionFound(QString version); - private slots: void handleToolOutput(); void handleReply(QPointer reply); private: - void getLatestVersion(); bool toolExists(); - QNetworkAccessManager m_netMgr; - QString m_updateTool; + void getLatestVersion(); QProcess m_runner; + QString m_updateTool; QNetworkRequest m_versionReq; + QNetworkAccessManager m_netMgr; + UpdateNotifier m_notifier; }; #endif // VERSIONCHECKER_H diff --git a/src/widgets/notificationpopup.cpp b/src/widgets/notificationpopup.cpp index a89290b6..662302e0 100644 --- a/src/widgets/notificationpopup.cpp +++ b/src/widgets/notificationpopup.cpp @@ -58,83 +58,25 @@ NotificationPopup::setupConnections() } void -NotificationPopup::setDockArea(Qt::DockWidgetArea dockPos) +NotificationPopup::registerSender(NotificationSender* sender) { - m_dockArea = dockPos; + connect( + sender, &NotificationSender::notify, this, &NotificationPopup::notify); } void -NotificationPopup::notify(QString message, NotificationPopup::Action icon) +NotificationPopup::setStyle(NotificationType type) { - QFontMetrics fm(m_textWidget->fontMetrics()); - m_textWidget->setText(message); - setNotificationIcon(icon); - - resize(fm.size(Qt::TextSingleLine, message).width() + 50, 40); - adjustLocation(); - move(m_notificationPos); - this->show(); - - if (m_fadeoutAnim->state() == QAbstractAnimation::Running) - m_fadeoutAnim->stop(); - m_opacityEffect->setOpacity(1); - m_notificationPeriod.start(); + if (type == NotificationSender::fail) + setStyleSheet("QFrame#Popup { background-color: #a50500 }"); + else + setStyleSheet(""); } void -NotificationPopup::completedDownload(QSharedPointer job) -{ - setStyleSheet(""); - QString msg = tr("Download Completed") + ": " + job->name(); - this->notify(msg, success); -} - -void -NotificationPopup::downloadError(QSharedPointer job) -{ - setStyleSheet("QFrame#Popup { background-color: #a50500 }"); - QString msg = tr("Download Failed") + ": " + job->name(); - this->notify(msg, fail); -} - -void -NotificationPopup::bookmarkAdded() -{ - setStyleSheet(""); - QString msg = tr("Verse added to bookmarks"); - this->notify(msg, bookmarkAdd); -} - -void -NotificationPopup::bookmarkRemoved() -{ - setStyleSheet(""); - QString msg = tr("Verse removed from bookmarks"); - this->notify(msg, bookmarkRemove); -} - -void -NotificationPopup::copiedToClipboard() -{ - setStyleSheet(""); - QString msg = tr("Verse text copied to clipboard"); - this->notify(msg, copiedText); -} - -void -NotificationPopup::checkUpdate(QString appVer) +NotificationPopup::setDockArea(Qt::DockWidgetArea dockPos) { - if (appVer.isEmpty()) - return; - - QString msg; - if (qApp->applicationVersion() == appVer) { - msg = tr("You are running the latest version"); - this->notify(msg, success); - } else { - msg = tr("Update available") + ": " + appVer; - this->notify(msg, updateInfo); - } + m_dockArea = dockPos; } void @@ -156,31 +98,31 @@ NotificationPopup::adjustLocation() } void -NotificationPopup::setNotificationIcon(Action icon) +NotificationPopup::setNotificationIcon(NotificationType type) { QString ico; int faStyle = fa_solid; - switch (icon) { - case NotificationPopup::info: + switch (type) { + case NotificationSender::info: ico = fa_info_circle; break; - case NotificationPopup::success: + case NotificationSender::success: ico = fa_check_circle; break; - case NotificationPopup::fail: + case NotificationSender::fail: ico = fa_xmark_circle; break; - case NotificationPopup::bookmarkAdd: + case NotificationSender::bookmarkAdd: ico = fa_bookmark; break; - case NotificationPopup::bookmarkRemove: + case NotificationSender::bookmarkRemove: ico = fa_bookmark; faStyle = fa_regular; break; - case NotificationPopup::copiedText: + case NotificationSender::copiedText: ico = fa_clipboard; break; - case NotificationPopup::updateInfo: + case NotificationSender::updateInfo: ico = fa_circle_up; break; } @@ -189,6 +131,24 @@ NotificationPopup::setNotificationIcon(Action icon) m_iconWidget->setText(ico); } +void +NotificationPopup::notify(NotificationType type, QString message) +{ + QFontMetrics fm(m_textWidget->fontMetrics()); + m_textWidget->setText(message); + setNotificationIcon(type); + + resize(fm.size(Qt::TextSingleLine, message).width() + 50, 40); + adjustLocation(); + move(m_notificationPos); + this->show(); + + if (m_fadeoutAnim->state() == QAbstractAnimation::Running) + m_fadeoutAnim->stop(); + m_opacityEffect->setOpacity(1); + m_notificationPeriod.start(); +} + QPoint NotificationPopup::notificationPos() const { diff --git a/src/widgets/notificationpopup.h b/src/widgets/notificationpopup.h index 9ec84421..d70b830e 100644 --- a/src/widgets/notificationpopup.h +++ b/src/widgets/notificationpopup.h @@ -17,9 +17,11 @@ #include #include #include +#include #include #include #include +typedef NotificationSender::Type NotificationType; /** * @brief NotificationPopup class represents an in-app popup for notifying the @@ -29,20 +31,6 @@ class NotificationPopup : public QFrame { Q_OBJECT public: - /** - * @brief The Action enum represents all possible action to notify the user of - */ - enum Action - { - info, ///< general information - success, ///< successful operation - fail, ///< failed operation - bookmarkAdd, ///< bookmark addition - bookmarkRemove, ///< bookmark removal - copiedText, ///< verse text copied - updateInfo ///< version information - }; - /** * @brief class constructor * @param parent - pointer to parent widget @@ -50,12 +38,12 @@ class NotificationPopup : public QFrame */ explicit NotificationPopup(QWidget* parent = nullptr); /** - * @brief show popup with the given message and action icon - * @param message - QString of message to show - * @param icon - NotificationPopup::Action entry + * @brief registerSender + * @param sender + * + * MODIFIED */ - void notify(QString message, Action icon); - + void registerSender(NotificationSender* sender); /** * @brief adjust the popup position based on the position of the side dock * position in the main window @@ -75,36 +63,14 @@ public slots: * @param dockPos - new dock position relative to the main window */ void setDockArea(Qt::DockWidgetArea dockPos); + +private slots: /** - * @brief slot to show a notification on download completion - * @param reciterIdx - ::Globals::recitersList index for the reciter - * @param surah - the surah that was downloaded - */ - void completedDownload(QSharedPointer job); - /** - * @brief slot to show a notification on download error - * @param reciterIdx - ::Globals::recitersList index for the reciter - * @param surah - the surah that was downloaded - */ - void downloadError(QSharedPointer job); - /** - * @brief slot to show a notification on bookmark addition - */ - void bookmarkAdded(); - /** - * @brief slot to show a notification on bookmark removal - */ - void bookmarkRemoved(); - /** - * @brief slot to show a notification after verse text is copied to clipboard - */ - void copiedToClipboard(); - /** - * @brief slot to check the passed version against the application version and - * show notification accordingly - * @param appVer - the fetched application version + * @brief show popup with the given message and action icon + * @param message - QString of message to show + * @param icon - NotificationPopup::Action entry */ - void checkUpdate(QString appVer); + void notify(NotificationType icon, QString message); private: QList>& m_recitersList = Reciter::reciters; @@ -116,11 +82,16 @@ public slots: * components and shortcuts. */ void setupConnections(); + /** + * @brief setStyle + * @param type + */ + void setStyle(NotificationType type); /** * @brief set the popup icon according to the given Action * @param icon - NotificationPopup::Action entry */ - void setNotificationIcon(Action icon); + void setNotificationIcon(NotificationType type); QPointer m_iconWidget; QPointer m_textWidget; QPointer m_fadeoutAnim; From bd2513e08c0e0e08b8f03c7ba0d34492c72ae5a7 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Wed, 28 Feb 2024 23:12:49 +0200 Subject: [PATCH 33/51] feat: move import and export options to file menu --- src/core/mainwindow.cpp | 375 ++++++++++++++--------------- src/core/mainwindow.h | 14 +- src/core/mainwindow.ui | 13 + src/dialogs/importexportdialog.cpp | 21 +- src/dialogs/importexportdialog.h | 12 +- src/dialogs/importexportdialog.ui | 8 +- src/dialogs/settingsdialog.cpp | 28 --- src/dialogs/settingsdialog.h | 8 - src/dialogs/settingsdialog.ui | 208 +++++++--------- 9 files changed, 318 insertions(+), 369 deletions(-) diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 73116f5e..0a824a4c 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -10,6 +10,7 @@ #include #include using namespace fa; +using std::make_pair; MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) @@ -59,6 +60,10 @@ MainWindow::loadIcons() StyleManager::awesome->icon(fa_solid, fa_columns)); ui->actionUpdates->setIcon( StyleManager::awesome->icon(fa_solid, fa_arrow_rotate_right)); + ui->actionImport->setIcon( + StyleManager::awesome->icon(fa_solid, fa_file_arrow_down)); + ui->actionExport->setIcon( + StyleManager::awesome->icon(fa_solid, fa_file_arrow_up)); } void @@ -90,6 +95,11 @@ MainWindow::loadComponents() m_contentDlg = new ContentDialog(this); m_jobMgr = new JobManager(this); m_versionChecker = new VersionChecker(this); + m_selectorDlg = new FileSelector(this); + m_importExportDlg = + new ImportExportDialog(this, + QSharedPointer::create(), + QSharedPointer::create()); QHBoxLayout* controls = new QHBoxLayout(); QFrame* controlsFrame = new QFrame(this); @@ -118,123 +128,134 @@ MainWindow::loadComponents() ui->cmbPage->setCurrentIndex(m_currVerse->page() - 1); } +void +MainWindow::setupConnections() +{ + connectMenubar(); + connectTray(); + connectReader(); + connectPlayer(); + connectControls(); + connectSettings(); + connectNotifiers(); +} + void MainWindow::setupShortcuts() { m_shortcutHandler->createShortcuts(this); + for (const auto& connection : { + make_pair(&ShortcutHandler::toggleMenubar, &MainWindow::toggleMenubar), + make_pair(&ShortcutHandler::toggleNavDock, &MainWindow::toggleNavDock), + make_pair(&ShortcutHandler::nextVerse, &MainWindow::nextVerse), + make_pair(&ShortcutHandler::prevVerse, &MainWindow::prevVerse), + make_pair(&ShortcutHandler::nextSurah, &MainWindow::nextSurah), + make_pair(&ShortcutHandler::prevSurah, &MainWindow::prevSurah), + make_pair(&ShortcutHandler::nextJuz, &MainWindow::nextJuz), + make_pair(&ShortcutHandler::prevJuz, &MainWindow::prevJuz), + make_pair(&ShortcutHandler::bookmarkCurrent, + &MainWindow::bookmarkCurrent), + make_pair(&ShortcutHandler::openDownloads, + &MainWindow::actionDMTriggered), + make_pair(&ShortcutHandler::openBookmarks, + &MainWindow::actionBookmarksTriggered), + make_pair(&ShortcutHandler::openKhatmah, + &MainWindow::actionKhatmahTriggered), + make_pair(&ShortcutHandler::openSearch, + &MainWindow::actionSearchTriggered), + make_pair(&ShortcutHandler::openSettings, + &MainWindow::actionPrefTriggered), + make_pair(&ShortcutHandler::openContent, + &MainWindow::actionTafsirTriggered), + make_pair(&ShortcutHandler::openAdvancedCopy, + &MainWindow::actionAdvancedCopyTriggered), + }) { + connect( + m_shortcutHandler.data(), connection.first, this, connection.second); + } +} - connect(m_shortcutHandler.data(), - &ShortcutHandler::toggleMenubar, - this, - &MainWindow::toggleMenubar); - connect(m_shortcutHandler.data(), - &ShortcutHandler::toggleNavDock, - this, - &MainWindow::toggleNavDock); - connect(m_shortcutHandler.data(), - &ShortcutHandler::bookmarkCurrent, - this, - &MainWindow::addCurrentToBookmarks); - connect(m_shortcutHandler.data(), - &ShortcutHandler::openDownloads, +void +MainWindow::connectPlayer() +{ + connect(m_player, + &VersePlayer::missingVerseFile, this, - &MainWindow::actionDMTriggered); - connect(m_shortcutHandler.data(), - &ShortcutHandler::openBookmarks, + &MainWindow::missingRecitationFileWarn); + connect(m_player, + &QMediaPlayer::playbackStateChanged, this, - &MainWindow::actionBookmarksTriggered); - connect(m_shortcutHandler.data(), - &ShortcutHandler::openKhatmah, + &MainWindow::updateTrayTooltip); + connect(m_player, + &QMediaPlayer::mediaStatusChanged, this, - &MainWindow::actionKhatmahTriggered); - connect(m_shortcutHandler.data(), - &ShortcutHandler::openSearch, + &MainWindow::mediaStatusChanged); +} + +void +MainWindow::connectTray() +{ + connect(m_systemTray, &SystemTray::exit, this, &QApplication::exit); + connect(m_systemTray, + &SystemTray::togglePlayback, + m_playerControls, + &PlayerControls::togglePlayback); + connect(m_systemTray, &SystemTray::showWindow, this, &MainWindow::show); + connect(m_systemTray, &SystemTray::hideWindow, this, &MainWindow::hide); + connect(m_systemTray, + &SystemTray::checkForUpdates, + m_versionChecker, + &VersionChecker::checkUpdates); + connect(m_systemTray, + &SystemTray::openAbout, this, - &MainWindow::actionSearchTriggered); - connect(m_shortcutHandler.data(), - &ShortcutHandler::openSettings, + &MainWindow::actionAboutTriggered); + connect(m_systemTray, + &SystemTray::openPrefs, this, &MainWindow::actionPrefTriggered); - connect(m_shortcutHandler.data(), - &ShortcutHandler::openTafsir, - this, - &MainWindow::actionTafsirTriggered); - connect(m_shortcutHandler.data(), - &ShortcutHandler::openAdvancedCopy, - this, - &MainWindow::actionAdvancedCopyTriggered); - connect(m_shortcutHandler.data(), - &ShortcutHandler::nextVerse, - this, - &MainWindow::nextVerse); - connect(m_shortcutHandler.data(), - &ShortcutHandler::prevVerse, - this, - &MainWindow::prevVerse); - connect(m_shortcutHandler.data(), - &ShortcutHandler::nextSurah, - this, - &MainWindow::nextSurah); - connect(m_shortcutHandler.data(), - &ShortcutHandler::prevSurah, - this, - &MainWindow::prevSurah); - connect(m_shortcutHandler.data(), - &ShortcutHandler::nextJuz, - this, - &MainWindow::nextJuz); - connect(m_shortcutHandler.data(), - &ShortcutHandler::prevJuz, - this, - &MainWindow::prevJuz); } void -MainWindow::setupConnections() +MainWindow::connectReader() { - // ########## Menubar ########## // - connect(ui->actionExit, &QAction::triggered, this, &QApplication::exit); - connect(ui->actionPereferences, - &QAction::triggered, - this, - &MainWindow::actionPrefTriggered); - connect(ui->actionDownloadManager, - &QAction::triggered, - this, - &MainWindow::actionDMTriggered); - connect(ui->actionFind, - &QAction::triggered, - this, - &MainWindow::actionSearchTriggered); - connect(ui->actionTafsir, - &QAction::triggered, - this, - &MainWindow::actionTafsirTriggered); - connect(ui->actionVOTD, - &QAction::triggered, - this, - &MainWindow::actionVotdTriggered); - connect(ui->actionAdvancedCopy, - &QAction::triggered, - this, - &MainWindow::actionAdvancedCopyTriggered); - connect(ui->actionBookmarks, - &QAction::triggered, - this, - &MainWindow::actionBookmarksTriggered); - connect(ui->actionKhatmah, - &QAction::triggered, - this, - &MainWindow::actionKhatmahTriggered); - connect(ui->actionAboutQC, - &QAction::triggered, + connect(m_verseDlg, + &VerseDialog::navigateToVerse, + m_reader, + &QuranReader::navigateToVerse); + connect(m_reader, + &QuranReader::copyVerseText, + m_cpyDlg, + &CopyDialog::copyVerseText); + connect(m_reader, + &QuranReader::showVerseTafsir, + m_contentDlg, + &ContentDialog::showVerseTafsir); + connect(m_reader, + &QuranReader::showVerseTranslation, + m_contentDlg, + &ContentDialog::showVerseTranslation); + connect(m_reader, + &QuranReader::showVerseThoughts, + m_contentDlg, + &ContentDialog::showVerseThoughts); + connect(m_reader, + &QuranReader::showBetaqa, + m_betaqaViewer, + &BetaqaViewer::showSurah); + connect(m_contentDlg, + &ContentDialog::missingTafsir, this, - &MainWindow::actionAboutTriggered); - connect(ui->actionUpdates, - &QAction::triggered, + &MainWindow::missingTafsir); + connect(m_contentDlg, + &ContentDialog::missingTranslation, this, - &MainWindow::actionUpdatesTriggered); + &MainWindow::missingTranslation); +} +void +MainWindow::connectControls() +{ // ########## page controls ########## // connect(ui->cmbPage, &QComboBox::currentIndexChanged, @@ -264,7 +285,6 @@ MainWindow::setupConnections() &PlayerControls::currentSurahChanged, this, &MainWindow::currentSurahChanged); - // ########## navigation dock ########## // connect(ui->lineEditSearchSurah, &QLineEdit::textChanged, @@ -274,60 +294,11 @@ MainWindow::setupConnections() &QListView::clicked, this, &MainWindow::listSurahNameClicked); +} - // ########## system tray ########## // - connect(m_systemTray, &SystemTray::exit, this, &QApplication::exit); - connect(m_systemTray, - &SystemTray::togglePlayback, - m_playerControls, - &PlayerControls::togglePlayback); - connect(m_systemTray, &SystemTray::showWindow, this, &MainWindow::show); - connect(m_systemTray, &SystemTray::hideWindow, this, &MainWindow::hide); - connect(m_systemTray, - &SystemTray::checkForUpdates, - m_versionChecker, - &VersionChecker::checkUpdates); - connect(m_systemTray, - &SystemTray::openAbout, - this, - &MainWindow::actionAboutTriggered); - connect(m_systemTray, - &SystemTray::openPrefs, - this, - &MainWindow::actionPrefTriggered); - - // ########## Notification Popup ########## // - connect(ui->sideDock, - &QDockWidget::dockLocationChanged, - m_popup, - &NotificationPopup::setDockArea); - connect(m_jobMgr, - &JobManager::jobCompleted, - m_popup, - &NotificationPopup::completedDownload); - connect(m_jobMgr, - &JobManager::jobFailed, - m_popup, - &NotificationPopup::downloadError); - connect(m_versionChecker, - &VersionChecker::versionFound, - m_popup, - &NotificationPopup::checkUpdate); - connect(m_cpyDlg, - &CopyDialog::verseCopied, - m_popup, - &NotificationPopup::copiedToClipboard); - connect(m_bookmarksDb.data(), - &BookmarksDb::bookmarkAdded, - m_popup, - &NotificationPopup::bookmarkAdded); - connect(m_bookmarksDb.data(), - &BookmarksDb::bookmarkRemoved, - m_popup, - &NotificationPopup::bookmarkRemoved); - - // ########## Settings Dialog ########## // - // Restart signal +void +MainWindow::connectSettings() +{ connect( m_settingsDlg, &SettingsDialog::restartApp, this, &MainWindow::restartApp); // qcf2 missing files warning @@ -373,56 +344,46 @@ MainWindow::setupConnections() &SettingsDialog::shortcutChanged, m_shortcutHandler.data(), &ShortcutHandler::shortcutChanged); +} - connect(m_player, - &VersePlayer::missingVerseFile, - this, - &MainWindow::missingRecitationFileWarn); - connect(m_player, - &QMediaPlayer::playbackStateChanged, - this, - &MainWindow::updateTrayTooltip); - connect(m_player, - &QMediaPlayer::mediaStatusChanged, - this, - &MainWindow::mediaStatusChanged); - +void +MainWindow::connectMenubar() +{ + connect(ui->actionExit, &QAction::triggered, this, &QApplication::exit); connect(ui->actionReaderViewToggle, &QAction::triggered, m_reader, &QuranReader::toggleReaderView); - connect(m_verseDlg, - &VerseDialog::navigateToVerse, - m_reader, - &QuranReader::navigateToVerse); - connect(m_reader, - &QuranReader::copyVerseText, - m_cpyDlg, - &CopyDialog::copyVerseText); - connect(m_reader, - &QuranReader::showVerseTafsir, - m_contentDlg, - &ContentDialog::showVerseTafsir); - connect(m_reader, - &QuranReader::showVerseTranslation, - m_contentDlg, - &ContentDialog::showVerseTranslation); - connect(m_reader, - &QuranReader::showVerseThoughts, - m_contentDlg, - &ContentDialog::showVerseThoughts); - connect(m_reader, - &QuranReader::showBetaqa, - m_betaqaViewer, - &BetaqaViewer::showSurah); - connect(m_contentDlg, - &ContentDialog::missingTafsir, - this, - &MainWindow::missingTafsir); - connect(m_contentDlg, - &ContentDialog::missingTranslation, - this, - &MainWindow::missingTranslation); + for (const auto& connection : { + make_pair(ui->actionPereferences, &MainWindow::actionPrefTriggered), + make_pair(ui->actionDownloadManager, &MainWindow::actionDMTriggered), + make_pair(ui->actionFind, &MainWindow::actionSearchTriggered), + make_pair(ui->actionTafsir, &MainWindow::actionTafsirTriggered), + make_pair(ui->actionVOTD, &MainWindow::actionVotdTriggered), + make_pair(ui->actionBookmarks, &MainWindow::actionBookmarksTriggered), + make_pair(ui->actionKhatmah, &MainWindow::actionKhatmahTriggered), + make_pair(ui->actionAboutQC, &MainWindow::actionAboutTriggered), + make_pair(ui->actionUpdates, &MainWindow::actionUpdatesTriggered), + make_pair(ui->actionImport, &MainWindow::importUserData), + make_pair(ui->actionExport, &MainWindow::exportUserData), + make_pair(ui->actionAdvancedCopy, + &MainWindow::actionAdvancedCopyTriggered), + }) { + connect(connection.first, &QAction::triggered, this, connection.second); + } +} + +void +MainWindow::connectNotifiers() +{ + m_popup->registerSender((NotificationSender*)(m_jobMgr->notifier())); + m_popup->registerSender((NotificationSender*)(m_versionChecker->notifier())); + m_popup->registerSender((NotificationSender*)(m_cpyDlg->notifier())); + m_popup->registerSender((NotificationSender*)(m_bookmarksDb->notifier())); + connect(ui->sideDock, + &QDockWidget::dockLocationChanged, + m_popup, + &NotificationPopup::setDockArea); } void @@ -641,7 +602,7 @@ MainWindow::updateTrayTooltip(QMediaPlayer::PlaybackState state) } void -MainWindow::addCurrentToBookmarks() +MainWindow::bookmarkCurrent() { if (!m_bookmarksDb->isBookmarked(*m_currVerse)) m_bookmarksDb->addBookmark(*m_currVerse, false); @@ -878,6 +839,22 @@ MainWindow::toggleNavDock() ui->sideDock->toggleViewAction()->toggle(); } +void +MainWindow::importUserData() +{ + QString path = m_selectorDlg->selectJson(FileSelector::Read); + if (!path.isEmpty()) + m_importExportDlg->selectImports(path); +} + +void +MainWindow::exportUserData() +{ + QString path = m_selectorDlg->selectJson(FileSelector::Write); + if (!path.isEmpty()) + m_importExportDlg->selectExports(path); +} + void MainWindow::resizeEvent(QResizeEvent* event) { diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 79ebac88..da839118 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -152,7 +152,7 @@ private slots: /** * @brief adds the current ::Verse to the bookmarks */ - void addCurrentToBookmarks(); + void bookmarkCurrent(); /** * @brief actionUpdatesTriggered */ @@ -246,6 +246,8 @@ private slots: * @brief toggles visiblity of the navigation dock */ void toggleNavDock(); + void importUserData(); + void exportUserData(); private: Ui::MainWindow* ui; @@ -279,6 +281,13 @@ private slots: * shortcuts */ void setupConnections(); + void connectPlayer(); + void connectTray(); + void connectReader(); + void connectControls(); + void connectSettings(); + void connectMenubar(); + void connectNotifiers(); /** * @brief initialize the surahs QListView in the side dock and select the * current verse's surah @@ -403,6 +412,9 @@ private slots: * @brief pointer to the votd dialog */ QPointer m_verseDlg; + QPointer m_selectorDlg; + QPointer m_importExportDlg; + /** * @brief pointer to the QProcess instance of the maintainence tool that * checks for updates diff --git a/src/core/mainwindow.ui b/src/core/mainwindow.ui index b2b55461..b46c414b 100644 --- a/src/core/mainwindow.ui +++ b/src/core/mainwindow.ui @@ -130,6 +130,9 @@ + + + @@ -517,6 +520,16 @@ Toggle reader view + + + Import + + + + + Export + + diff --git a/src/dialogs/importexportdialog.cpp b/src/dialogs/importexportdialog.cpp index e153119c..6ac9db20 100644 --- a/src/dialogs/importexportdialog.cpp +++ b/src/dialogs/importexportdialog.cpp @@ -3,9 +3,10 @@ #include -ImportExportDialog::ImportExportDialog(QWidget* parent, - QPointer importer, - QPointer exporter) +ImportExportDialog::ImportExportDialog( + QWidget* parent, + QSharedPointer importer, + QSharedPointer exporter) : ui(new Ui::ImportExportDialog) , m_importer(importer) , m_exporter(exporter) @@ -16,11 +17,11 @@ ImportExportDialog::ImportExportDialog(QWidget* parent, &QDialogButtonBox::clicked, this, &ImportExportDialog::dialogButtonClicked); - connect(m_importer, + connect(m_importer.data(), &UserDataImporter::error, this, &ImportExportDialog::importError); - connect(m_exporter, + connect(m_exporter.data(), &UserDataExporter::error, this, &ImportExportDialog::exportError); @@ -93,14 +94,16 @@ ImportExportDialog::exportSelected() const m_exporter->save(); } -void ImportExportDialog::setExporter(QPointer newExporter) +void +ImportExportDialog::setExporter(QSharedPointer newExporter) { - m_exporter = newExporter; + m_exporter = newExporter; } -void ImportExportDialog::setImporter(QPointer newImporter) +void +ImportExportDialog::setImporter(QSharedPointer newImporter) { - m_importer = newImporter; + m_importer = newImporter; } void diff --git a/src/dialogs/importexportdialog.h b/src/dialogs/importexportdialog.h index 1b21e6a1..9df29395 100644 --- a/src/dialogs/importexportdialog.h +++ b/src/dialogs/importexportdialog.h @@ -21,15 +21,15 @@ class ImportExportDialog : public QDialog Export }; explicit ImportExportDialog(QWidget* parent, - QPointer importer, - QPointer exporter); + QSharedPointer importer, + QSharedPointer exporter); ~ImportExportDialog(); void selectImports(QString filepath); void selectExports(QString filepath); - void setImporter(QPointer newImporter); - void setExporter(QPointer newExporter); + void setImporter(QSharedPointer newImporter); + void setExporter(QSharedPointer newExporter); private slots: void dialogButtonClicked(QAbstractButton* btn); @@ -43,8 +43,8 @@ private slots: void setCheckedExports(); void importSelected() const; void exportSelected() const; - QPointer m_importer; - QPointer m_exporter; + QSharedPointer m_importer; + QSharedPointer m_exporter; Mode m_mode; }; diff --git a/src/dialogs/importexportdialog.ui b/src/dialogs/importexportdialog.ui index 744c5d1f..317f2957 100644 --- a/src/dialogs/importexportdialog.ui +++ b/src/dialogs/importexportdialog.ui @@ -16,8 +16,14 @@ 176 + + + 417 + 246 + + - Dialog + Data Selection diff --git a/src/dialogs/settingsdialog.cpp b/src/dialogs/settingsdialog.cpp index 9f01a4bd..68ae9f85 100644 --- a/src/dialogs/settingsdialog.cpp +++ b/src/dialogs/settingsdialog.cpp @@ -13,7 +13,6 @@ SettingsDialog::SettingsDialog(QWidget* parent, VersePlayer* vPlayerPtr) : QDialog(parent) , ui(new Ui::SettingsDialog) , m_vPlayerPtr(vPlayerPtr) - , m_selectorDlg(new FileSelector(this)) { ui->setupUi(this); ui->cmbQuranFontSz->setValidator(new QIntValidator(10, 72)); @@ -23,9 +22,6 @@ SettingsDialog::SettingsDialog(QWidget* parent, VersePlayer* vPlayerPtr) ui->tableViewShortcuts->horizontalHeader()->setStretchLastSection(true); ui->tableViewShortcuts->setItemDelegate(new ShortcutDelegate); - m_importExportDlg = - new ImportExportDialog(this, &m_jsonImporter, &m_jsonExporter); - fillLanguageCombobox(); setCurrentSettingsAsRef(); setupConnections(); @@ -38,14 +34,6 @@ SettingsDialog::setupConnections() &QDialogButtonBox::clicked, this, &SettingsDialog::btnBoxAction); - connect(ui->btnImport, - &QPushButton::clicked, - this, - &SettingsDialog::importUserData); - connect(ui->btnExport, - &QPushButton::clicked, - this, - &SettingsDialog::exportUserData); } void @@ -391,22 +379,6 @@ SettingsDialog::btnBoxAction(QAbstractButton* btn) } } -void -SettingsDialog::importUserData() -{ - QString path = m_selectorDlg->selectJson(FileSelector::Read); - if (!path.isEmpty()) - m_importExportDlg->selectImports(path); -} - -void -SettingsDialog::exportUserData() -{ - QString path = m_selectorDlg->selectJson(FileSelector::Write); - if (!path.isEmpty()) - m_importExportDlg->selectExports(path); -} - void SettingsDialog::showWindow() { diff --git a/src/dialogs/settingsdialog.h b/src/dialogs/settingsdialog.h index 75d993f6..37e388fe 100644 --- a/src/dialogs/settingsdialog.h +++ b/src/dialogs/settingsdialog.h @@ -216,10 +216,6 @@ public slots: */ void closeEvent(QCloseEvent* event); -private slots: - void importUserData(); - void exportUserData(); - private: Ui::SettingsDialog* ui; const int m_qcfVer = Settings::qcfVersion; @@ -320,10 +316,6 @@ private slots: * @brief pointer to VersePlayer instance. */ QPointer m_vPlayerPtr; - QPointer m_selectorDlg; - QPointer m_importExportDlg; - JsonDataImporter m_jsonImporter; - JsonDataExporter m_jsonExporter; /** * @brief model used by the shortcuts QTableView */ diff --git a/src/dialogs/settingsdialog.ui b/src/dialogs/settingsdialog.ui index 23c0ebdd..4c10d3c2 100644 --- a/src/dialogs/settingsdialog.ui +++ b/src/dialogs/settingsdialog.ui @@ -38,128 +38,102 @@ General - + - - - - - - - - Theme - - - - - - - - Light - - - - - Sepia - - - - - Dark - - - - - - - - - - - - Language - - - - - - - - - - - - - - Audio output device - - - - - - - - - - - - - - Features - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Daily verse - - - - - - - Missing recitation warning - - - - - - - - - - - - - + + + + + Theme + + + + + + - Import + Light - - - - + + - Export + Sepia - - - - + + + + Dark + + + + + + + + + + + + Language + + + + + + + + + + + + + + Audio output device + + + + + + + + + + + + + + Features + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Daily verse + + + + + + + Missing recitation warning + + + + From 2df8bc1fcd52e43202ff19b5100d49a9443429d1 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Fri, 1 Mar 2024 14:45:48 +0200 Subject: [PATCH 34/51] refactor: change static utility classes to follow singleton design pattern --- CMakeLists.txt | 4 +- src/core/mainwindow.cpp | 177 ++++++++++++++++------------- src/core/mainwindow.h | 26 +++-- src/core/mainwindow.ui | 14 ++- src/core/playercontrols.cpp | 37 +++--- src/core/playercontrols.h | 14 +-- src/core/quranreader.cpp | 170 ++++++++++++++------------- src/core/quranreader.h | 20 ++-- src/database/betaqatdb.cpp | 15 +-- src/database/betaqatdb.h | 10 +- src/database/bookmarksdb.cpp | 12 +- src/database/bookmarksdb.h | 12 +- src/database/glyphsdb.cpp | 25 ++-- src/database/glyphsdb.h | 18 +-- src/database/qurandb.cpp | 57 +++++----- src/database/qurandb.h | 43 ++++--- src/database/tafsirdb.cpp | 21 ++-- src/database/tafsirdb.h | 18 +-- src/database/translationdb.cpp | 26 +++-- src/database/translationdb.h | 21 ++-- src/dialogs/aboutdialog.cpp | 3 +- src/dialogs/aboutdialog.h | 5 +- src/dialogs/bookmarksdialog.cpp | 33 +++--- src/dialogs/bookmarksdialog.h | 10 +- src/dialogs/contentdialog.cpp | 83 ++++++++------ src/dialogs/contentdialog.h | 22 ++-- src/dialogs/copydialog.cpp | 22 ++-- src/dialogs/copydialog.h | 4 +- src/dialogs/downloaderdialog.cpp | 37 +++--- src/dialogs/downloaderdialog.h | 12 +- src/dialogs/fileselector.cpp | 2 +- src/dialogs/khatmahdialog.cpp | 45 ++++---- src/dialogs/khatmahdialog.h | 8 +- src/dialogs/searchdialog.cpp | 46 ++++---- src/dialogs/searchdialog.h | 11 +- src/dialogs/settingsdialog.cpp | 89 ++++++++------- src/dialogs/settingsdialog.h | 20 ++-- src/dialogs/versedialog.cpp | 17 ++- src/dialogs/versedialog.h | 8 +- src/downloader/contentjob.cpp | 6 +- src/downloader/contentjob.h | 5 +- src/downloader/qcftask.cpp | 2 +- src/downloader/qcftask.h | 2 +- src/downloader/recitationtask.cpp | 6 +- src/downloader/recitationtask.h | 6 +- src/downloader/surahjob.cpp | 6 +- src/downloader/surahjob.h | 4 +- src/downloader/tafsirtask.cpp | 6 +- src/downloader/tafsirtask.h | 4 +- src/downloader/translationtask.cpp | 6 +- src/downloader/translationtask.h | 4 +- src/main.cpp | 16 ++- src/types/reciter.cpp | 21 ++-- src/types/reciter.h | 4 +- src/types/tafsir.cpp | 21 +++- src/types/tafsir.h | 5 +- src/types/translation.cpp | 21 +++- src/types/translation.h | 5 +- src/types/verse.cpp | 10 +- src/types/verse.h | 6 +- src/utils/configuration.cpp | 125 ++++++++++++++++++++ src/utils/configuration.h | 51 +++++++++ src/utils/dirmanager.cpp | 117 ++++++++++++++----- src/utils/dirmanager.h | 27 ++++- src/utils/fontmanager.cpp | 83 ++++++++------ src/utils/fontmanager.h | 28 ++--- src/utils/jsondataexporter.cpp | 10 +- src/utils/jsondataexporter.h | 2 +- src/utils/jsondataimporter.cpp | 8 +- src/utils/jsondataimporter.h | 2 +- src/utils/settings.cpp | 90 --------------- src/utils/settings.h | 41 ------- src/utils/shortcuthandler.cpp | 49 ++++---- src/utils/shortcuthandler.h | 22 ++-- src/utils/stylemanager.cpp | 53 ++++++--- src/utils/stylemanager.h | 16 ++- src/utils/verseplayer.cpp | 26 +++-- src/utils/verseplayer.h | 8 +- src/widgets/betaqaviewer.cpp | 3 +- src/widgets/betaqaviewer.h | 2 +- src/widgets/notificationpopup.cpp | 11 +- src/widgets/notificationpopup.h | 6 +- src/widgets/quranpagebrowser.cpp | 70 ++++++------ src/widgets/quranpagebrowser.h | 11 +- 84 files changed, 1269 insertions(+), 975 deletions(-) create mode 100644 src/utils/configuration.cpp create mode 100644 src/utils/configuration.h delete mode 100644 src/utils/settings.cpp delete mode 100644 src/utils/settings.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f26d7bb1..4734f773 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,8 +101,8 @@ set(PROJECT_SOURCES src/notifiers/copynotifier.cpp src/notifiers/jobnotifier.h src/notifiers/jobnotifier.cpp - src/utils/settings.h - src/utils/settings.cpp + src/utils/configuration.h + src/utils/configuration.cpp src/utils/shortcuthandler.h src/utils/shortcuthandler.cpp src/utils/verseplayer.h diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 0a824a4c..8d5d7a62 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -16,16 +16,24 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_verseValidator(new QIntValidator(this)) + , m_currVerse(Verse::getCurrent()) + , m_config(Configuration::getInstance()) + , m_shortcutHandler(ShortcutHandler::getInstance()) + , m_bookmarksDb(BookmarksDb::getInstance()) + , m_quranDb(QuranDb::getInstance()) + , m_translationDb(TranslationDb::getInstance()) + , m_reciters(Reciter::reciters) + , m_tafasir(Tafsir::tafasir) { ui->setupUi(this); loadIcons(); loadVerse(); loadComponents(); - if (m_settings->value("WindowState").isNull()) - m_settings->setValue("WindowState", saveState()); + if (m_config.settings().value("WindowState").isNull()) + m_config.settings().setValue("WindowState", saveState()); else - restoreState(m_settings->value("WindowState").toByteArray()); + restoreState(m_config.settings().value("WindowState").toByteArray()); // connectors setupShortcuts(); @@ -40,50 +48,41 @@ MainWindow::MainWindow(QWidget* parent) void MainWindow::loadIcons() { - ui->actionKhatmah->setIcon(StyleManager::awesome->icon(fa_solid, fa_list)); - ui->actionDownloadManager->setIcon( - StyleManager::awesome->icon(fa_solid, fa_download)); - ui->actionExit->setIcon(StyleManager::awesome->icon(fa_solid, fa_xmark)); - ui->actionFind->setIcon( - StyleManager::awesome->icon(fa_solid, fa_magnifying_glass)); - ui->actionTafsir->setIcon( - StyleManager::awesome->icon(fa_solid, fa_book_open)); - ui->actionVOTD->setIcon( - StyleManager::awesome->icon(fa_solid, fa_calendar_day)); - ui->actionBookmarks->setIcon( - StyleManager::awesome->icon(fa_solid, fa_bookmark)); - ui->actionPereferences->setIcon( - StyleManager::awesome->icon(fa_solid, fa_gear)); - ui->actionAdvancedCopy->setIcon( - StyleManager::awesome->icon(fa_solid, fa_clipboard)); - ui->actionReaderViewToggle->setIcon( - StyleManager::awesome->icon(fa_solid, fa_columns)); - ui->actionUpdates->setIcon( - StyleManager::awesome->icon(fa_solid, fa_arrow_rotate_right)); - ui->actionImport->setIcon( - StyleManager::awesome->icon(fa_solid, fa_file_arrow_down)); - ui->actionExport->setIcon( - StyleManager::awesome->icon(fa_solid, fa_file_arrow_up)); + fa::QtAwesome& awesome = StyleManager::getInstance().awesome(); + ui->actionKhatmah->setIcon(awesome.icon(fa_solid, fa_list)); + ui->actionDownloadManager->setIcon(awesome.icon(fa_solid, fa_download)); + ui->actionExit->setIcon(awesome.icon(fa_solid, fa_xmark)); + ui->actionFind->setIcon(awesome.icon(fa_solid, fa_magnifying_glass)); + ui->actionTafsir->setIcon(awesome.icon(fa_solid, fa_book_open)); + ui->actionVOTD->setIcon(awesome.icon(fa_solid, fa_calendar_day)); + ui->actionBookmarks->setIcon(awesome.icon(fa_solid, fa_bookmark)); + ui->actionPereferences->setIcon(awesome.icon(fa_solid, fa_gear)); + ui->actionAdvancedCopy->setIcon(awesome.icon(fa_solid, fa_clipboard)); + ui->actionReaderViewToggle->setIcon(awesome.icon(fa_solid, fa_columns)); + ui->actionUpdates->setIcon(awesome.icon(fa_solid, fa_arrow_rotate_right)); + ui->actionImport->setIcon(awesome.icon(fa_solid, fa_file_arrow_down)); + ui->actionExport->setIcon(awesome.icon(fa_solid, fa_file_arrow_up)); } void MainWindow::loadVerse() { - int id = m_settings->value("Reader/Khatmah").toInt(); - Verse v = *m_currVerse; - m_bookmarksDb->setActiveKhatmah(id); - if (!m_bookmarksDb->loadVerse(id, v)) { + int id = m_config.settings().value("Reader/Khatmah").toInt(); + Verse v = m_currVerse; + m_bookmarksDb.setActiveKhatmah(id); + if (!m_bookmarksDb.loadVerse(id, v)) { QString name = id ? tr("Khatmah") + " " + QString::number(id) : tr("Default"); - m_bookmarksDb->addKhatmah(v, name, id); + m_bookmarksDb.addKhatmah(v, name, id); } - m_currVerse->update(v); + m_currVerse.update(v); } void MainWindow::loadComponents() { - m_player = new VersePlayer(this, m_settings->value("Reciter", 0).toInt()); + m_player = + new VersePlayer(this, m_config.settings().value("Reciter", 0).toInt()); m_reader = new QuranReader(this, m_player); m_playerControls = new PlayerControls(this, m_player, m_reader); m_settingsDlg = new SettingsDialog(this, m_player); @@ -122,10 +121,10 @@ MainWindow::loadComponents() } // sets without emitting signal - setCmbVerseIdx(m_currVerse->number() - 1); - setCmbJuzIdx(m_quranDb->getJuzOfPage(m_currVerse->page()) - 1); + setCmbVerseIdx(m_currVerse.number() - 1); + setCmbJuzIdx(m_quranDb.getJuzOfPage(m_currVerse.page()) - 1); - ui->cmbPage->setCurrentIndex(m_currVerse->page() - 1); + ui->cmbPage->setCurrentIndex(m_currVerse.page() - 1); } void @@ -143,7 +142,12 @@ MainWindow::setupConnections() void MainWindow::setupShortcuts() { - m_shortcutHandler->createShortcuts(this); + m_shortcutHandler.createShortcuts(this); + connect( + &m_shortcutHandler, &ShortcutHandler::togglePlayerControls, this, [this]() { + actionPlayerControlsToggled(!m_playerControls->isVisible()); + }); + for (const auto& connection : { make_pair(&ShortcutHandler::toggleMenubar, &MainWindow::toggleMenubar), make_pair(&ShortcutHandler::toggleNavDock, &MainWindow::toggleNavDock), @@ -170,8 +174,7 @@ MainWindow::setupShortcuts() make_pair(&ShortcutHandler::openAdvancedCopy, &MainWindow::actionAdvancedCopyTriggered), }) { - connect( - m_shortcutHandler.data(), connection.first, this, connection.second); + connect(&m_shortcutHandler, connection.first, this, connection.second); } } @@ -324,7 +327,7 @@ MainWindow::connectSettings() &QuranReader::addSideContent); connect(m_settingsDlg, &SettingsDialog::translationChanged, - m_translationDb.data(), + &m_translationDb, &TranslationDb::updateLoadedTranslation); connect(m_settingsDlg, &SettingsDialog::sideFontChanged, @@ -342,7 +345,7 @@ MainWindow::connectSettings() // shortcut change connect(m_settingsDlg, &SettingsDialog::shortcutChanged, - m_shortcutHandler.data(), + &m_shortcutHandler, &ShortcutHandler::shortcutChanged); } @@ -354,6 +357,10 @@ MainWindow::connectMenubar() &QAction::triggered, m_reader, &QuranReader::toggleReaderView); + connect(ui->actionPlayerControls, + &QAction::toggled, + this, + &MainWindow::actionPlayerControlsToggled); for (const auto& connection : { make_pair(ui->actionPereferences, &MainWindow::actionPrefTriggered), make_pair(ui->actionDownloadManager, &MainWindow::actionDMTriggered), @@ -363,6 +370,7 @@ MainWindow::connectMenubar() make_pair(ui->actionBookmarks, &MainWindow::actionBookmarksTriggered), make_pair(ui->actionKhatmah, &MainWindow::actionKhatmahTriggered), make_pair(ui->actionAboutQC, &MainWindow::actionAboutTriggered), + make_pair(ui->actionAboutQt, &MainWindow::actionAboutQttriggered), make_pair(ui->actionUpdates, &MainWindow::actionUpdatesTriggered), make_pair(ui->actionImport, &MainWindow::importUserData), make_pair(ui->actionExport, &MainWindow::exportUserData), @@ -379,7 +387,7 @@ MainWindow::connectNotifiers() m_popup->registerSender((NotificationSender*)(m_jobMgr->notifier())); m_popup->registerSender((NotificationSender*)(m_versionChecker->notifier())); m_popup->registerSender((NotificationSender*)(m_cpyDlg->notifier())); - m_popup->registerSender((NotificationSender*)(m_bookmarksDb->notifier())); + m_popup->registerSender((NotificationSender*)(m_bookmarksDb.notifier())); connect(ui->sideDock, &QDockWidget::dockLocationChanged, m_popup, @@ -391,7 +399,7 @@ MainWindow::setupSurahsDock() { for (int i = 1; i < 115; i++) { QString item = QString::number(i).rightJustified(3, '0') + ' ' + - m_quranDb->surahNames().at(i - 1); + m_quranDb.surahNames().at(i - 1); m_surahList.append(item); } @@ -399,10 +407,10 @@ MainWindow::setupSurahsDock() ui->listViewSurahs->setModel(&m_surahListModel); QItemSelectionModel* selector = ui->listViewSurahs->selectionModel(); - selector->select(m_surahListModel.index(m_currVerse->surah() - 1), + selector->select(m_surahListModel.index(m_currVerse.surah() - 1), QItemSelectionModel::Rows | QItemSelectionModel::Select); - ui->listViewSurahs->scrollTo(m_surahListModel.index(m_currVerse->surah() - 1), + ui->listViewSurahs->scrollTo(m_surahListModel.index(m_currVerse.surah() - 1), QAbstractItemView::PositionAtCenter); } @@ -421,7 +429,8 @@ MainWindow::setupMenubarButton() .arg(QString::number(ui->menubar->height()), QString::number(ui->menubar->height() * 2))); ui->menubar->setCornerWidget(toggleNav); - toggleNav->setIcon(StyleManager::awesome->icon(fa_solid, fa_compass)); + toggleNav->setIcon( + StyleManager::getInstance().awesome().icon(fa_solid, fa_compass)); toggleNav->setIconSize(QSize(20, 20)); connect(toggleNav, &QPushButton::toggled, this, [this](bool checked) { @@ -443,7 +452,7 @@ MainWindow::syncSelectedSurah() { QItemSelectionModel* select = ui->listViewSurahs->selectionModel(); select->clearSelection(); - QModelIndex surah = m_surahListModel.index(m_currVerse->surah() - 1); + QModelIndex surah = m_surahListModel.index(m_currVerse.surah() - 1); select->select(surah, QItemSelectionModel::Rows | QItemSelectionModel::Select); ui->listViewSurahs->scrollTo(surah, QAbstractItemView::PositionAtCenter); @@ -454,9 +463,9 @@ MainWindow::syncSelectedSurah() void MainWindow::currentVerseChanged() { - setCmbVerseIdx(m_currVerse->number() - 1); - setCmbPageIdx(m_currVerse->page() - 1); - setCmbJuzIdx(m_quranDb->getJuzOfPage(m_currVerse->page()) - 1); + setCmbVerseIdx(m_currVerse.number() - 1); + setCmbPageIdx(m_currVerse.page() - 1); + setCmbJuzIdx(m_quranDb.getJuzOfPage(m_currVerse.page()) - 1); } void @@ -497,17 +506,17 @@ MainWindow::setCmbJuzIdx(int idx) void MainWindow::setVerseComboBoxRange(bool forceUpdate) { - m_verseValidator->setTop(m_currVerse->surahCount()); + m_verseValidator->setTop(m_currVerse.surahCount()); // updates values in the combobox with the current surah verses m_internalVerseChange = true; ui->cmbVerse->clear(); - for (int i = 1; i <= m_currVerse->surahCount(); i++) + for (int i = 1; i <= m_currVerse.surahCount(); i++) ui->cmbVerse->addItem(QString::number(i), i); m_internalVerseChange = false; ui->cmbVerse->setValidator(m_verseValidator); - setCmbVerseIdx(m_currVerse->number() - 1); + setCmbVerseIdx(m_currVerse.number() - 1); } void @@ -517,7 +526,7 @@ MainWindow::searchSurahTextChanged(const QString& arg1) m_surahListModel.setStringList(m_surahList); syncSelectedSurah(); } else { - QList suggestions = m_quranDb->searchSurahNames(arg1); + QList suggestions = m_quranDb.searchSurahNames(arg1); QStringList res; foreach (int sura, suggestions) res.append(m_surahList.at(sura - 1)); @@ -556,17 +565,17 @@ MainWindow::cmbVerseChanged(int newVerseIdx) } int verse = newVerseIdx + 1; - int page = m_quranDb->getVersePage(m_currVerse->surah(), verse); + int page = m_quranDb.getVersePage(m_currVerse.surah(), verse); - if (page != m_currVerse->page()) + if (page != m_currVerse.page()) m_reader->gotoPage(page, false); - m_currVerse->setPage(page); - m_currVerse->setNumber(verse); + m_currVerse.setPage(page); + m_currVerse.setNumber(verse); m_reader->highlightCurrentVerse(); setCmbPageIdx(page - 1); - setCmbJuzIdx(m_quranDb->getJuzOfPage(page) - 1); + setCmbJuzIdx(m_quranDb.getJuzOfPage(page) - 1); // open newly set verse recitation file m_player->loadActiveVerse(); @@ -579,7 +588,7 @@ MainWindow::cmbJuzChanged(int newJuzIdx) qDebug() << "Internal jozz change"; return; } - int page = m_quranDb->getJuzStartPage(newJuzIdx + 1); + int page = m_quranDb.getJuzStartPage(newJuzIdx + 1); m_reader->gotoPage(page); } @@ -596,7 +605,7 @@ MainWindow::updateTrayTooltip(QMediaPlayer::PlaybackState state) if (state == QMediaPlayer::PlayingState) { m_systemTray->setTooltip( tr("Now playing: ") + m_player->reciterName() + " - " + tr("Surah ") + - m_quranDb->surahNames().at(m_currVerse->surah() - 1)); + m_quranDb.surahNames().at(m_currVerse.surah() - 1)); } else m_systemTray->setTooltip(tr("Quran Companion")); } @@ -604,8 +613,8 @@ MainWindow::updateTrayTooltip(QMediaPlayer::PlaybackState state) void MainWindow::bookmarkCurrent() { - if (!m_bookmarksDb->isBookmarked(*m_currVerse)) - m_bookmarksDb->addBookmark(*m_currVerse, false); + if (!m_bookmarksDb.isBookmarked(m_currVerse)) + m_bookmarksDb.addBookmark(m_currVerse, false); } void @@ -659,7 +668,7 @@ MainWindow::missingTranslation(int idx) void MainWindow::missingRecitationFileWarn(int reciterIdx, int surah) { - if (!m_settings->value("MissingFileWarning").toBool()) + if (!m_config.settings().value("MissingFileWarning").toBool()) return; QMessageBox::StandardButton btn = @@ -715,7 +724,7 @@ MainWindow::actionKhatmahTriggered() &QuranReader::navigateToVerse); } - m_bookmarksDb->saveActiveKhatmah(*m_currVerse); + m_bookmarksDb.saveActiveKhatmah(m_currVerse); m_khatmahDlg->show(); } @@ -729,7 +738,7 @@ MainWindow::actionAdvancedCopyTriggered() void MainWindow::actionTafsirTriggered() { - m_contentDlg->showVerseTafsir(*m_currVerse); + m_contentDlg->showVerseTafsir(m_currVerse); } void @@ -746,7 +755,7 @@ MainWindow::actionAboutTriggered() } void -MainWindow::on_actionAboutQt_triggered() +MainWindow::actionAboutQttriggered() { QMessageBox::aboutQt(this, tr("About Qt")); } @@ -765,6 +774,14 @@ MainWindow::actionSearchTriggered() m_searchDlg->show(); } +void +MainWindow::actionPlayerControlsToggled(bool checked) +{ + m_playerControls->setVisible(checked); + if (m_config.settings().value("Reader/AdaptiveFont").toBool()) + m_reader->redrawQuranPage(); +} + void MainWindow::navigateToSurah(QModelIndex& index) { @@ -775,11 +792,11 @@ MainWindow::navigateToSurah(QModelIndex& index) void MainWindow::nextVerse() { - if (m_currVerse->surah() == 114 && m_currVerse->number() == 6) + if (m_currVerse.surah() == 114 && m_currVerse.number() == 6) return; bool keepPlaying = m_player->isOn(); - m_reader->navigateToVerse(m_currVerse->next()); + m_reader->navigateToVerse(m_currVerse.next()); if (keepPlaying) m_player->play(); } @@ -787,11 +804,11 @@ MainWindow::nextVerse() void MainWindow::prevVerse() { - if (m_currVerse->surah() == 1 && m_currVerse->number() == 1) + if (m_currVerse.surah() == 1 && m_currVerse.number() == 1) return; bool keepPlaying = m_player->isOn(); - m_reader->navigateToVerse(m_currVerse->prev()); + m_reader->navigateToVerse(m_currVerse.prev()); if (keepPlaying) m_player->play(); } @@ -813,15 +830,15 @@ MainWindow::prevJuz() void MainWindow::nextSurah() { - if (m_currVerse->surah() < 114) - m_reader->gotoSurah(m_currVerse->surah() + 1); + if (m_currVerse.surah() < 114) + m_reader->gotoSurah(m_currVerse.surah() + 1); } void MainWindow::prevSurah() { - if (m_currVerse->surah() > 1) - m_reader->gotoSurah(m_currVerse->surah() - 1); + if (m_currVerse.surah() > 1) + m_reader->gotoSurah(m_currVerse.surah() - 1); } void @@ -867,11 +884,11 @@ MainWindow::resizeEvent(QResizeEvent* event) void MainWindow::saveReaderState() { - m_settings->setValue("WindowState", saveState()); - m_settings->setValue("Reciter", m_playerControls->currentReciter()); - m_settings->sync(); + m_config.settings().setValue("WindowState", saveState()); + m_config.settings().setValue("Reciter", m_playerControls->currentReciter()); + m_config.settings().sync(); - m_bookmarksDb->saveActiveKhatmah(*m_currVerse); + m_bookmarksDb.saveActiveKhatmah(m_currVerse); } void diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index da839118..0a9b53c7 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -189,6 +189,13 @@ private slots: * @brief open the SearchDialog, create instance if not set */ void actionSearchTriggered(); + /** + * @brief actionPlayerControlsToggled + * @param checked + * + * MODIFIED + */ + void actionPlayerControlsToggled(bool checked); /** * @brief open the about messagebox for the application */ @@ -196,7 +203,7 @@ private slots: /** * @brief open the about messagebox for Qt */ - void on_actionAboutQt_triggered(); + void actionAboutQttriggered(); /** * @brief move to the next ::Verse relative to m_currVerse */ @@ -251,15 +258,14 @@ private slots: private: Ui::MainWindow* ui; - QSharedPointer m_currVerse = Verse::current(); - QSharedPointer m_quranDb = QuranDb::current(); - QSharedPointer m_bookmarksDb = BookmarksDb::current(); - QSharedPointer m_translationDb = TranslationDb::current(); - QSharedPointer m_shortcutHandler = - ShortcutHandler::current(); - QSharedPointer m_settings = Settings::settings; - const QList>& m_reciters = Reciter::reciters; - const QList>& m_tafasir = Tafsir::tafasir; + Verse& m_currVerse; + Configuration& m_config; + ShortcutHandler& m_shortcutHandler; + BookmarksDb& m_bookmarksDb; + const QuranDb& m_quranDb; + const TranslationDb& m_translationDb; + const QList& m_reciters; + const QList& m_tafasir; /** * @brief initalizes different parts used by the app */ diff --git a/src/core/mainwindow.ui b/src/core/mainwindow.ui index b46c414b..e8660c46 100644 --- a/src/core/mainwindow.ui +++ b/src/core/mainwindow.ui @@ -70,7 +70,7 @@ 933 - + 0 @@ -114,6 +114,7 @@ + @@ -530,6 +531,17 @@ Export + + + true + + + true + + + Player Controls + + diff --git a/src/core/playercontrols.cpp b/src/core/playercontrols.cpp index 467fae12..563ee821 100644 --- a/src/core/playercontrols.cpp +++ b/src/core/playercontrols.cpp @@ -12,45 +12,46 @@ PlayerControls::PlayerControls(QWidget* parent, , ui(new Ui::PlayerControls) , m_player(player) , m_reader(reader) + , m_currVerse(Verse::getCurrent()) + , m_config(Configuration::getInstance()) + , m_reciters(Reciter::reciters) { ui->setupUi(this); loadIcons(); setupConnections(); - foreach (const QSharedPointer& r, m_reciters) - ui->cmbReciter->addItem(r->displayName()); + foreach (const Reciter& r, m_reciters) + ui->cmbReciter->addItem(r.displayName()); - ui->cmbReciter->setCurrentIndex(m_settings->value("Reciter", 0).toInt()); + ui->cmbReciter->setCurrentIndex( + m_config.settings().value("Reciter", 0).toInt()); } void PlayerControls::loadIcons() { - ui->btnPlay->setIcon(StyleManager::awesome->icon(fa_solid, fa_play)); - ui->btnPause->setIcon(StyleManager::awesome->icon(fa_solid, fa_pause)); - ui->btnStop->setIcon(StyleManager::awesome->icon(fa_solid, fa_stop)); + fa::QtAwesome& awesome = StyleManager::getInstance().awesome(); + ui->btnPlay->setIcon(awesome.icon(fa_solid, fa_play)); + ui->btnPause->setIcon(awesome.icon(fa_solid, fa_pause)); + ui->btnStop->setIcon(awesome.icon(fa_solid, fa_stop)); ui->lbSpeaker->setText(QString(fa_volume_high)); - ui->lbSpeaker->setFont(StyleManager::awesome->font(fa_solid, 16)); + ui->lbSpeaker->setFont(awesome.font(fa_solid, 16)); } void PlayerControls::setupConnections() { - QSharedPointer handler = ShortcutHandler::current(); - connect(handler.data(), - &ShortcutHandler::togglePlayerControls, - this, - &PlayerControls::toggleVisibility); - connect(handler.data(), + const ShortcutHandler& handler = ShortcutHandler::getInstance(); + connect(&handler, &ShortcutHandler::togglePlayback, this, &PlayerControls::togglePlayback); - connect(handler.data(), + connect(&handler, &ShortcutHandler::incrementVolume, this, &PlayerControls::incrementVolume); - connect(handler.data(), + connect(&handler, &ShortcutHandler::decrementVolume, this, &PlayerControls::decrementVolume); @@ -161,12 +162,6 @@ PlayerControls::volumeSliderValueChanged(int position) } } -void -PlayerControls::toggleVisibility() -{ - setVisible(!isVisible()); -} - void PlayerControls::incrementVolume() { diff --git a/src/core/playercontrols.h b/src/core/playercontrols.h index 0c584feb..1f8d4895 100644 --- a/src/core/playercontrols.h +++ b/src/core/playercontrols.h @@ -63,10 +63,6 @@ private slots: * @param position - position in the slider (0 - 100) */ void volumeSliderValueChanged(int position); - /** - * @brief toggle the visibility of the player controls in the main window - */ - void toggleVisibility(); /** * @brief utility to increment the VersePlayer playback volume by steps of 5 */ @@ -78,9 +74,9 @@ private slots: private: Ui::PlayerControls* ui; - QSharedPointer m_currVerse = Verse::current(); - QSharedPointer const m_settings = Settings::settings; - const QList>& m_reciters = Reciter::reciters; + const Verse& m_currVerse; + Configuration& m_config; + const QList& m_reciters; /** * @brief load icons for different UI elements */ @@ -96,8 +92,8 @@ private slots: /** * @brief pointer to VersePlayer instance */ - QPointer m_player = nullptr; - QPointer m_reader = nullptr; + QPointer m_player; + QPointer m_reader; }; #endif // PLAYERCONTROLS_H diff --git a/src/core/quranreader.cpp b/src/core/quranreader.cpp index 3ca6ac50..7ec1dd34 100644 --- a/src/core/quranreader.cpp +++ b/src/core/quranreader.cpp @@ -11,6 +11,14 @@ QuranReader::QuranReader(QWidget* parent, VersePlayer* player) : QWidget(parent) , ui(new Ui::QuranReader) , m_player(player) + , m_currVerse(Verse::getCurrent()) + , m_config(Configuration::getInstance()) + , m_tafsirDb(TafsirDb::getInstance()) + , m_translationDb(TranslationDb::getInstance()) + , m_bookmarksDb(BookmarksDb::getInstance()) + , m_quranDb(QuranDb::getInstance()) + , m_glyphsDb(GlyphsDb::getInstance()) + , m_tafasir(Tafsir::tafasir) { ui->setupUi(this); setLayoutDirection(Qt::LeftToRight); @@ -20,7 +28,7 @@ QuranReader::QuranReader(QWidget* parent, VersePlayer* player) updateSideFont(); updateVerseType(); redrawQuranPage(true); - if (m_readerMode == ReaderMode::SinglePage) + if (m_config.readerMode() == ReaderMode::SinglePage) addSideContent(); setupConnections(); @@ -29,16 +37,18 @@ QuranReader::QuranReader(QWidget* parent, VersePlayer* player) void QuranReader::loadIcons() { - ui->btnNext->setIcon(StyleManager::awesome->icon(fa_solid, fa_arrow_left)); - ui->btnPrev->setIcon(StyleManager::awesome->icon(fa_solid, fa_arrow_right)); + ui->btnNext->setIcon( + StyleManager::getInstance().awesome().icon(fa_solid, fa_arrow_left)); + ui->btnPrev->setIcon( + StyleManager::getInstance().awesome().icon(fa_solid, fa_arrow_right)); } void QuranReader::loadReader() { - if (m_readerMode == ReaderMode::SinglePage) { + if (m_config.readerMode() == ReaderMode::SinglePage) { m_activeQuranBrowser = m_quranBrowsers[0] = - new QuranPageBrowser(ui->frmPageContent, m_currVerse->page()); + new QuranPageBrowser(ui->frmPageContent, m_currVerse.page()); QWidget* scrollWidget = new QWidget(); scrollWidget->setObjectName("scrollWidget"); @@ -63,17 +73,17 @@ QuranReader::loadReader() else { // even Quran pages are always on the left side - if (m_currVerse->page() % 2 == 0) { + if (m_currVerse.page() % 2 == 0) { m_quranBrowsers[0] = - new QuranPageBrowser(ui->frmPageContent, m_currVerse->page() - 1); + new QuranPageBrowser(ui->frmPageContent, m_currVerse.page() - 1); m_activeQuranBrowser = m_quranBrowsers[1] = - new QuranPageBrowser(ui->frmSidePanel, m_currVerse->page()); + new QuranPageBrowser(ui->frmSidePanel, m_currVerse.page()); } else { m_activeQuranBrowser = m_quranBrowsers[0] = - new QuranPageBrowser(ui->frmPageContent, m_currVerse->page()); + new QuranPageBrowser(ui->frmPageContent, m_currVerse.page()); m_quranBrowsers[1] = - new QuranPageBrowser(ui->frmSidePanel, m_currVerse->page() + 1); + new QuranPageBrowser(ui->frmSidePanel, m_currVerse.page() + 1); } ui->frmSidePanel->layout()->addWidget(m_quranBrowsers[1]); @@ -108,27 +118,23 @@ QuranReader::setupConnections() m_player, &VersePlayer::loadActiveVerse); - QSharedPointer handler = ShortcutHandler::current(); - connect(handler.data(), - &ShortcutHandler::nextPage, - this, - &QuranReader::btnNextClicked); - connect(handler.data(), - &ShortcutHandler::prevPage, - this, - &QuranReader::btnPrevClicked); - connect(handler.data(), + const ShortcutHandler& handler = ShortcutHandler::getInstance(); + connect( + &handler, &ShortcutHandler::nextPage, this, &QuranReader::btnNextClicked); + connect( + &handler, &ShortcutHandler::prevPage, this, &QuranReader::btnPrevClicked); + connect(&handler, &ShortcutHandler::toggleReaderView, this, &QuranReader::toggleReaderView); for (int i = 0; i <= 1; i++) { if (m_quranBrowsers[i]) { - connect(handler.data(), + connect(&handler, &ShortcutHandler::zoomIn, m_quranBrowsers[i], &QuranPageBrowser::actionZoomIn); - connect(handler.data(), + connect(&handler, &ShortcutHandler::zoomOut, m_quranBrowsers[i], &QuranPageBrowser::actionZoomOut); @@ -152,17 +158,19 @@ void QuranReader::updateSideFont() { m_sideFont = - qvariant_cast(m_settings->value("Reader/SideContentFont")); + qvariant_cast(m_config.settings().value("Reader/SideContentFont")); } void QuranReader::updateVerseType() { VerseType type = - qvariant_cast(m_settings->value("Reader/VerseType")); - m_versesFont.setFamily(FontManager::verseFontname(type, m_currVerse->page())); - m_versesFont.setPointSize(m_settings->value("Reader/VerseFontSize").toInt()); - m_quranDb->setVerseType(type); + qvariant_cast(m_config.settings().value("Reader/VerseType")); + m_versesFont.setFamily( + FontManager::getInstance().verseFontname(type, m_currVerse.page())); + m_versesFont.setPointSize( + m_config.settings().value("Reader/VerseFontSize").toInt()); + m_quranDb.setVerseType(type); } void @@ -187,12 +195,13 @@ void QuranReader::redrawQuranPage(bool manualSz) { if (m_activeQuranBrowser == m_quranBrowsers[0]) { - m_quranBrowsers[0]->constructPage(m_currVerse->page(), manualSz); - if (m_readerMode == Settings::DoublePage && m_quranBrowsers[1]) - m_quranBrowsers[1]->constructPage(m_currVerse->page() + 1, manualSz); + m_quranBrowsers[0]->constructPage(m_currVerse.page(), manualSz); + if (m_config.readerMode() == Configuration::DoublePage && + m_quranBrowsers[1]) + m_quranBrowsers[1]->constructPage(m_currVerse.page() + 1, manualSz); } else { - m_quranBrowsers[0]->constructPage(m_currVerse->page() - 1, manualSz); - m_quranBrowsers[1]->constructPage(m_currVerse->page(), manualSz); + m_quranBrowsers[0]->constructPage(m_currVerse.page() - 1, manualSz); + m_quranBrowsers[1]->constructPage(m_currVerse.page(), manualSz); } updatePageVerseInfoList(); @@ -201,7 +210,7 @@ QuranReader::redrawQuranPage(bool manualSz) void QuranReader::addSideContent() { - if (m_readerMode != ReaderMode::SinglePage) + if (m_config.readerMode() != ReaderMode::SinglePage) return; if (!m_verseFrameList.isEmpty()) { @@ -214,20 +223,21 @@ QuranReader::addSideContent() QLabel* contentLb; VerseFrame* verseContFrame; QString prevLbContent, currLbContent, glyphs; - if (m_quranDb->verseType() == Settings::Qcf) - m_versesFont.setFamily(FontManager::pageFontname(m_currVerse->page())); + if (m_quranDb.verseType() == Configuration::Qcf) + m_versesFont.setFamily( + FontManager::getInstance().pageFontname(m_currVerse.page())); - m_translationDb->setCurrentTranslation( - m_settings->value("Reader/Translation").toInt()); + m_translationDb.setCurrentTranslation( + m_config.settings().value("Reader/Translation").toInt()); for (int i = m_activeVList->size() - 1; i >= 0; i--) { const Verse* verse = &(m_activeVList->at(i)); verseContFrame = new VerseFrame(m_scrlVerseByVerse->widget()); verselb = new ClickableLabel(verseContFrame); contentLb = new QLabel(verseContFrame); - glyphs = m_quranDb->verseType() == Settings::Qcf - ? m_glyphsDb->getVerseGlyphs(verse->surah(), verse->number()) - : m_quranDb->verseText(verse->surah(), verse->number()); + glyphs = m_quranDb.verseType() == Configuration::Qcf + ? m_glyphsDb.getVerseGlyphs(verse->surah(), verse->number()) + : m_quranDb.verseText(verse->surah(), verse->number()); verseContFrame->setObjectName( QString("%0_%1").arg(verse->surah()).arg(verse->number())); @@ -238,7 +248,7 @@ QuranReader::addSideContent() verselb->setWordWrap(true); currLbContent = - m_translationDb->getTranslation(verse->surah(), verse->number()); + m_translationDb.getTranslation(verse->surah(), verse->number()); if (currLbContent == prevLbContent) { currLbContent = '-'; @@ -270,17 +280,17 @@ QuranReader::addSideContent() void QuranReader::highlightCurrentVerse() { - if (m_currVerse->number() == 0) + if (m_currVerse.number() == 0) return; // idx may be -1 if verse number is 0 (basmallah) - int idx = m_activeVList->indexOf(*m_currVerse); + int idx = m_activeVList->indexOf(m_currVerse); if (idx < 0) idx = 0; m_activeQuranBrowser->highlightVerse(idx); - if (m_readerMode == ReaderMode::SinglePage) + if (m_config.readerMode() == ReaderMode::SinglePage) setHighlightedFrame(); } @@ -291,8 +301,8 @@ QuranReader::setHighlightedFrame() m_highlightedFrm->setSelected(false); VerseFrame* verseFrame = m_scrlVerseByVerse->widget()->findChild( - QString("%0_%1").arg(QString::number(m_currVerse->surah()), - QString::number(m_currVerse->number()))); + QString("%0_%1").arg(QString::number(m_currVerse.surah()), + QString::number(m_currVerse.number()))); verseFrame->setSelected(true); @@ -305,7 +315,7 @@ QuranReader::navigateToVerse(const Verse& v) { gotoPage(v.page(), false); - m_currVerse->update(v); + m_currVerse.update(v); emit currentSurahChanged(); emit currentVerseChanged(); highlightCurrentVerse(); @@ -315,19 +325,17 @@ void QuranReader::updatePageVerseInfoList() { if (m_activeQuranBrowser == m_quranBrowsers[0]) { - m_vLists[0] = - Verse::fromList(m_quranDb->verseInfoList(m_currVerse->page())); - if (m_readerMode == Settings::DoublePage) + m_vLists[0] = Verse::fromList(m_quranDb.verseInfoList(m_currVerse.page())); + if (m_config.readerMode() == Configuration::DoublePage) m_vLists[1] = - Verse::fromList(m_quranDb->verseInfoList(m_currVerse->page() + 1)); + Verse::fromList(m_quranDb.verseInfoList(m_currVerse.page() + 1)); m_activeVList = &m_vLists[0]; } else { m_vLists[0] = - Verse::fromList(m_quranDb->verseInfoList(m_currVerse->page() - 1)); - m_vLists[1] = - Verse::fromList(m_quranDb->verseInfoList(m_currVerse->page())); + Verse::fromList(m_quranDb.verseInfoList(m_currVerse.page() - 1)); + m_vLists[1] = Verse::fromList(m_quranDb.verseInfoList(m_currVerse.page())); m_activeVList = &m_vLists[1]; } @@ -336,7 +344,8 @@ QuranReader::updatePageVerseInfoList() void QuranReader::btnNextClicked() { - if (m_readerMode == ReaderMode::SinglePage || m_currVerse->page() % 2 == 0) + if (m_config.readerMode() == ReaderMode::SinglePage || + m_currVerse.page() % 2 == 0) nextPage(1); else nextPage(2); @@ -345,7 +354,8 @@ QuranReader::btnNextClicked() void QuranReader::btnPrevClicked() { - if (m_readerMode == ReaderMode::SinglePage || m_currVerse->page() % 2 != 0) + if (m_config.readerMode() == ReaderMode::SinglePage || + m_currVerse.page() % 2 != 0) prevPage(1); else prevPage(2); @@ -380,8 +390,8 @@ QuranReader::selectVerse(int browserIdx, int IdxInPage) switchActivePage(); const Verse& v = m_vLists[browserIdx].at(IdxInPage); - int currSurah = m_currVerse->surah(); - m_currVerse->update(v); + int currSurah = m_currVerse.surah(); + m_currVerse.update(v); emit currentVerseChanged(); if (currSurah != v.surah()) @@ -392,7 +402,7 @@ void QuranReader::setVerseToStartOfPage() { // set the current verse to the verse at the top of the page - m_currVerse->update(m_activeVList->at(0)); + m_currVerse.update(m_activeVList->at(0)); } void @@ -411,7 +421,7 @@ QuranReader::verseAnchorClicked(const QUrl& hrefUrl) Verse v(m_vLists[browerIdx].at(idx)); QuranPageBrowser::Action chosenAction = - senderBrowser->lmbVerseMenu(m_bookmarksDb->isBookmarked(v)); + senderBrowser->lmbVerseMenu(m_bookmarksDb.isBookmarked(v)); switch (chosenAction) { case QuranPageBrowser::Play: @@ -437,10 +447,10 @@ QuranReader::verseAnchorClicked(const QUrl& hrefUrl) emit copyVerseText(v); break; case QuranPageBrowser::AddBookmark: - m_bookmarksDb->addBookmark(v, false); + m_bookmarksDb.addBookmark(v, false); break; case QuranPageBrowser::RemoveBookmark: - m_bookmarksDb->removeBookmark(v, false); + m_bookmarksDb.removeBookmark(v, false); default: break; } @@ -455,11 +465,11 @@ QuranReader::verseClicked() int surah = data.at(0).toInt(); int verse = data.at(1).toInt(); - m_currVerse->setNumber(verse); + m_currVerse.setNumber(verse); emit currentVerseChanged(); - if (m_currVerse->surah() != surah) { - m_currVerse->setSurah(surah); + if (m_currVerse.surah() != surah) { + m_currVerse.setSurah(surah); emit currentSurahChanged(); } @@ -477,7 +487,7 @@ QuranReader::gotoPage(int page, bool updateElements, bool automaticFlip) m_player->stop(); if (m_activeQuranBrowser->page() != page) { - if (m_readerMode == ReaderMode::SinglePage) + if (m_config.readerMode() == ReaderMode::SinglePage) gotoSinglePage(page); else gotoDoublePage(page); @@ -493,18 +503,18 @@ QuranReader::gotoPage(int page, bool updateElements, bool automaticFlip) void QuranReader::gotoSinglePage(int page) { - m_currVerse->setPage(page); + m_currVerse.setPage(page); redrawQuranPage(); - m_currVerse->update(m_activeVList->at(0)); + m_currVerse.update(m_activeVList->at(0)); addSideContent(); } void QuranReader::gotoDoublePage(int page) { - int currPage = m_currVerse->page(); - m_currVerse->setPage(page); + int currPage = m_currVerse.page(); + m_currVerse.setPage(page); int pageBrowerIdx = page % 2 == 0; @@ -520,10 +530,10 @@ void QuranReader::nextPage(int step) { bool keepPlaying = m_player->playbackState() == QMediaPlayer::PlayingState; - if (m_currVerse->page() + step <= 604) { - gotoPage(m_currVerse->page() + step, true, true); + if (m_currVerse.page() + step <= 604) { + gotoPage(m_currVerse.page() + step, true, true); - if (m_readerMode == ReaderMode::SinglePage) + if (m_config.readerMode() == ReaderMode::SinglePage) m_scrlVerseByVerse->verticalScrollBar()->setValue(0); // if the page is flipped automatically, resume playback @@ -538,10 +548,10 @@ void QuranReader::prevPage(int step) { bool keepPlaying = m_player->playbackState() == QMediaPlayer::PlayingState; - if (m_currVerse->page() - step >= 1) { - gotoPage(m_currVerse->page() - step, true, true); + if (m_currVerse.page() - step >= 1) { + gotoPage(m_currVerse.page() - step, true, true); - if (m_readerMode == ReaderMode::SinglePage) + if (m_config.readerMode() == ReaderMode::SinglePage) m_scrlVerseByVerse->verticalScrollBar()->setValue(0); if (keepPlaying) { @@ -555,12 +565,12 @@ void QuranReader::gotoSurah(int surahIdx) { // getting surah index - int page = m_quranDb->surahStartPage(surahIdx); + int page = m_quranDb.surahStartPage(surahIdx); gotoPage(page, false); - m_currVerse->setPage(page); - m_currVerse->setSurah(surahIdx); - m_currVerse->setNumber((surahIdx == 9 || surahIdx == 1) ? 1 : 0); + m_currVerse.setPage(page); + m_currVerse.setSurah(surahIdx); + m_currVerse.setNumber((surahIdx == 9 || surahIdx == 1) ? 1 : 0); // syncing the player & playing basmalah m_player->playCurrentVerse(); diff --git a/src/core/quranreader.h b/src/core/quranreader.h index b7fdbc9c..5d34b8ce 100644 --- a/src/core/quranreader.h +++ b/src/core/quranreader.h @@ -11,7 +11,7 @@ #include #include #include -typedef Settings::ReaderMode ReaderMode; +typedef Configuration::ReaderMode ReaderMode; namespace Ui { class QuranReader; @@ -141,16 +141,14 @@ private slots: private: Ui::QuranReader* ui; - QSharedPointer m_currVerse = Verse::current(); - QSharedPointer m_quranDb = QuranDb::current(); - QSharedPointer m_glyphsDb = GlyphsDb::current(); - QSharedPointer m_bookmarksDb = BookmarksDb::current(); - QSharedPointer m_tafsirDb = TafsirDb::current(); - QSharedPointer m_translationDb = TranslationDb::current(); - QSharedPointer m_settings = Settings::settings; - const QList>& m_recitersList = Reciter::reciters; - const QList>& m_tafasir = Tafsir::tafasir; - const ReaderMode& m_readerMode = Settings::readerMode; + Verse& m_currVerse; + Configuration& m_config; + TafsirDb& m_tafsirDb; + TranslationDb& m_translationDb; + BookmarksDb& m_bookmarksDb; + QuranDb& m_quranDb; + const GlyphsDb& m_glyphsDb; + const QList& m_tafasir; void setupConnections(); /** * @brief load icons for different UI elements diff --git a/src/database/betaqatdb.cpp b/src/database/betaqatdb.cpp index 31cce9bc..b465435f 100644 --- a/src/database/betaqatdb.cpp +++ b/src/database/betaqatdb.cpp @@ -1,16 +1,17 @@ #include "betaqatdb.h" #include -QSharedPointer -BetaqatDb::current() +BetaqatDb& +BetaqatDb::getInstance() { - static QSharedPointer betaqatDb = - QSharedPointer::create(); - return betaqatDb; + static BetaqatDb bdb; + return bdb; } BetaqatDb::BetaqatDb() : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "BetaqatCon")) + , m_assetsDir(DirManager::getInstance().assetsDir()) + , m_config(Configuration::getInstance()) { BetaqatDb::open(); } @@ -18,7 +19,7 @@ BetaqatDb::BetaqatDb() void BetaqatDb::open() { - setDatabaseName(m_assetsDir->absoluteFilePath("betaqat.db")); + setDatabaseName(m_assetsDir.absoluteFilePath("betaqat.db")); if (!QSqlDatabase::open()) qFatal("Error opening betaqat db"); } @@ -34,7 +35,7 @@ BetaqatDb::getBetaqa(const int surah) { QSqlQuery dbQuery(*this); - if (m_languageCode == QLocale::Arabic) + if (m_config.language() == QLocale::Arabic) dbQuery.prepare("SELECT text FROM content WHERE sura=:i"); else dbQuery.prepare("SELECT text_en FROM content WHERE sura=:i"); diff --git a/src/database/betaqatdb.h b/src/database/betaqatdb.h index 19ec1063..6ec78cfb 100644 --- a/src/database/betaqatdb.h +++ b/src/database/betaqatdb.h @@ -4,16 +4,15 @@ #include #include #include +#include #include -#include class BetaqatDb : public DbConnection , QSqlDatabase { public: - static QSharedPointer current(); - BetaqatDb(); + static BetaqatDb& getInstance(); void open(); Type type(); /** @@ -24,8 +23,9 @@ class BetaqatDb QString getBetaqa(const int surah); private: - const QLocale::Language m_languageCode = Settings::language; - const QSharedPointer m_assetsDir = DirManager::assetsDir; + BetaqatDb(); + const Configuration& m_config; + const QDir& m_assetsDir; }; #endif // BETAQATDB_H diff --git a/src/database/bookmarksdb.cpp b/src/database/bookmarksdb.cpp index b97ef4be..2d4a2644 100644 --- a/src/database/bookmarksdb.cpp +++ b/src/database/bookmarksdb.cpp @@ -2,17 +2,19 @@ #include #include -QSharedPointer -BookmarksDb::current() +BookmarksDb& +BookmarksDb::getInstance() { - static QSharedPointer bookmarkDb = - QSharedPointer::create(); + static BookmarksDb bookmarkDb; return bookmarkDb; } BookmarksDb::BookmarksDb() : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "BookmarksCon")) , m_notifier(this) + , m_config(Configuration::getInstance()) + , m_configDir(DirManager::getInstance().configDir()) + , m_quranDb(QuranDb::getInstance()) { BookmarksDb::open(); QSqlQuery dbQuery(*this); @@ -32,7 +34,7 @@ BookmarksDb::BookmarksDb() void BookmarksDb::open() { - setDatabaseName(m_configDir->absoluteFilePath("bookmarks.db")); + setDatabaseName(m_configDir.absoluteFilePath("bookmarks.db")); if (!QSqlDatabase::open()) qFatal("Error opening bookmarks db"); } diff --git a/src/database/bookmarksdb.h b/src/database/bookmarksdb.h index 3e3d7618..8344a418 100644 --- a/src/database/bookmarksdb.h +++ b/src/database/bookmarksdb.h @@ -9,16 +9,15 @@ #include #include #include +#include #include -#include class BookmarksDb : public DbConnection , QSqlDatabase { public: - static QSharedPointer current(); - BookmarksDb(); + static BookmarksDb& getInstance(); void open(); Type type(); /** @@ -115,9 +114,10 @@ class BookmarksDb const BookmarksNotifier* notifier() const; private: - const QSharedPointer m_quranDb = QuranDb::current(); - const QSharedPointer m_settings = Settings::settings; - const QSharedPointer m_configDir = DirManager::configDir; + BookmarksDb(); + const QuranDb& m_quranDb; + const Configuration& m_config; + const QDir& m_configDir; BookmarksNotifier m_notifier; /** * @brief integer id of the current active khatmah diff --git a/src/database/glyphsdb.cpp b/src/database/glyphsdb.cpp index 8fc6df28..88cdd6c5 100644 --- a/src/database/glyphsdb.cpp +++ b/src/database/glyphsdb.cpp @@ -1,15 +1,17 @@ #include "glyphsdb.h" #include -QSharedPointer -GlyphsDb::current() +GlyphsDb& +GlyphsDb::getInstance() { - static QSharedPointer glyphsDb = QSharedPointer::create(); - return glyphsDb; + static GlyphsDb gdb; + return gdb; } GlyphsDb::GlyphsDb() : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "GlyphsCon")) + , m_config(Configuration::getInstance()) + , m_assetsDir(DirManager::getInstance().assetsDir()) { GlyphsDb::open(); } @@ -17,7 +19,7 @@ GlyphsDb::GlyphsDb() void GlyphsDb::open() { - setDatabaseName(m_assetsDir->absoluteFilePath("glyphs.db")); + setDatabaseName(m_assetsDir.absoluteFilePath("glyphs.db")); if (!QSqlDatabase::open()) qFatal("Error opening glyphs db"); } @@ -29,12 +31,13 @@ GlyphsDb::type() } QStringList -GlyphsDb::getPageLines(const int page) +GlyphsDb::getPageLines(const int page) const { QSqlQuery dbQuery(*this); QString query = "SELECT %0 FROM pages WHERE page_no=%1"; - query = query.arg("qcf_v" + QString::number(m_qcfVer), QString::number(page)); + query = query.arg("qcf_v" + QString::number(m_config.qcfVersion()), + QString::number(page)); dbQuery.prepare(query); if (!dbQuery.exec()) @@ -47,7 +50,7 @@ GlyphsDb::getPageLines(const int page) } QString -GlyphsDb::getSurahNameGlyph(const int sura) +GlyphsDb::getSurahNameGlyph(const int sura) const { QSqlQuery dbQuery(*this); @@ -63,7 +66,7 @@ GlyphsDb::getSurahNameGlyph(const int sura) } QString -GlyphsDb::getJuzGlyph(const int juz) +GlyphsDb::getJuzGlyph(const int juz) const { QSqlQuery dbQuery(*this); @@ -79,12 +82,12 @@ GlyphsDb::getJuzGlyph(const int juz) } QString -GlyphsDb::getVerseGlyphs(const int sIdx, const int vIdx) +GlyphsDb::getVerseGlyphs(const int sIdx, const int vIdx) const { QSqlQuery dbQuery(*this); QString query = "SELECT %0 FROM ayah_glyphs WHERE surah=%1 AND ayah=%2"; - query = query.arg("qcf_v" + QString::number(m_qcfVer), + query = query.arg("qcf_v" + QString::number(m_config.qcfVersion()), QString::number(sIdx), QString::number(vIdx)); diff --git a/src/database/glyphsdb.h b/src/database/glyphsdb.h index 7697725b..d6885c55 100644 --- a/src/database/glyphsdb.h +++ b/src/database/glyphsdb.h @@ -5,16 +5,15 @@ #include #include #include +#include #include -#include class GlyphsDb : public DbConnection , QSqlDatabase { public: - static QSharedPointer current(); - GlyphsDb(); + static GlyphsDb& getInstance(); void open(); Type type(); /** @@ -22,31 +21,32 @@ class GlyphsDb * @param page - Quran page number * @return QList of page lines */ - QStringList getPageLines(const int page); + QStringList getPageLines(const int page) const; /** * @brief gets the surah name glyph for the QCF_BSML font, used to render * surah frame in Quran page * @param sura - sura number (1-114) * @return QString of glyphs */ - QString getSurahNameGlyph(const int sura); + QString getSurahNameGlyph(const int sura) const; /** * @brief gets the juz name in arabic, used in page header * @param juz - juz number * @return QString of the juz name */ - QString getJuzGlyph(const int juz); + QString getJuzGlyph(const int juz) const; /** * @brief gets the verse QCF glyphs for the corresponding QCF page font * @param sIdx - sura number (1-114) * @param vIdx - verse number * @return QString of verse glyphs */ - QString getVerseGlyphs(const int sIdx, const int vIdx); + QString getVerseGlyphs(const int sIdx, const int vIdx) const; private: - const int m_qcfVer = Settings::qcfVersion; - const QSharedPointer m_assetsDir = DirManager::assetsDir; + GlyphsDb(); + Configuration& m_config; + const QDir& m_assetsDir; }; #endif // GLYPHSDB_H diff --git a/src/database/qurandb.cpp b/src/database/qurandb.cpp index b71dfab3..f538467a 100644 --- a/src/database/qurandb.cpp +++ b/src/database/qurandb.cpp @@ -2,15 +2,17 @@ #include #include -QSharedPointer -QuranDb::current() +QuranDb& +QuranDb::getInstance() { - static QSharedPointer quranDb = QSharedPointer::create(); - return quranDb; + static QuranDb qdb; + return qdb; } QuranDb::QuranDb() : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "QuranCon")) + , m_assetsDir(DirManager::getInstance().assetsDir()) + , m_config(Configuration::getInstance()) { QuranDb::open(); for (int i = 1; i <= 114; i++) @@ -20,7 +22,7 @@ QuranDb::QuranDb() void QuranDb::open() { - setDatabaseName(m_assetsDir->absoluteFilePath("quran.db")); + setDatabaseName(m_assetsDir.absoluteFilePath("quran.db")); if (!QSqlDatabase::open()) qFatal("Error opening quran db"); } @@ -32,7 +34,7 @@ QuranDb::type() } QPair -QuranDb::pageMetadata(const int page) +QuranDb::pageMetadata(const int page) const { QSqlQuery dbQuery(*this); dbQuery.prepare( @@ -48,12 +50,12 @@ QuranDb::pageMetadata(const int page) } int -QuranDb::getVersePage(const int& surahIdx, const int& verse) +QuranDb::getVersePage(const int& surahIdx, const int& verse) const { QSqlQuery dbQuery(*this); QString query = "SELECT page FROM verses_v%0 WHERE sura_no=%1 AND aya_no=%2"; - dbQuery.prepare(query.arg(QString::number(m_qcfVer), + dbQuery.prepare(query.arg(QString::number(m_config.qcfVersion()), QString::number(surahIdx), QString::number(verse))); @@ -66,7 +68,7 @@ QuranDb::getVersePage(const int& surahIdx, const int& verse) } int -QuranDb::getJuzStartPage(const int juz) +QuranDb::getJuzStartPage(const int juz) const { QSqlQuery dbQuery(*this); @@ -84,7 +86,7 @@ QuranDb::getJuzStartPage(const int juz) } int -QuranDb::getJuzOfPage(const int page) +QuranDb::getJuzOfPage(const int page) const { QSqlQuery dbQuery(*this); @@ -101,14 +103,15 @@ QuranDb::getJuzOfPage(const int page) } QList> -QuranDb::verseInfoList(const int page) +QuranDb::verseInfoList(const int page) const { QList> viList; QSqlQuery dbQuery(*this); QString query = "SELECT sura_no,aya_no FROM verses_v%0 WHERE page=%1 ORDER BY id"; - dbQuery.prepare(query.arg(QString::number(m_qcfVer), QString::number(page))); + dbQuery.prepare( + query.arg(QString::number(m_config.qcfVersion()), QString::number(page))); if (!dbQuery.exec()) { qCritical() << "Error occurred during getVerseInfoList SQL statment exec"; @@ -123,7 +126,7 @@ QuranDb::verseInfoList(const int page) } QString -QuranDb::verseText(const int sIdx, const int vIdx) +QuranDb::verseText(const int sIdx, const int vIdx) const { QSqlQuery dbQuery(*this); if (m_verseType == VerseType::Annotated) @@ -145,7 +148,7 @@ QuranDb::verseText(const int sIdx, const int vIdx) } int -QuranDb::surahStartPage(int surahIdx) +QuranDb::surahStartPage(int surahIdx) const { QSqlQuery dbQuery(*this); @@ -161,11 +164,11 @@ QuranDb::surahStartPage(int surahIdx) } QString -QuranDb::surahName(const int sIdx, bool ar) +QuranDb::surahName(const int sIdx, bool ar) const { QSqlQuery dbQuery(*this); - if (m_languageCode == QLocale::Arabic || ar) + if (m_config.language() == QLocale::Arabic || ar) dbQuery.prepare("SELECT sura_name_ar FROM verses_v1 WHERE sura_no=:i"); else dbQuery.prepare("SELECT sura_name_en FROM verses_v1 WHERE sura_no=:i"); @@ -180,7 +183,7 @@ QuranDb::surahName(const int sIdx, bool ar) } QList -QuranDb::verseById(const int id) +QuranDb::verseById(const int id) const { QSqlQuery dbQuery(*this); dbQuery.prepare("SELECT page,sura_no,aya_no FROM verses_v1 WHERE id=:i"); @@ -197,12 +200,12 @@ QuranDb::verseById(const int id) } int -QuranDb::versePage(const int& surahIdx, const int& verse) +QuranDb::versePage(const int& surahIdx, const int& verse) const { QSqlQuery dbQuery(*this); QString query = "SELECT page FROM verses_v%0 WHERE sura_no=%1 AND aya_no=%2"; - dbQuery.prepare(query.arg(QString::number(m_qcfVer), + dbQuery.prepare(query.arg(QString::number(m_config.qcfVersion()), QString::number(surahIdx), QString::number(verse))); @@ -215,7 +218,7 @@ QuranDb::versePage(const int& surahIdx, const int& verse) } QList -QuranDb::searchSurahNames(QString text) +QuranDb::searchSurahNames(QString text) const { QList results; QSqlQuery dbQuery(*this); @@ -241,13 +244,13 @@ QuranDb::searchSurahNames(QString text) QList> QuranDb::searchSurahs(QString searchText, const QList surahs, - const bool whole) + const bool whole) const { QList> results; QSqlQuery dbQuery(*this); QString q = "SELECT page,sura_no,aya_no FROM verses_v" + - QString::number(m_qcfVer) + " WHERE ("; + QString::number(m_config.qcfVersion()) + " WHERE ("; for (int i = 0; i < surahs.size(); i++) { q.append("sura_no=" + QString::number(surahs.at(i)) + ' '); if (i != surahs.size() - 1) @@ -276,13 +279,15 @@ QuranDb::searchSurahs(QString searchText, } QList> -QuranDb::searchVerses(QString searchText, const int range[], const bool whole) +QuranDb::searchVerses(QString searchText, + const int range[], + const bool whole) const { QList> results; QSqlQuery dbQuery(*this); QString q = "SELECT page,sura_no,aya_no FROM verses_v" + - QString::number(m_qcfVer) + + QString::number(m_config.qcfVersion()) + " WHERE (page >= " + QString::number(range[0]) + " AND page <= " + QString::number(range[1]) + ")"; @@ -309,13 +314,13 @@ QuranDb::searchVerses(QString searchText, const int range[], const bool whole) } QList -QuranDb::randomVerse() +QuranDb::randomVerse() const { QSqlQuery dbQuery(*this); int id = QRandomGenerator::global()->bounded(1, 6237); dbQuery.prepare("SELECT page,sura_no,aya_no FROM verses_v" + - QString::number(m_qcfVer) + + QString::number(m_config.qcfVersion()) + " WHERE id=" + QString::number(id)); if (!dbQuery.exec()) { diff --git a/src/database/qurandb.h b/src/database/qurandb.h index 12dce5a5..ea9e6b65 100644 --- a/src/database/qurandb.h +++ b/src/database/qurandb.h @@ -5,17 +5,16 @@ #include #include #include +#include #include -#include -typedef Settings::VerseType VerseType; +typedef Configuration::VerseType VerseType; class QuranDb : public DbConnection , QSqlDatabase { public: - static QSharedPointer current(); - QuranDb(); + static QuranDb& getInstance(); void open(); Type type(); /** @@ -24,73 +23,73 @@ class QuranDb * @param page - Quran page number * @return QList of 2 integers [0: surah index, 1: juz number] */ - QPair pageMetadata(const int page); + QPair pageMetadata(const int page) const; /** * @brief gets the page where the verse is found * @param surahIdx - sura number * @param verse - verse number * @return page number */ - int getVersePage(const int& surahIdx, const int& verse); + int getVersePage(const int& surahIdx, const int& verse) const; /** * @brief gets the page where the corresponding juz starts * @param juz - juz number * @return page number */ - int getJuzStartPage(const int juz); + int getJuzStartPage(const int juz) const; /** * @brief get the juz which the passed page is a part of * @param page - page number * @return juz number */ - int getJuzOfPage(const int page); + int getJuzOfPage(const int page) const; /** * @brief gets a QList of ::Verse instances for the page verses * @param page - Quran page number * @return QList of ::Verse instances */ - QList> verseInfoList(const int page); + QList> verseInfoList(const int page) const; /** * @brief gets the verse text * @param sIdx - sura number (1-114) * @param vIdx - verse number * @return QString of the verse text */ - QString verseText(const int sIdx, const int vIdx); + QString verseText(const int sIdx, const int vIdx) const; /** * @brief gets the page where the surah begins * @param surahIdx - sura number * @return page of the first verse in the sura */ - int surahStartPage(int surahIdx); + int surahStartPage(int surahIdx) const; /** * @brief gets the surah name in English or Arabic (default is English) * @param sIdx - sura number * @param ar - boolean to return arabic sura name * @return QString containing the sura name */ - QString surahName(const int sIdx, bool ar = false); + QString surahName(const int sIdx, bool ar = false) const; /** * @brief get the verse with the corresponding id and return it as a ::Verse * instance * @param id - verse id * @return ::Verse instance */ - QList verseById(const int id); + QList verseById(const int id) const; /** * @brief gets the page where the verse is found * @param surahIdx - sura number * @param verse - verse number * @return page number */ - int versePage(const int& surahIdx, const int& verse); + int versePage(const int& surahIdx, const int& verse) const; /** * @brief searches the database for surahs matching the given text pattern, * the pattern can be either in English or Arabic * @param text - name / part of the name of the sura * @return QList of sura numbers which contain the given text */ - QList searchSurahNames(QString text); + QList searchSurahNames(QString text) const; /** * @brief search specific surahs for the given search text * @param searchText - text to search for @@ -100,7 +99,7 @@ class QuranDb */ QList> searchSurahs(QString searchText, const QList surahs, - const bool whole = false); + const bool whole = false) const; /** * @brief search a range of pages for the given search text * @param searchText - text to search for @@ -110,12 +109,12 @@ class QuranDb */ QList> searchVerses(QString searchText, const int range[2] = new int[2]{ 1, 604 }, - const bool whole = false); + const bool whole = false) const; /** * @brief gets a random verse from the Quran * @return QPair of ::Verse instance and verse text */ - QList randomVerse(); + QList randomVerse() const; /** * @brief setVerseType * @param newVerseType @@ -133,10 +132,10 @@ class QuranDb QStringList surahNames() const; private: - const int m_qcfVer = Settings::qcfVersion; - const QLocale::Language m_languageCode = Settings::language; - const QSharedPointer m_assetsDir = DirManager::assetsDir; - VerseType m_verseType = Settings::Qcf; + QuranDb(); + Configuration& m_config; + const QDir& m_assetsDir; + VerseType m_verseType; /** * @brief QList of sura names (Arabic if UI language is Arabic, Otherwise * English) diff --git a/src/database/tafsirdb.cpp b/src/database/tafsirdb.cpp index 2f808783..83ae6396 100644 --- a/src/database/tafsirdb.cpp +++ b/src/database/tafsirdb.cpp @@ -1,15 +1,18 @@ #include "tafsirdb.h" #include -QSharedPointer -TafsirDb::current() +TafsirDb& +TafsirDb::getInstance() { - static QSharedPointer tafsirDb = QSharedPointer::create(); - return tafsirDb; + static TafsirDb tadb; + return tadb; } TafsirDb::TafsirDb() : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "TafsirCon")) + , m_config(Configuration::getInstance()) + , m_dirMgr(DirManager::getInstance()) + , m_tafasir(Tafsir::tafasir) { updateLoadedTafsir(); } @@ -31,7 +34,7 @@ TafsirDb::type() void TafsirDb::updateLoadedTafsir() { - int curr = m_settings->value("Reader/Tafsir").toInt(); + int curr = m_config.settings().value("Reader/Tafsir").toInt(); setCurrentTafsir(curr); } @@ -40,12 +43,12 @@ TafsirDb::setCurrentTafsir(int idx) { if (idx < 0 || idx >= m_tafasir.size()) return false; - if (m_currTafsir == m_tafasir[idx]) + if (m_currTafsir == &m_tafasir[idx]) return true; - m_currTafsir = m_tafasir[idx]; + m_currTafsir = &m_tafasir[idx]; const QDir& baseDir = - m_currTafsir->isExtra() ? *m_downloadsDir : *m_assetsDir; + m_currTafsir->isExtra() ? m_dirMgr.downloadsDir() : m_dirMgr.assetsDir(); QString path = "tafasir/" + m_currTafsir->filename(); if (!baseDir.exists(path)) return false; @@ -72,7 +75,7 @@ TafsirDb::getTafsir(const int sIdx, const int vIdx) return dbQuery.value(0).toString(); } -QSharedPointer +const Tafsir* TafsirDb::currTafsir() const { return m_currTafsir; diff --git a/src/database/tafsirdb.h b/src/database/tafsirdb.h index c3ef29a0..30315620 100644 --- a/src/database/tafsirdb.h +++ b/src/database/tafsirdb.h @@ -2,12 +2,13 @@ #define TAFSIRDB_H #include +#include #include #include #include #include +#include #include -#include class TafsirDb : public DbConnection @@ -15,8 +16,7 @@ class TafsirDb { public: - static QSharedPointer current(); - TafsirDb(); + static TafsirDb& getInstance(); void open(); Type type(); /** @@ -40,17 +40,17 @@ class TafsirDb * @brief getter for m_currTafsir * @return the currently set DBManager::Tafasir */ - QSharedPointer<::Tafsir> currTafsir() const; + const ::Tafsir* currTafsir() const; private: - const QSharedPointer m_settings = Settings::settings; - const QSharedPointer m_assetsDir = DirManager::assetsDir; - const QSharedPointer m_downloadsDir = DirManager::downloadsDir; - const QList>& m_tafasir = Tafsir::tafasir; + TafsirDb(); + Configuration& m_config; + const DirManager& m_dirMgr; + const QList<::Tafsir>& m_tafasir; /** * @brief the current active DBManager::Tafasir */ - QSharedPointer<::Tafsir> m_currTafsir; + const ::Tafsir* m_currTafsir; /** * @brief path to the currently active tafsir database file */ diff --git a/src/database/translationdb.cpp b/src/database/translationdb.cpp index 1a3b79c7..59c5c5c9 100644 --- a/src/database/translationdb.cpp +++ b/src/database/translationdb.cpp @@ -1,16 +1,18 @@ #include "translationdb.h" #include -QSharedPointer -TranslationDb::current() +TranslationDb& +TranslationDb::getInstance() { - static QSharedPointer translationDb = - QSharedPointer::create(); - return translationDb; + static TranslationDb tdb; + return tdb; } TranslationDb::TranslationDb() : QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", "TranslationCon")) + , m_dirMgr(DirManager::getInstance()) + , m_config(Configuration::getInstance()) + , m_translations(Translation::translations) { } @@ -31,7 +33,7 @@ TranslationDb::type() void TranslationDb::updateLoadedTranslation() { - int curr = m_settings->value("Reader/Translation").toInt(); + int curr = m_config.settings().value("Reader/Translation").toInt(); setCurrentTranslation(curr); } @@ -40,11 +42,13 @@ TranslationDb::setCurrentTranslation(int idx) { if (idx < 0 || idx >= m_translations.size()) return false; - if (m_currTr == m_translations[idx]) + if (m_currTr == &m_translations[idx]) return true; - m_currTr = m_translations[idx]; - const QDir& baseDir = m_currTr->isExtra() ? *m_downloadsDir : *m_assetsDir; + m_currTr = &m_translations[idx]; + const QDir& baseDir = m_currTr->isExtra() + ? DirManager::getInstance().downloadsDir() + : DirManager::getInstance().assetsDir(); QString path = "translations/" + m_currTr->filename(); if (!baseDir.exists(path)) return false; @@ -55,7 +59,7 @@ TranslationDb::setCurrentTranslation(int idx) } QString -TranslationDb::getTranslation(const int sIdx, const int vIdx) +TranslationDb::getTranslation(const int sIdx, const int vIdx) const { QSqlQuery dbQuery(*this); @@ -71,7 +75,7 @@ TranslationDb::getTranslation(const int sIdx, const int vIdx) return dbQuery.value(0).toString(); } -QSharedPointer +const Translation* TranslationDb::currTranslation() const { return m_currTr; diff --git a/src/database/translationdb.h b/src/database/translationdb.h index 4a614fe2..a88e220f 100644 --- a/src/database/translationdb.h +++ b/src/database/translationdb.h @@ -3,12 +3,13 @@ #include #include +#include #include #include #include #include +#include #include -#include class TranslationDb : public DbConnection @@ -16,8 +17,7 @@ class TranslationDb { Q_OBJECT public: - static QSharedPointer current(); - TranslationDb(); + static TranslationDb& getInstance(); void open(); Type type(); /** @@ -32,14 +32,14 @@ class TranslationDb * @param vIdx - verse number * @return QString containing the verse translation */ - QString getTranslation(const int sIdx, const int vIdx); + QString getTranslation(const int sIdx, const int vIdx) const; /** * @brief currTranslation * @return * * MODIFIED */ - QSharedPointer<::Translation> currTranslation() const; + const ::Translation* currTranslation() const; public slots: /** @@ -48,15 +48,14 @@ public slots: void updateLoadedTranslation(); private: - const QSharedPointer m_assetsDir = DirManager::assetsDir; - const QSharedPointer m_downloadsDir = DirManager::downloadsDir; - const QSharedPointer m_settings = Settings::settings; - const QList>& m_translations = - Translation::translations; + TranslationDb(); + Configuration& m_config; + const DirManager& m_dirMgr; + const QList<::Translation>& m_translations; /** * @brief the current active DBManager::Translation */ - QSharedPointer<::Translation> m_currTr; + const ::Translation* m_currTr; /** * @brief path to the currently active translation database file */ diff --git a/src/dialogs/aboutdialog.cpp b/src/dialogs/aboutdialog.cpp index 65153abb..4fd94dbf 100644 --- a/src/dialogs/aboutdialog.cpp +++ b/src/dialogs/aboutdialog.cpp @@ -4,6 +4,7 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent) , ui(new Ui::AboutDialog) + , m_config(Configuration::getInstance()) { ui->setupUi(this); QFont bolded = qApp->font(); @@ -26,7 +27,7 @@ AboutDialog::AboutDialog(QWidget* parent) ui->lbContribTranslation->text().arg(tr("Contribute to translations"))); connect(ui->btnBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - if (m_lang == QLocale::Arabic) + if (m_config.language() == QLocale::Arabic) ui->aboutTabWidget->setObjectName("rtlTabWidget"); } diff --git a/src/dialogs/aboutdialog.h b/src/dialogs/aboutdialog.h index 1c66ed55..89019b99 100644 --- a/src/dialogs/aboutdialog.h +++ b/src/dialogs/aboutdialog.h @@ -2,7 +2,8 @@ #define ABOUTDIALOG_H #include -#include +#include +#include namespace Ui { class AboutDialog; @@ -18,7 +19,7 @@ class AboutDialog : public QDialog private: Ui::AboutDialog* ui; - QLocale::Language m_lang = Settings::language; + const Configuration& m_config; }; #endif // ABOUTDIALOG_H diff --git a/src/dialogs/bookmarksdialog.cpp b/src/dialogs/bookmarksdialog.cpp index e209bf0e..10ed7d9e 100644 --- a/src/dialogs/bookmarksdialog.cpp +++ b/src/dialogs/bookmarksdialog.cpp @@ -12,15 +12,20 @@ BookmarksDialog::BookmarksDialog(QWidget* parent) : QDialog(parent) , ui(new Ui::BookmarksDialog) + , m_config(Configuration::getInstance()) + , m_quranDb(QuranDb::getInstance()) + , m_bookmarksDb(BookmarksDb::getInstance()) + , m_glpyhsDb(GlyphsDb::getInstance()) { ui->setupUi(this); ui->navBar->setLayoutDirection(Qt::LeftToRight); - ui->btnNext->setIcon( - StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_left)); - ui->btnPrev->setIcon( - StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_right)); + ui->btnNext->setIcon(StyleManager::getInstance().awesome().icon( + fa::fa_solid, fa::fa_arrow_left)); + ui->btnPrev->setIcon(StyleManager::getInstance().awesome().icon( + fa::fa_solid, fa::fa_arrow_right)); ui->scrollArea->setLayoutDirection(Qt::LeftToRight); - setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_bookmark)); + setWindowIcon( + StyleManager::getInstance().awesome().icon(fa::fa_solid, fa::fa_bookmark)); ui->listViewBookmarkedSurahs->setModel(&m_surahsModel); loadDefault(); @@ -82,7 +87,7 @@ BookmarksDialog::loadBookmarks(int surah) { if (m_shownSurah != surah) { m_shownSurah = surah; - m_shownVerses = m_bookmarksDb->bookmarkedVerses(surah); + m_shownVerses = m_bookmarksDb.bookmarkedVerses(surah); if (m_shownSurah == -1) m_allBookmarked = m_shownVerses; } @@ -106,8 +111,8 @@ BookmarksDialog::loadBookmarks(int surah) for (int i = m_startIdx; i < end; i++) { const Verse& verse = m_shownVerses.at(i); - QString fontName = - FontManager::verseFontname(m_quranDb->verseType(), verse.page()); + QString fontName = FontManager::getInstance().verseFontname( + m_quranDb.verseType(), verse.page()); QFrame* frame = new QFrame(ui->scrlBookmarks); frame->setProperty("bookmark", true); @@ -131,12 +136,12 @@ BookmarksDialog::loadBookmarks(int surah) removeFromFav, &QPushButton::clicked, this, &BookmarksDialog::btnRemove); QString info = tr("Surah: ") + - m_quranDb->surahNames().at(verse.surah() - 1) + " - " + + m_quranDb.surahNames().at(verse.surah() - 1) + " - " + tr("Verse: ") + QString::number(verse.number()); QString glyphs = - m_quranDb->verseType() == Settings::Qcf - ? m_glpyhsDb->getVerseGlyphs(verse.surah(), verse.number()) - : m_quranDb->verseText(verse.surah(), verse.number()); + m_quranDb.verseType() == Configuration::Qcf + ? m_glpyhsDb.getVerseGlyphs(verse.surah(), verse.number()) + : m_quranDb.verseText(verse.surah(), verse.number()); lbMeta->setText(info); lbMeta->setAlignment(Qt::AlignLeft); @@ -187,7 +192,7 @@ BookmarksDialog::loadSurahs() } for (int s : surahs) { - item = new QStandardItem(m_quranDb->surahNames().at(s - 1)); + item = new QStandardItem(m_quranDb.surahNames().at(s - 1)); item->setData(Qt::AlignCenter, Qt::TextAlignmentRole); item->setToolTip(item->text()); item->setData(s, Qt::UserRole); @@ -227,7 +232,7 @@ BookmarksDialog::btnRemove() QStringList info = sender()->parent()->objectName().split('-'); Verse verse{ info.at(0).toInt(), info.at(1).toInt(), info.at(2).toInt() }; - if (m_bookmarksDb->removeBookmark(verse, true)) { + if (m_bookmarksDb.removeBookmark(verse, true)) { QFrame* frm = qobject_cast(sender()->parent()); int idx = m_frames.indexOf(frm); if (idx != -1) diff --git a/src/dialogs/bookmarksdialog.h b/src/dialogs/bookmarksdialog.h index a3168679..64753ba5 100644 --- a/src/dialogs/bookmarksdialog.h +++ b/src/dialogs/bookmarksdialog.h @@ -16,7 +16,7 @@ #include #include #include -#include +#include namespace Ui { class BookmarksDialog; @@ -119,10 +119,10 @@ private slots: private: Ui::BookmarksDialog* ui; - const int m_qcfVer = Settings::qcfVersion; - QSharedPointer m_quranDb = QuranDb::current(); - QSharedPointer m_bookmarksDb = BookmarksDb::current(); - QSharedPointer m_glpyhsDb = GlyphsDb::current(); + const Configuration& m_config; + const GlyphsDb& m_glpyhsDb; + const QuranDb& m_quranDb; + BookmarksDb& m_bookmarksDb; /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/dialogs/contentdialog.cpp b/src/dialogs/contentdialog.cpp index c87e9967..1a258b46 100644 --- a/src/dialogs/contentdialog.cpp +++ b/src/dialogs/contentdialog.cpp @@ -12,22 +12,33 @@ ContentDialog::ContentDialog(QWidget* parent) : QDialog(parent) , ui(new Ui::ContentDialog) - , m_tafsir(m_settings->value("Reader/Tafsir").toInt()) - , m_translation(m_settings->value("Reader/Translation").toInt()) + , m_config(Configuration::getInstance()) + , m_quranDb(QuranDb::getInstance()) + , m_bookmarksDb(BookmarksDb::getInstance()) + , m_glyphsDb(GlyphsDb::getInstance()) + , m_tafsirDb(TafsirDb::getInstance()) + , m_translationDb(TranslationDb::getInstance()) + , m_tafasir(Tafsir::tafasir) + , m_translations(Translation::translations) + , m_currMode(Tafsir) + , m_internalLoading(false) { - setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_book_open)); + setWindowIcon( + StyleManager::getInstance().awesome().icon(fa::fa_solid, fa::fa_book_open)); ui->setupUi(this); ui->frmNav->setLayoutDirection(Qt::LeftToRight); - ui->btnNext->setIcon( - StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_left)); - ui->btnPrev->setIcon( - StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_right)); + ui->btnNext->setIcon(StyleManager::getInstance().awesome().icon( + fa::fa_solid, fa::fa_arrow_left)); + ui->btnPrev->setIcon(StyleManager::getInstance().awesome().icon( + fa::fa_solid, fa::fa_arrow_right)); - if (m_qcfVer == 1) + if (m_config.qcfVersion() == 1) m_fontSZ = 18; else m_fontSZ = 16; + m_tafsir = m_config.settings().value("Reader/Tafsir").toInt(); + m_translation = m_config.settings().value("Reader/Translation").toInt(); // connectors setupConnections(); } @@ -54,12 +65,12 @@ ContentDialog::showVerseTafsir(const Verse& v) { static bool reload = false; if (reload) { - m_tafsirDb->updateLoadedTafsir(); + m_tafsirDb.updateLoadedTafsir(); reload = false; } - if (!m_tafsirDb->currTafsir()->isAvailable()) { - int i = m_tafasir.indexOf(m_tafsirDb->currTafsir()); + if (!m_tafsirDb.currTafsir()->isAvailable()) { + int i = m_tafasir.indexOf(*m_tafsirDb.currTafsir()); reload = true; emit missingTafsir(i); return; @@ -75,12 +86,12 @@ ContentDialog::showVerseTranslation(const Verse& v) { static bool reload = false; if (reload) { - m_translationDb->updateLoadedTranslation(); + m_translationDb.updateLoadedTranslation(); reload = false; } - if (!m_translationDb->currTranslation()->isAvailable()) { - int i = m_translations.indexOf(m_translationDb->currTranslation()); + if (!m_translationDb.currTranslation()->isAvailable()) { + int i = m_translations.indexOf(*m_translationDb.currTranslation()); reload = true; emit missingTranslation(i); return; @@ -103,7 +114,7 @@ void ContentDialog::setSideFont() { QFont sideFont = - qvariant_cast(m_settings->value("Reader/SideContentFont")); + qvariant_cast(m_config.settings().value("Reader/SideContentFont")); ui->tedContent->setFont(sideFont); } @@ -114,15 +125,15 @@ ContentDialog::setShownVerse(const Verse& newShownVerse) if (m_shownVerse.number() == 0) m_shownVerse.setNumber(1); - QString title = tr("Surah: ") + m_quranDb->surahName(m_shownVerse.surah()) + + QString title = tr("Surah: ") + m_quranDb.surahName(m_shownVerse.surah()) + " - " + tr("Verse: ") + QString::number(m_shownVerse.number()); QString glyphs = - m_quranDb->verseType() == Settings::Qcf - ? m_glyphsDb->getVerseGlyphs(m_shownVerse.surah(), m_shownVerse.number()) - : m_quranDb->verseText(m_shownVerse.surah(), m_shownVerse.number()); - QString fontFamily = - FontManager::verseFontname(m_quranDb->verseType(), m_shownVerse.page()); + m_quranDb.verseType() == Configuration::Qcf + ? m_glyphsDb.getVerseGlyphs(m_shownVerse.surah(), m_shownVerse.number()) + : m_quranDb.verseText(m_shownVerse.surah(), m_shownVerse.number()); + QString fontFamily = FontManager::getInstance().getInstance().verseFontname( + m_quranDb.verseType(), m_shownVerse.page()); ui->lbVerseInfo->setText(title); ui->lbVerseText->setWordWrap(true); @@ -185,8 +196,8 @@ void ContentDialog::tafsirChanged() { m_tafsir = ui->cmbContent->currentData().toInt(); - m_settings->setValue("Reader/Tafsir", m_tafsir); - if (m_tafsirDb->setCurrentTafsir(m_tafsir)) + m_config.settings().setValue("Reader/Tafsir", m_tafsir); + if (m_tafsirDb.setCurrentTafsir(m_tafsir)) loadVerseTafsir(); } @@ -242,9 +253,9 @@ void ContentDialog::cmbLoadTafasir() { for (int i = 0; i < m_tafasir.size(); i++) { - const QSharedPointer<::Tafsir>& t = m_tafasir.at(i); - if (t->isAvailable()) - ui->cmbContent->addItem(t->displayName(), i); + const ::Tafsir& t = m_tafasir.at(i); + if (t.isAvailable()) + ui->cmbContent->addItem(t.displayName(), i); } int idx = ui->cmbContent->findData(m_tafsir); @@ -255,9 +266,9 @@ void ContentDialog::cmbLoadTranslations() { for (int i = 0; i < m_translations.size(); i++) { - const QSharedPointer<::Translation>& tr = m_translations[i]; - if (tr->isAvailable()) - ui->cmbContent->addItem(tr->displayName(), i); + const ::Translation& tr = m_translations[i]; + if (tr.isAvailable()) + ui->cmbContent->addItem(tr.displayName(), i); } int idx = ui->cmbContent->findData(m_translation); @@ -267,26 +278,26 @@ ContentDialog::cmbLoadTranslations() void ContentDialog::loadVerseTafsir() { - if (m_tafsirDb->currTafsir()->isText()) + if (m_tafsirDb.currTafsir()->isText()) ui->tedContent->setText( - m_tafsirDb->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); + m_tafsirDb.getTafsir(m_shownVerse.surah(), m_shownVerse.number())); else ui->tedContent->setHtml( - m_tafsirDb->getTafsir(m_shownVerse.surah(), m_shownVerse.number())); + m_tafsirDb.getTafsir(m_shownVerse.surah(), m_shownVerse.number())); } void ContentDialog::loadVerseTranslation() { - m_translationDb->setCurrentTranslation(m_translation); - ui->tedContent->setText(m_translationDb->getTranslation( + m_translationDb.setCurrentTranslation(m_translation); + ui->tedContent->setText(m_translationDb.getTranslation( m_shownVerse.surah(), m_shownVerse.number())); } void ContentDialog::loadVerseThoughts() { - ui->tedContent->setText(m_bookmarksDb->getThoughts(m_shownVerse)); + ui->tedContent->setText(m_bookmarksDb.getThoughts(m_shownVerse)); ui->tedContent->setReadOnly(false); ui->tedContent->setCursorWidth(1); } @@ -296,7 +307,7 @@ ContentDialog::saveVerseThoughts() { ui->tedContent->setCursorWidth(0); ui->tedContent->setReadOnly(true); - m_bookmarksDb->saveThoughts(m_shownVerse, ui->tedContent->toPlainText()); + m_bookmarksDb.saveThoughts(m_shownVerse, ui->tedContent->toPlainText()); } void diff --git a/src/dialogs/contentdialog.h b/src/dialogs/contentdialog.h index 7e1c33c7..1ef9f351 100644 --- a/src/dialogs/contentdialog.h +++ b/src/dialogs/contentdialog.h @@ -92,16 +92,14 @@ private slots: private: Ui::ContentDialog* ui; - QSharedPointer m_quranDb = QuranDb::current(); - QSharedPointer m_glyphsDb = GlyphsDb::current(); - QSharedPointer m_bookmarksDb = BookmarksDb::current(); - QSharedPointer m_tafsirDb = TafsirDb::current(); - QSharedPointer m_translationDb = TranslationDb::current(); - const int m_qcfVer = Settings::qcfVersion; - const QSharedPointer m_settings = Settings::settings; - const QList>& m_tafasir = Tafsir::tafasir; - const QList>& m_translations = - Translation::translations; + Configuration& m_config; + TafsirDb& m_tafsirDb; + TranslationDb& m_translationDb; + BookmarksDb& m_bookmarksDb; + const QuranDb& m_quranDb; + const GlyphsDb& m_glyphsDb; + const QList<::Tafsir>& m_tafasir; + const QList<::Translation>& m_translations; /** * @brief setSideFont * @@ -182,7 +180,7 @@ private slots: * * MODIFIED */ - Mode m_currMode = Mode::Tafsir; + Mode m_currMode; int m_tafsir; int m_translation; /** @@ -196,7 +194,7 @@ private slots: /** * @brief m_internalLoading */ - bool m_internalLoading = false; + bool m_internalLoading; }; #endif // CONTENTDIALOG_H diff --git a/src/dialogs/copydialog.cpp b/src/dialogs/copydialog.cpp index e2c33e07..b45913af 100644 --- a/src/dialogs/copydialog.cpp +++ b/src/dialogs/copydialog.cpp @@ -5,6 +5,8 @@ CopyDialog::CopyDialog(QWidget* parent) : QDialog(parent) , ui(new Ui::CopyDialog) , m_verseValidator(new QIntValidator(this)) + , m_currVerse(Verse::getCurrent()) + , m_quranDb(QuranDb::getInstance()) , m_notifier(this) { ui->setupUi(this); @@ -18,13 +20,13 @@ void CopyDialog::copyVerseText(const Verse v) { QClipboard* clip = QApplication::clipboard(); - QString text = m_quranDb->verseText(v.surah(), v.number()); + QString text = m_quranDb.verseText(v.surah(), v.number()); QString vNum = QString::number(v.number()); text.remove(text.size() - 1, 1); text = text.trimmed(); text = "{" + text + "}"; text += ' '; - text += "[" + m_quranDb->surahNames().at(v.surah() - 1) + ":" + vNum + "]"; + text += "[" + m_quranDb.surahNames().at(v.surah() - 1) + ":" + vNum + "]"; clip->setText(text); m_notifier.copied(); } @@ -34,8 +36,8 @@ CopyDialog::copyRange() { int from = ui->cmbCopyFrom->currentText().toInt(); int to = ui->cmbCopyTo->currentText().toInt(); - if (to < from || from <= 0 || from > m_currVerse->surahCount() || to <= 0 || - to > m_currVerse->surahCount()) { + if (to < from || from <= 0 || from > m_currVerse.surahCount() || to <= 0 || + to > m_currVerse.surahCount()) { QMessageBox::warning( this, tr("Invalid range"), tr("The entered verse range is invalid")); return; @@ -44,13 +46,13 @@ CopyDialog::copyRange() QString final = "{ "; QClipboard* clip = QApplication::clipboard(); for (int i = from; i <= to; i++) { - QString text = m_quranDb->verseText(m_currVerse->surah(), i); + QString text = m_quranDb.verseText(m_currVerse.surah(), i); text.remove(text.size() - 1, 1); text += "(" + QString::number(i) + ") "; final.append(text); } - final += "} [" + m_quranDb->surahNames().at(m_currVerse->surah() - 1) + "]"; + final += "} [" + m_quranDb.surahNames().at(m_currVerse.surah() - 1) + "]"; clip->setText(final); m_notifier.copied(); } @@ -60,17 +62,17 @@ CopyDialog::show() { ui->cmbCopyFrom->clear(); ui->cmbCopyTo->clear(); - ui->lbCopySurahName->setText(m_quranDb->surahName(m_currVerse->surah())); + ui->lbCopySurahName->setText(m_quranDb.surahName(m_currVerse.surah())); - for (int i = 1; i <= m_currVerse->surahCount(); i++) { + for (int i = 1; i <= m_currVerse.surahCount(); i++) { ui->cmbCopyFrom->addItem(QString::number(i)); ui->cmbCopyTo->addItem(QString::number(i)); } - int idx = m_currVerse->number() ? m_currVerse->number() - 1 : 0; + int idx = m_currVerse.number() ? m_currVerse.number() - 1 : 0; ui->cmbCopyFrom->setCurrentIndex(idx); ui->cmbCopyTo->setCurrentIndex(idx); m_verseValidator->setBottom(1); - m_verseValidator->setTop(m_currVerse->surahCount()); + m_verseValidator->setTop(m_currVerse.surahCount()); QDialog::show(); } diff --git a/src/dialogs/copydialog.h b/src/dialogs/copydialog.h index ed3d0411..bca0d435 100644 --- a/src/dialogs/copydialog.h +++ b/src/dialogs/copydialog.h @@ -38,8 +38,8 @@ private slots: private: Ui::CopyDialog* ui; - QSharedPointer m_currVerse = Verse::current(); - QSharedPointer m_quranDb = QuranDb::current(); + const QuranDb& m_quranDb; + const Verse& m_currVerse; QPointer m_verseValidator; CopyNotifier m_notifier; }; diff --git a/src/dialogs/downloaderdialog.cpp b/src/dialogs/downloaderdialog.cpp index 6a0c0d77..4c09d036 100644 --- a/src/dialogs/downloaderdialog.cpp +++ b/src/dialogs/downloaderdialog.cpp @@ -14,13 +14,19 @@ DownloaderDialog::DownloaderDialog(QWidget* parent, JobManager* manager) : QDialog(parent) , ui(new Ui::DownloaderDialog) , m_jobMgr(manager) - , m_surahDisplayNames(m_quranDb->surahNames()) + , m_config(Configuration::getInstance()) + , m_quranDb(QuranDb::getInstance()) + , m_reciters(Reciter::reciters) + , m_tafasir(Tafsir::tafasir) + , m_translations(Translation::translations) { ui->setupUi(this); - setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_download)); + setWindowIcon( + StyleManager::getInstance().awesome().icon(fa::fa_solid, fa::fa_download)); // treeview setup + m_surahDisplayNames = m_quranDb.surahNames(); QStringList headers; headers.append(tr("Name")); headers.append(tr("Number")); @@ -82,9 +88,9 @@ void DownloaderDialog::populateTreeModel() { // add reciters - for (const QSharedPointer& reciter : m_reciters) { - QStandardItem* item = new QStandardItem(reciter->displayName()); - item->setToolTip(reciter->displayName()); + for (const Reciter& reciter : m_reciters) { + QStandardItem* item = new QStandardItem(reciter.displayName()); + item->setToolTip(reciter.displayName()); m_treeModel.invisibleRootItem()->appendRow(item); for (int j = 1; j <= 114; j++) { @@ -105,10 +111,10 @@ DownloaderDialog::populateTreeModel() m_treeModel.invisibleRootItem()->appendRow(tafsir); // -- tafasir for (int i = 0; i < m_tafasir.size(); i++) { - const QSharedPointer& t = m_tafasir.at(i); - if (!t->isExtra()) + const Tafsir& t = m_tafasir.at(i); + if (!t.isExtra()) continue; - QStandardItem* item = new QStandardItem(t->displayName()); + QStandardItem* item = new QStandardItem(t.displayName()); item->setData("tadb", Qt::UserRole); item->setData(i, Qt::UserRole + 1); tafsir->appendRow(item); @@ -120,11 +126,11 @@ DownloaderDialog::populateTreeModel() tafsir->setData("translation", Qt::UserRole); m_treeModel.invisibleRootItem()->appendRow(translation); // -- translations - for (int i = 0; i < m_tr.size(); i++) { - const QSharedPointer& tr = m_tr.at(i); - if (!tr->isExtra()) + for (int i = 0; i < m_translations.size(); i++) { + const Translation& tr = m_translations.at(i); + if (!tr.isExtra()) continue; - QStandardItem* item = new QStandardItem(tr->displayName()); + QStandardItem* item = new QStandardItem(tr.displayName()); item->setData("trdb", Qt::UserRole); item->setData(i, Qt::UserRole + 1); translation->appendRow(item); @@ -211,7 +217,7 @@ DownloaderDialog::addTaskProgress(QSharedPointer job) QString objName; if (job->type() == DownloadJob::Recitation) { SurahJob* sJob = qobject_cast(job.data()); - QString reciter = m_reciters.at(sJob->reciter())->displayName(); + QString reciter = m_reciters.at(sJob->reciter()).displayName(); QString surahName = m_surahDisplayNames.at(sJob->surah() - 1); objName = reciter + tr(" // Surah: ") + surahName; } else { @@ -223,7 +229,7 @@ DownloaderDialog::addTaskProgress(QSharedPointer job) prgFrm->setObjectName(objName); QBoxLayout* downInfo; - if (m_languageCode == QLocale::Arabic) + if (m_lang == QLocale::Arabic) downInfo = new QBoxLayout(QBoxLayout::RightToLeft, prgFrm); else downInfo = new QHBoxLayout(prgFrm); @@ -398,7 +404,8 @@ DownloaderDialog::topTaskDownloadError(QSharedPointer failed) void DownloaderDialog::openDownloadsDir() { - QUrl url = QUrl::fromLocalFile(DirManager::downloadsDir->absolutePath()); + QUrl url = QUrl::fromLocalFile( + DirManager::getInstance().downloadsDir().absolutePath()); QDesktopServices::openUrl(url); } diff --git a/src/dialogs/downloaderdialog.h b/src/dialogs/downloaderdialog.h index 65ebe429..091b9286 100644 --- a/src/dialogs/downloaderdialog.h +++ b/src/dialogs/downloaderdialog.h @@ -19,7 +19,7 @@ #include #include #include -#include +#include #include namespace Ui { @@ -122,11 +122,11 @@ private slots: private: Ui::DownloaderDialog* ui; - QSharedPointer m_quranDb = QuranDb::current(); - const int m_languageCode = Settings::language; - const QList>& m_reciters = Reciter::reciters; - const QList>& m_tafasir = Tafsir::tafasir; - const QList>& m_tr = Translation::translations; + const Configuration& m_config; + const QuranDb& m_quranDb; + const QList& m_reciters; + const QList& m_tafasir; + const QList& m_translations; /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/dialogs/fileselector.cpp b/src/dialogs/fileselector.cpp index 3ca41f9e..3728e5d4 100644 --- a/src/dialogs/fileselector.cpp +++ b/src/dialogs/fileselector.cpp @@ -6,7 +6,7 @@ FileSelector::FileSelector(QWidget* parent) : QFileDialog(parent) { setFileMode(QFileDialog::AnyFile); - setDirectory(DirManager::downloadsDir->absolutePath()); + setDirectory(DirManager::getInstance().downloadsDir().absolutePath()); } QString diff --git a/src/dialogs/khatmahdialog.cpp b/src/dialogs/khatmahdialog.cpp index a313469a..2731fe22 100644 --- a/src/dialogs/khatmahdialog.cpp +++ b/src/dialogs/khatmahdialog.cpp @@ -1,17 +1,22 @@ #include "khatmahdialog.h" #include "ui_khatmahdialog.h" #include -#include +#include #include KhatmahDialog::KhatmahDialog(QWidget* parent) : QDialog(parent) , ui(new Ui::KhatmahDialog) + , m_currVerse(Verse::getCurrent()) + , m_config(Configuration::getInstance()) + , m_quranDb(QuranDb::getInstance()) + , m_bookmarksDb(BookmarksDb::getInstance()) { ui->setupUi(this); - setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_list)); + setWindowIcon( + StyleManager::getInstance().awesome().icon(fa::fa_solid, fa::fa_list)); ui->lbCurrKhatmah->setText( - m_bookmarksDb->getKhatmahName(m_bookmarksDb->activeKhatmah())); + m_bookmarksDb.getKhatmahName(m_bookmarksDb.activeKhatmah())); loadAll(); connect(ui->btnStartKhatmah, @@ -24,11 +29,11 @@ QPointer KhatmahDialog::loadKhatmah(const int id) { Verse v; - m_bookmarksDb->loadVerse(id, v); + m_bookmarksDb.loadVerse(id, v); QFrame* frame = new QFrame(ui->scrlDialogContent); // property used for styling frame->setProperty("khatmah", true); - if (!Settings::darkMode) { + if (!m_config.darkMode()) { QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect(frame); shadow->setColor(palette().color(QPalette::Shadow)); shadow->setOffset(1); @@ -39,7 +44,7 @@ KhatmahDialog::loadKhatmah(const int id) QHBoxLayout* frmLayout = new QHBoxLayout(); QVBoxLayout* lbLayout = new QVBoxLayout(); QVBoxLayout* btnLayout = new QVBoxLayout(); - InputField* ifName = new InputField(frame, m_bookmarksDb->getKhatmahName(id)); + InputField* ifName = new InputField(frame, m_bookmarksDb.getKhatmahName(id)); QLabel* lbPosition = new QLabel(frame); QPushButton* activate = new QPushButton(tr("Set as active"), frame); QPushButton* remove = new QPushButton(tr("Remove"), frame); @@ -50,13 +55,13 @@ KhatmahDialog::loadKhatmah(const int id) activate->setStyleSheet( "QPushButton { min-width: 150px; max-width: 150px; }"); remove->setStyleSheet("QPushButton { min-width: 150px; max-width: 150px; }"); - if (id == m_bookmarksDb->activeKhatmah()) { + if (id == m_bookmarksDb.activeKhatmah()) { m_currActive = frame; activate->setDisabled(true); remove->setDisabled(true); } - QString info = tr("Surah: ") + m_quranDb->surahNames().at(v.surah() - 1) + + QString info = tr("Surah: ") + m_quranDb.surahNames().at(v.surah() - 1) + " - " + tr("Verse: ") + QString::number(v.number()); lbPosition->setText(info); @@ -95,10 +100,10 @@ KhatmahDialog::loadKhatmah(const int id) void KhatmahDialog::loadAll() { - m_khatmahIds = m_bookmarksDb->getAllKhatmah(); + m_khatmahIds = m_bookmarksDb.getAllKhatmah(); for (int i = 0; i < m_khatmahIds.size(); i++) { m_names.insert(m_khatmahIds.at(i), - m_bookmarksDb->getKhatmahName(m_khatmahIds.at(i))); + m_bookmarksDb.getKhatmahName(m_khatmahIds.at(i))); loadKhatmah(m_khatmahIds.at(i)); } } @@ -106,9 +111,9 @@ KhatmahDialog::loadAll() void KhatmahDialog::startNewKhatmah() { - int id = m_bookmarksDb->addKhatmah(m_currVerse->toList(), "new"); + int id = m_bookmarksDb.addKhatmah(m_currVerse.toList(), "new"); QString gen = tr("Khatmah ") + QString::number(id); - m_bookmarksDb->editKhatmahName(id, gen); + m_bookmarksDb.editKhatmahName(id, gen); InputField* inpField = loadKhatmah(id); m_khatmahIds.append(id); m_names.insert(id, gen); @@ -120,14 +125,14 @@ KhatmahDialog::renameKhatmah(QString name) { InputField* caller = qobject_cast(sender()); int id = caller->parent()->objectName().toInt(); - bool ok = m_bookmarksDb->editKhatmahName(id, name); + bool ok = m_bookmarksDb.editKhatmahName(id, name); if (!ok) caller->setText(m_names.value(id)); else { m_names[id] = name; caller->setText(name); caller->clearFocus(); - if (id == m_bookmarksDb->activeKhatmah()) + if (id == m_bookmarksDb.activeKhatmah()) ui->lbCurrKhatmah->setText(name); } } @@ -136,7 +141,7 @@ void KhatmahDialog::removeKhatmah() { int id = sender()->parent()->objectName().toInt(); - m_bookmarksDb->removeKhatmah(id); + m_bookmarksDb.removeKhatmah(id); QFrame* rem = ui->scrlDialogContent->findChild(QString::number(id)); m_frmLst[m_frmLst.indexOf(rem)] = nullptr; delete rem; @@ -149,10 +154,10 @@ KhatmahDialog::setActiveKhatmah() QFrame* newActive = qobject_cast(sender()->parent()); QVariant id = newActive->objectName(); - m_settings->setValue("Reader/Khatmah", id); - m_bookmarksDb->saveActiveKhatmah(*m_currVerse); - m_bookmarksDb->setActiveKhatmah(id.toInt()); - m_bookmarksDb->loadVerse(id.toInt(), v); + m_config.settings().setValue("Reader/Khatmah", id); + m_bookmarksDb.saveActiveKhatmah(m_currVerse); + m_bookmarksDb.setActiveKhatmah(id.toInt()); + m_bookmarksDb.loadVerse(id.toInt(), v); newActive->findChild("activate")->setEnabled(false); newActive->findChild("remove")->setEnabled(false); @@ -160,7 +165,7 @@ KhatmahDialog::setActiveKhatmah() m_currActive->findChild("remove")->setEnabled(true); m_currActive = newActive; - ui->lbCurrKhatmah->setText(m_bookmarksDb->getKhatmahName(id.toInt())); + ui->lbCurrKhatmah->setText(m_bookmarksDb.getKhatmahName(id.toInt())); emit navigateToVerse(v); } diff --git a/src/dialogs/khatmahdialog.h b/src/dialogs/khatmahdialog.h index 1417f49a..6e4c4db3 100644 --- a/src/dialogs/khatmahdialog.h +++ b/src/dialogs/khatmahdialog.h @@ -74,10 +74,10 @@ private slots: private: Ui::KhatmahDialog* ui; - const QSharedPointer m_currVerse = Verse::current(); - QSharedPointer m_quranDb = QuranDb::current(); - QSharedPointer m_bookmarksDb = BookmarksDb::current(); - QSharedPointer m_settings = Settings::settings; + const Verse& m_currVerse; + QuranDb& m_quranDb; + BookmarksDb& m_bookmarksDb; + Configuration& m_config; /** * @brief load all khatmah entries available */ diff --git a/src/dialogs/searchdialog.cpp b/src/dialogs/searchdialog.cpp index c282c0ca..209d3e8c 100644 --- a/src/dialogs/searchdialog.cpp +++ b/src/dialogs/searchdialog.cpp @@ -12,24 +12,26 @@ SearchDialog::SearchDialog(QWidget* parent) : QDialog(parent) , ui(new Ui::SearchDialog) - , m_surahNames(m_quranDb->surahNames()) + , m_config(Configuration::getInstance()) + , m_quranDb(QuranDb::getInstance()) + , m_glyphsDb(GlyphsDb::getInstance()) { - setWindowIcon( - StyleManager::awesome->icon(fa::fa_solid, fa::fa_magnifying_glass)); + setWindowIcon(StyleManager::getInstance().awesome().icon( + fa::fa_solid, fa::fa_magnifying_glass)); ui->setupUi(this); ui->frmNavBtns->setLayoutDirection(Qt::LeftToRight); - ui->btnNext->setIcon( - StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_left)); - ui->btnPrev->setIcon( - StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_right)); + ui->btnNext->setIcon(StyleManager::getInstance().awesome().icon( + fa::fa_solid, fa::fa_arrow_left)); + ui->btnPrev->setIcon(StyleManager::getInstance().awesome().icon( + fa::fa_solid, fa::fa_arrow_right)); ui->btnNext->setDisabled(true); ui->btnPrev->setDisabled(true); - ui->btnTransfer->setIcon( - StyleManager::awesome->icon(fa::fa_solid, fa::fa_arrow_right_arrow_left)); + ui->btnTransfer->setIcon(StyleManager::getInstance().awesome().icon( + fa::fa_solid, fa::fa_arrow_right_arrow_left)); ui->listViewAllSurahs->setModel(&m_modelAllSurahs); ui->listViewSelected->setModel(&m_modelSelectedSurahs); - if (m_lang == QLocale::Arabic) + if (m_config.language() == QLocale::Arabic) ui->searchTabWidget->setObjectName("rtlTabWidget"); fillListView(); @@ -74,13 +76,13 @@ SearchDialog::getResults() ui->spnEndPage->setValue(range[0]); range[1] = ui->spnEndPage->value(); - m_currResults = Verse::fromList(m_quranDb->searchVerses( + m_currResults = Verse::fromList(m_quranDb.searchVerses( m_searchText, range, ui->chkWholeWord->isChecked())); } else { m_currResults = - Verse::fromList(m_quranDb->searchSurahs(m_searchText, - m_selectedSurahMap.values(), - ui->chkWholeWord->isChecked())); + Verse::fromList(m_quranDb.searchSurahs(m_searchText, + m_selectedSurahMap.values(), + ui->chkWholeWord->isChecked())); } ui->lbResultCount->setText(QString::number(m_currResults.size()) + tr(" Search results")); @@ -114,17 +116,17 @@ SearchDialog::showResults() for (int i = m_startResult; i < endIdx; i++) { Verse v = m_currResults.at(i); QString fontName = - FontManager::verseFontname(m_quranDb->verseType(), v.page()); + FontManager::getInstance().verseFontname(m_quranDb.verseType(), v.page()); VerseFrame* vFrame = new VerseFrame(ui->srclResults); QLabel* lbInfo = new QLabel(vFrame); ClickableLabel* clkLb = new ClickableLabel(vFrame); - QString info = tr("Surah: ") + m_surahNames.at(v.surah() - 1) + " - " + - tr("Verse: ") + QString::number(v.number()); - QString glyphs = m_quranDb->verseType() == Settings::Qcf - ? m_glyphsDb->getVerseGlyphs(v.surah(), v.number()) - : m_quranDb->verseText(v.surah(), v.number()); + QString info = tr("Surah: ") + m_quranDb.surahNames().at(v.surah() - 1) + + " - " + tr("Verse: ") + QString::number(v.number()); + QString glyphs = m_quranDb.verseType() == Configuration::Qcf + ? m_glyphsDb.getVerseGlyphs(v.surah(), v.number()) + : m_quranDb.verseText(v.surah(), v.number()); lbInfo->setText(info); lbInfo->setMaximumHeight(50); @@ -184,7 +186,7 @@ void SearchDialog::fillListView() { for (int i = 1; i <= 114; i++) { - QStandardItem* surah = new QStandardItem(m_surahNames.at(i - 1)); + QStandardItem* surah = new QStandardItem(m_quranDb.surahNames().at(i - 1)); m_modelAllSurahs.invisibleRootItem()->appendRow(surah); } } @@ -202,7 +204,7 @@ SearchDialog::btnTransferClicked() if (m_selectedSurahMap.contains(midx.data().toString())) continue; // KEY: visible surah name - VALUE: surah number - int sIdx = m_surahNames.indexOf(midx.data().toString()); + int sIdx = m_quranDb.surahNames().indexOf(midx.data().toString()); m_selectedSurahMap.insert(midx.data().toString(), sIdx + 1); m_modelSelectedSurahs.appendRow( new QStandardItem(midx.data().toString())); // add surah to selected view diff --git a/src/dialogs/searchdialog.h b/src/dialogs/searchdialog.h index ed3d5a6a..364eee49 100644 --- a/src/dialogs/searchdialog.h +++ b/src/dialogs/searchdialog.h @@ -97,9 +97,9 @@ private slots: private: Ui::SearchDialog* ui; - const QLocale::Language m_lang = Settings::language; - QSharedPointer m_quranDb = QuranDb::current(); - QSharedPointer m_glyphsDb = GlyphsDb::current(); + const Configuration& m_config; + const QuranDb& m_quranDb; + const GlyphsDb& m_glyphsDb; /** * @brief connects signals and slots for different UI * components and shortcuts. @@ -132,11 +132,6 @@ private slots: * @brief Current search text. */ QString m_searchText; - /** - * @brief QStringList for the surah names which are displayed above search - * results and in the QListView. - */ - QStringList m_surahNames; /** * @brief Model for the QListView that shows all surahs to select from. */ diff --git a/src/dialogs/settingsdialog.cpp b/src/dialogs/settingsdialog.cpp index 68ae9f85..6cdb8f1c 100644 --- a/src/dialogs/settingsdialog.cpp +++ b/src/dialogs/settingsdialog.cpp @@ -13,11 +13,17 @@ SettingsDialog::SettingsDialog(QWidget* parent, VersePlayer* vPlayerPtr) : QDialog(parent) , ui(new Ui::SettingsDialog) , m_vPlayerPtr(vPlayerPtr) + , m_config(Configuration::getInstance()) + , m_downloadsDir(DirManager::getInstance().downloadsDir()) + , m_shortcutDescription(ShortcutHandler::getInstance().shortcutsDescription()) + , m_tafasir(Tafsir::tafasir) + , m_translations(Translation::translations) { ui->setupUi(this); ui->cmbQuranFontSz->setValidator(new QIntValidator(10, 72)); ui->cmbSideFontSz->setValidator(new QIntValidator(10, 72)); - setWindowIcon(StyleManager::awesome->icon(fa::fa_solid, fa::fa_gear)); + setWindowIcon( + StyleManager::getInstance().awesome().icon(fa::fa_solid, fa::fa_gear)); ui->tableViewShortcuts->setModel(&m_shortcutsModel); ui->tableViewShortcuts->horizontalHeader()->setStretchLastSection(true); ui->tableViewShortcuts->setItemDelegate(new ShortcutDelegate); @@ -48,7 +54,7 @@ SettingsDialog::populateShortcutsModel() QStandardItem *desc = new QStandardItem(), *keySeq = new QStandardItem(); desc->setData(key, Qt::UserRole); desc->setText(tr(m_shortcutDescription.value(key).toStdString().c_str())); - keySeq->setText(m_settings->value("Shortcuts/" + key).toString()); + keySeq->setText(m_config.settings().value("Shortcuts/" + key).toString()); QList row; row.append(desc); @@ -70,32 +76,34 @@ SettingsDialog::updateContentCombobox() { ui->cmbTranslation->clear(); for (int i = 0; i < m_translations.size(); i++) { - const QSharedPointer& tr = m_translations[i]; - if (tr->isAvailable()) - ui->cmbTranslation->addItem(tr->displayName(), i); + const Translation& tr = m_translations[i]; + if (tr.isAvailable()) + ui->cmbTranslation->addItem(tr.displayName(), i); } - m_translation = - ui->cmbTranslation->findData(m_settings->value("Reader/Translation")); + m_translation = ui->cmbTranslation->findData( + m_config.settings().value("Reader/Translation")); ui->cmbTranslation->setCurrentIndex(m_translation); } void SettingsDialog::setCurrentSettingsAsRef() { - m_votd = m_settings->value("VOTD").toBool(); - m_fgHighlight = m_settings->value("Reader/FGHighlight").toBool(); - m_missingFileWarning = m_settings->value("MissingFileWarning").toBool(); + m_votd = m_config.settings().value("VOTD").toBool(); + m_fgHighlight = m_config.settings().value("Reader/FGHighlight").toBool(); + m_missingFileWarning = + m_config.settings().value("MissingFileWarning").toBool(); - m_adaptive = m_settings->value("Reader/AdaptiveFont").toBool(); + m_adaptive = m_config.settings().value("Reader/AdaptiveFont").toBool(); m_quranFontSize = - m_settings->value("Reader/QCF" + QString::number(m_qcfVer) + "Size") + m_config.settings() + .value("Reader/QCF" + QString::number(m_config.qcfVersion()) + "Size") .toInt(); m_sideFont = - qvariant_cast(m_settings->value("Reader/SideContentFont")); + qvariant_cast(m_config.settings().value("Reader/SideContentFont")); - m_verseType = m_settings->value("Reader/VerseType").toInt(); - m_verseFontSize = m_settings->value("Reader/VerseFontSize").toInt(); + m_verseType = m_config.settings().value("Reader/VerseType").toInt(); + m_verseFontSize = m_config.settings().value("Reader/VerseFontSize").toInt(); m_audioDevices = QMediaDevices::audioOutputs(); ui->cmbAudioDevices->clear(); @@ -107,10 +115,10 @@ SettingsDialog::setCurrentSettingsAsRef() // set ui elements to current settings ui->cmbAudioDevices->setCurrentIndex(m_audioOutIdx); - ui->cmbLang->setCurrentIndex(ui->cmbLang->findData(m_languageCode)); - ui->cmbTheme->setCurrentIndex(m_themeIdx); - ui->cmbReaderMode->setCurrentIndex(m_readerMode); - ui->cmbQCF->setCurrentIndex(m_qcfVer - 1); + ui->cmbLang->setCurrentIndex(ui->cmbLang->findData(m_config.language())); + ui->cmbTheme->setCurrentIndex(m_config.themeId()); + ui->cmbReaderMode->setCurrentIndex(m_config.readerMode()); + ui->cmbQCF->setCurrentIndex(m_config.qcfVersion() - 1); ui->cmbQuranFontSz->setCurrentText(QString::number(m_quranFontSize)); ui->fntCmbSide->setCurrentFont(m_sideFont); ui->cmbSideFontSz->setCurrentText(QString::number(m_sideFont.pointSize())); @@ -135,7 +143,7 @@ SettingsDialog::checkShortcuts() const QString& key = m_shortcutsModel.item(row, 0)->data(Qt::UserRole).toString(); const QString& keySequence = m_shortcutsModel.item(row, 1)->text(); - if (m_settings->value("Shortcuts/" + key).toString() != keySequence) + if (m_config.settings().value("Shortcuts/" + key).toString() != keySequence) updateShortcut(key, keySequence); } } @@ -143,7 +151,7 @@ SettingsDialog::checkShortcuts() void SettingsDialog::updateTheme(int themeIdx) { - m_settings->setValue("Theme", themeIdx); + m_config.settings().setValue("Theme", themeIdx); if (m_restartReq) return; @@ -158,7 +166,7 @@ SettingsDialog::updateTheme(int themeIdx) void SettingsDialog::updateLang(QLocale::Language lang) { - m_settings->setValue("Language", lang); + m_config.settings().setValue("Language", lang); if (m_restartReq) return; @@ -173,19 +181,19 @@ SettingsDialog::updateLang(QLocale::Language lang) void SettingsDialog::updateDailyVerse(bool on) { - m_settings->setValue("VOTD", on); + m_config.settings().setValue("VOTD", on); } void SettingsDialog::updateFileWarning(bool on) { - m_settings->setValue("MissingFileWarning", on); + m_config.settings().setValue("MissingFileWarning", on); } void SettingsDialog::updateTranslation(int idx) { - m_settings->setValue("Reader/Translation", idx); + m_config.settings().setValue("Reader/Translation", idx); emit translationChanged(); m_renderSideContent = true; @@ -194,7 +202,7 @@ SettingsDialog::updateTranslation(int idx) void SettingsDialog::updateReaderMode(int idx) { - m_settings->setValue("Reader/Mode", idx); + m_config.settings().setValue("Reader/Mode", idx); if (m_restartReq) return; @@ -207,13 +215,13 @@ SettingsDialog::updateReaderMode(int idx) void SettingsDialog::updateQuranFont(int qcfV) { - if (qcfV == 2 && !FontManager::qcfExists()) { + if (qcfV == 2 && !FontManager::getInstance().getInstance().qcfExists()) { emit qcf2Missing(); ui->cmbQCF->setCurrentIndex(0); return; } - m_settings->setValue("Reader/QCF", qcfV); + m_config.settings().setValue("Reader/QCF", qcfV); if (m_restartReq) return; @@ -228,13 +236,14 @@ SettingsDialog::updateQuranFont(int qcfV) void SettingsDialog::updateAdaptiveFont(bool on) { - m_settings->setValue("Reader/AdaptiveFont", on); + m_config.settings().setValue("Reader/AdaptiveFont", on); } void SettingsDialog::updateQuranFontSize(QString size) { - m_settings->setValue("Reader/QCF" + QString::number(m_qcfVer) + "Size", size); + m_config.settings().setValue( + "Reader/QCF" + QString::number(m_config.qcfVersion()) + "Size", size); emit quranFontChanged(); m_renderQuranPage = true; } @@ -242,7 +251,7 @@ SettingsDialog::updateQuranFontSize(QString size) void SettingsDialog::updateFgHighlight(bool on) { - m_settings->setValue("Reader/FGHighlight", on); + m_config.settings().setValue("Reader/FGHighlight", on); emit highlightLayerChanged(); } @@ -252,7 +261,7 @@ SettingsDialog::updateSideFont(QFont fnt) fnt.setPointSize(m_sideFont.pointSize()); m_sideFont = fnt; - m_settings->setValue("Reader/SideContentFont", m_sideFont); + m_config.settings().setValue("Reader/SideContentFont", m_sideFont); emit sideFontChanged(); m_renderSideContent = true; } @@ -261,7 +270,7 @@ void SettingsDialog::updateSideFontSize(QString size) { m_sideFont.setPointSize(size.toInt()); - m_settings->setValue("Reader/SideContentFont", m_sideFont); + m_config.settings().setValue("Reader/SideContentFont", m_sideFont); emit sideFontChanged(); m_renderSideContent = true; } @@ -269,7 +278,7 @@ SettingsDialog::updateSideFontSize(QString size) void SettingsDialog::updateVerseType(int vt) { - m_settings->setValue("Reader/VerseType", vt); + m_config.settings().setValue("Reader/VerseType", vt); emit verseTypeChanged(); m_renderSideContent = true; } @@ -277,7 +286,7 @@ SettingsDialog::updateVerseType(int vt) void SettingsDialog::updateVerseFontsize(QString size) { - m_settings->setValue("Reader/VerseFontSize", size); + m_config.settings().setValue("Reader/VerseFontSize", size); emit verseTypeChanged(); m_renderSideContent = true; } @@ -285,7 +294,7 @@ SettingsDialog::updateVerseFontsize(QString size) void SettingsDialog::updateShortcut(QString key, QString keySequence) { - m_settings->setValue("Shortcuts/" + key, keySequence); + m_config.settings().setValue("Shortcuts/" + key, keySequence); emit shortcutChanged(key); } @@ -294,11 +303,11 @@ SettingsDialog::applyAllChanges() { QLocale::Language chosenLang = qvariant_cast(ui->cmbLang->currentData()); - if (chosenLang != m_languageCode) { + if (chosenLang != m_config.language()) { updateLang(chosenLang); } - if (ui->cmbTheme->currentIndex() != m_themeIdx) + if (ui->cmbTheme->currentIndex() != m_config.themeId()) updateTheme(ui->cmbTheme->currentIndex()); if (ui->chkDailyVerse->isChecked() != m_votd) @@ -313,7 +322,7 @@ SettingsDialog::applyAllChanges() if (ui->cmbTranslation->currentIndex() != m_translation) updateTranslation(ui->cmbTranslation->currentData().toInt()); - if (ui->cmbQCF->currentIndex() + 1 != m_qcfVer) + if (ui->cmbQCF->currentIndex() + 1 != m_config.qcfVersion()) updateQuranFont(ui->cmbQCF->currentIndex() + 1); if (ui->cmbVerseText->currentIndex() != m_verseType) @@ -325,7 +334,7 @@ SettingsDialog::applyAllChanges() if (ui->chkAdaptive->isChecked() != m_adaptive) updateAdaptiveFont(ui->chkAdaptive->isChecked()); - if (ui->cmbReaderMode->currentIndex() != m_readerMode) + if (ui->cmbReaderMode->currentIndex() != m_config.readerMode()) updateReaderMode(ui->cmbReaderMode->currentIndex()); bool forceManualFont = false; diff --git a/src/dialogs/settingsdialog.h b/src/dialogs/settingsdialog.h index 37e388fe..99730069 100644 --- a/src/dialogs/settingsdialog.h +++ b/src/dialogs/settingsdialog.h @@ -25,12 +25,12 @@ #include #include #include +#include #include #include -#include #include #include -typedef Settings::ReaderMode ReaderMode; +typedef Configuration::ReaderMode ReaderMode; namespace Ui { class SettingsDialog; @@ -218,17 +218,11 @@ public slots: private: Ui::SettingsDialog* ui; - const int m_qcfVer = Settings::qcfVersion; - const int m_themeIdx = Settings::themeId; - const ReaderMode m_readerMode = Settings::readerMode; - const QLocale::Language m_languageCode = Settings::language; - QSharedPointer const m_settings = Settings::settings; - const QDir& m_downloadsDir = *DirManager::downloadsDir; - const QList>& m_tafasir = Tafsir::tafasir; - const QList>& m_translations = - Translation::translations; - const QMap& m_shortcutDescription = - ShortcutHandler::shortcutsDescription; + Configuration& m_config; + const QDir& m_downloadsDir; + const QList& m_tafasir; + const QList& m_translations; + const QMap& m_shortcutDescription; /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/dialogs/versedialog.cpp b/src/dialogs/versedialog.cpp index f651c95d..a66a9c9f 100644 --- a/src/dialogs/versedialog.cpp +++ b/src/dialogs/versedialog.cpp @@ -4,16 +4,21 @@ VerseDialog::VerseDialog(QWidget* parent) : QDialog(parent) , ui(new Ui::VerseDialog) + , m_config(Configuration::getInstance()) + , m_quranDb(QuranDb::getInstance()) + , m_translationDb(TranslationDb::getInstance()) + , m_timestampFile( + DirManager::getInstance().configDir().absoluteFilePath("votd.log")) { ui->setupUi(this); QFont sideFont = - qvariant_cast(m_settings->value("Reader/SideContentFont")); + qvariant_cast(m_config.settings().value("Reader/SideContentFont")); ui->lbContent->setFont(sideFont); ui->lbInfo->setFont(sideFont); ui->lbVerse->setFont(QFont("kfgqpc_hafs_uthmanic _script", 20)); - if (m_settings->value("VOTD").toBool()) + if (m_config.settings().value("VOTD").toBool()) showVOTD(true); } @@ -47,7 +52,7 @@ VerseDialog::votdShown() void VerseDialog::genVerseOfTheDay() { - m_votd = m_quranDb->randomVerse(); + m_votd = m_quranDb.randomVerse(); } void @@ -82,11 +87,11 @@ void VerseDialog::updateLabels() { ui->lbVerse->setText( - "ﵩ " + m_quranDb->verseText(m_votd.surah(), m_votd.number()) + " ﵨ"); + "ﵩ " + m_quranDb.verseText(m_votd.surah(), m_votd.number()) + " ﵨ"); ui->lbContent->setText( - m_translationDb->getTranslation(m_votd.surah(), m_votd.number())); + m_translationDb.getTranslation(m_votd.surah(), m_votd.number())); ui->lbInfo->setText(qApp->translate("BookmarksDialog", "Surah: ") + - m_quranDb->surahName(m_votd.surah()) + " - " + + m_quranDb.surahName(m_votd.surah()) + " - " + qApp->translate("BookmarksDialog", "Verse: ") + QString::number(m_votd.number())); } diff --git a/src/dialogs/versedialog.h b/src/dialogs/versedialog.h index 7ed39791..890990c0 100644 --- a/src/dialogs/versedialog.h +++ b/src/dialogs/versedialog.h @@ -35,10 +35,10 @@ public slots: private: Ui::VerseDialog* ui; - QSharedPointer m_quranDb = QuranDb::current(); - QSharedPointer m_translationDb = TranslationDb::current(); - const QSharedPointer m_settings = Settings::settings; - QFile m_timestampFile = DirManager::configDir->absoluteFilePath("votd.log"); + Configuration& m_config; + const QuranDb& m_quranDb; + const TranslationDb& m_translationDb; + QFile m_timestampFile; /** * @brief generate the verse of the day and set the votd html */ diff --git a/src/downloader/contentjob.cpp b/src/downloader/contentjob.cpp index 89865c0d..9f1be23d 100644 --- a/src/downloader/contentjob.cpp +++ b/src/downloader/contentjob.cpp @@ -5,6 +5,8 @@ ContentJob::ContentJob(Type type, int idx) : m_idx(idx) , m_type(type) + , m_tafasir(Tafsir::tafasir) + , m_translations(Translation::translations) , m_isDownloading(false) , m_taskDlr(this) { @@ -73,8 +75,8 @@ QString ContentJob::name() { return m_type == DownloadJob::TafsirFile - ? m_tafasir.at(m_idx)->displayName() - : m_translations.at(m_idx)->displayName(); + ? m_tafasir.at(m_idx).displayName() + : m_translations.at(m_idx).displayName(); } ContentJob::~ContentJob() diff --git a/src/downloader/contentjob.h b/src/downloader/contentjob.h index 7cd82873..1cb5debf 100644 --- a/src/downloader/contentjob.h +++ b/src/downloader/contentjob.h @@ -25,9 +25,8 @@ class ContentJob : public DownloadJob void fileFound(); private: - QList>& m_tafasir = Tafsir::tafasir; - QList>& m_translations = - Translation::translations; + QList& m_tafasir; + QList& m_translations; TaskDownloader m_taskDlr; QNetworkAccessManager m_netMgr; DownloadTask* m_task; diff --git a/src/downloader/qcftask.cpp b/src/downloader/qcftask.cpp index 78f53e81..e6264be8 100644 --- a/src/downloader/qcftask.cpp +++ b/src/downloader/qcftask.cpp @@ -33,6 +33,6 @@ QcfTask::url() const QFileInfo QcfTask::destination() const { - return QFileInfo(m_downloadsDir->absoluteFilePath( + return QFileInfo(m_downloadsDir.absoluteFilePath( "QCFV2/QCF2" + QString::number(m_page).rightJustified(3, '0') + ".ttf")); } diff --git a/src/downloader/qcftask.h b/src/downloader/qcftask.h index dfa138db..caa8c321 100644 --- a/src/downloader/qcftask.h +++ b/src/downloader/qcftask.h @@ -17,7 +17,7 @@ class QcfTask : public DownloadTask QFileInfo destination() const override; private: - QSharedPointer m_downloadsDir = DirManager::downloadsDir; + const QDir& m_downloadsDir = DirManager::getInstance().downloadsDir(); int m_page; }; diff --git a/src/downloader/recitationtask.cpp b/src/downloader/recitationtask.cpp index 27810611..57303060 100644 --- a/src/downloader/recitationtask.cpp +++ b/src/downloader/recitationtask.cpp @@ -42,7 +42,7 @@ RecitationTask::operator=(RecitationTask other) QUrl RecitationTask::url() const { - const Reciter& r = *m_reciters.at(m_reciter); + const Reciter& r = m_reciters.at(m_reciter); QString url = r.baseUrl(); if (r.useId()) url.append(QString::number(Verse::id(m_surah, m_verse)) + ".mp3"); @@ -57,8 +57,8 @@ QFileInfo RecitationTask::destination() const { static const QString path = "recitations/%0/%1.mp3"; - return QFileInfo(m_downloadsDir->absoluteFilePath( - path.arg(m_reciters.at(m_reciter)->baseDirName(), + return QFileInfo(m_downloadsDir.absoluteFilePath( + path.arg(m_reciters.at(m_reciter).baseDirName(), QString::number(m_surah).rightJustified(3, '0') + QString::number(m_verse).rightJustified(3, '0')))); } diff --git a/src/downloader/recitationtask.h b/src/downloader/recitationtask.h index 153c86d0..b5961bac 100644 --- a/src/downloader/recitationtask.h +++ b/src/downloader/recitationtask.h @@ -21,9 +21,9 @@ class RecitationTask : public DownloadTask QFileInfo destination() const override; private: - QSharedPointer m_quranDb = QuranDb::current(); - QSharedPointer m_downloadsDir = DirManager::downloadsDir; - const QList>& m_reciters = Reciter::reciters; + const QuranDb& m_quranDb = QuranDb::getInstance(); + const QDir& m_downloadsDir = DirManager::getInstance().downloadsDir(); + const QList& m_reciters = Reciter::reciters; int m_reciter; int m_surah; int m_verse; diff --git a/src/downloader/surahjob.cpp b/src/downloader/surahjob.cpp index a0a54654..d6cfa448 100644 --- a/src/downloader/surahjob.cpp +++ b/src/downloader/surahjob.cpp @@ -8,6 +8,8 @@ SurahJob::SurahJob(int reciter, int surah) , m_surahCount(Verse::surahVerseCount(surah)) , m_isDownloading(false) , m_taskDlr(this) + , m_quranDb(QuranDb::getInstance()) + , m_reciters(Reciter::reciters) { connect( &m_taskDlr, &TaskDownloader::completed, this, &SurahJob::taskFinished); @@ -118,8 +120,8 @@ SurahJob::type() QString SurahJob::name() { - return m_reciters.at(m_reciter)->displayName() + " - " + - m_quranDb->surahNames().at(m_surah - 1); + return m_reciters.at(m_reciter).displayName() + " - " + + m_quranDb.surahNames().at(m_surah - 1); } int diff --git a/src/downloader/surahjob.h b/src/downloader/surahjob.h index 21ee5a98..f608a3d9 100644 --- a/src/downloader/surahjob.h +++ b/src/downloader/surahjob.h @@ -32,8 +32,8 @@ private slots: void taskFailed(); private: - QSharedPointer m_quranDb = QuranDb::current(); - QList>& m_reciters = Reciter::reciters; + const QuranDb& m_quranDb; + QList& m_reciters; TaskDownloader m_taskDlr; QQueue m_queue; QNetworkAccessManager m_netMgr; diff --git a/src/downloader/tafsirtask.cpp b/src/downloader/tafsirtask.cpp index 9ce52fe3..1f6c6499 100644 --- a/src/downloader/tafsirtask.cpp +++ b/src/downloader/tafsirtask.cpp @@ -27,13 +27,13 @@ TafsirTask::url() const { return QUrl::fromEncoded( ("https://github.com/0xzer0x/quran-companion/raw/main/extras/tafasir/" + - m_tafasir.at(m_idx)->filename()) + m_tafasir.at(m_idx).filename()) .toLatin1()); } QFileInfo TafsirTask::destination() const { - return QFileInfo(m_downloadsDir->absoluteFilePath( - "tafasir/" + m_tafasir.at(m_idx)->filename())); + return QFileInfo(m_downloadsDir.absoluteFilePath( + "tafasir/" + m_tafasir.at(m_idx).filename())); } diff --git a/src/downloader/tafsirtask.h b/src/downloader/tafsirtask.h index 924c956e..7da71e8e 100644 --- a/src/downloader/tafsirtask.h +++ b/src/downloader/tafsirtask.h @@ -18,8 +18,8 @@ class TafsirTask : public DownloadTask QFileInfo destination() const override; private: - QSharedPointer m_downloadsDir = DirManager::downloadsDir; - const QList>& m_tafasir = Tafsir::tafasir; + const QDir& m_downloadsDir = DirManager::getInstance().downloadsDir(); + const QList& m_tafasir = Tafsir::tafasir; int m_idx; }; diff --git a/src/downloader/translationtask.cpp b/src/downloader/translationtask.cpp index 1e405430..4ca4fc35 100644 --- a/src/downloader/translationtask.cpp +++ b/src/downloader/translationtask.cpp @@ -27,13 +27,13 @@ TranslationTask::url() const { return QUrl::fromEncoded(("https://github.com/0xzer0x/quran-companion/raw/" "main/extras/translations/" + - m_translations.at(m_idx)->filename()) + m_translations.at(m_idx).filename()) .toLatin1()); } QFileInfo TranslationTask::destination() const { - return QFileInfo(m_downloadsDir->absoluteFilePath( - "translations/" + m_translations.at(m_idx)->filename())); + return QFileInfo(m_downloadsDir.absoluteFilePath( + "translations/" + m_translations.at(m_idx).filename())); } diff --git a/src/downloader/translationtask.h b/src/downloader/translationtask.h index 581cc3e8..7e9f3dff 100644 --- a/src/downloader/translationtask.h +++ b/src/downloader/translationtask.h @@ -18,8 +18,8 @@ class TranslationTask : public DownloadTask QFileInfo destination() const override; private: - QSharedPointer m_downloadsDir = DirManager::downloadsDir; - const QList>& m_translations = + const QDir& m_downloadsDir = DirManager::getInstance().downloadsDir(); + const QList& m_translations = Translation::translations; int m_idx; }; diff --git a/src/main.cpp b/src/main.cpp index 7021fd4d..09468453 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,10 +9,10 @@ #include #include #include +#include #include #include #include -#include #include #include @@ -33,16 +33,14 @@ main(int argc, char* argv[]) QSplashScreen splash(QPixmap(":/resources/splash.png")); splash.show(); - DirManager::setup(); - Logger::startLogger(DirManager::configDir->absolutePath()); + Logger::startLogger(DirManager::getInstance().configDir().absolutePath()); Logger::attach(); - Settings::setup(); - ShortcutHandler::populateDescriptionMap(); - StyleManager::loadTheme(); - FontManager::loadUiFonts(); - FontManager::loadQcf(); - Settings::loadQmFiles(); + Configuration::getInstance().checkGroups(); + Configuration::getInstance().loadUiTranslation(); + ShortcutHandler::getInstance().populateDescriptionMap(); + StyleManager::getInstance().loadTheme(); + FontManager::getInstance().loadFonts(); Tafsir::populateTafasir(); Translation::populateTranslations(); diff --git a/src/types/reciter.cpp b/src/types/reciter.cpp index e3a1e593..f282ec84 100644 --- a/src/types/reciter.cpp +++ b/src/types/reciter.cpp @@ -4,7 +4,7 @@ #include #include -QList> Reciter::reciters; +QList Reciter::reciters; void Reciter::populateReciters() @@ -22,12 +22,13 @@ Reciter::populateReciters() QString displayName = qApp->translate( "MainWindow", reader.attributes().value("display").toLatin1()); QString baseUrl = reader.attributes().value("url").toString(); - QString basmallahPath = DirManager::basmallahDir->absoluteFilePath( - reader.attributes().value("basmallah").toString()); + QString basmallahPath = + DirManager::getInstance().basmallahDir().absoluteFilePath( + reader.attributes().value("basmallah").toString()); bool useId = reader.attributes().value("useid").toInt(); - reciters.append(QSharedPointer::create( - baseDirName, displayName, basmallahPath, baseUrl, useId)); + reciters.append( + Reciter(baseDirName, displayName, basmallahPath, baseUrl, useId)); } } } @@ -36,12 +37,12 @@ Reciter::populateReciters() reciters.squeeze(); // create reciters directories - DirManager::downloadsDir->cd("recitations"); - foreach (const QSharedPointer& r, reciters) { - if (!DirManager::downloadsDir->exists(r->baseDirName())) - DirManager::downloadsDir->mkdir(r->baseDirName()); + QString temp = "recitations/%0"; + foreach (const Reciter& r, reciters) { + QString path = temp.arg(r.baseDirName()); + if (!DirManager::getInstance().downloadsDir().exists(path)) + DirManager::getInstance().downloadsDir().mkdir(path); } - DirManager::downloadsDir->cdUp(); } Reciter::Reciter(QString dir, diff --git a/src/types/reciter.h b/src/types/reciter.h index 132e3f72..b3c30bfd 100644 --- a/src/types/reciter.h +++ b/src/types/reciter.h @@ -12,8 +12,8 @@ class Reciter { public: - static QList> reciters; - static void populateReciters(); + static QList reciters; + static void populateReciters(); explicit Reciter(QString dir, QString display, diff --git a/src/types/tafsir.cpp b/src/types/tafsir.cpp index be02ac1c..e6f62023 100644 --- a/src/types/tafsir.cpp +++ b/src/types/tafsir.cpp @@ -6,7 +6,7 @@ #include #include -QList> Tafsir::tafasir; +QList Tafsir::tafasir; void Tafsir::populateTafasir() @@ -25,8 +25,7 @@ Tafsir::populateTafasir() QString file = reader.attributes().value("file").toString(); bool isText = reader.attributes().value("text").toInt(); bool isExtra = reader.attributes().value("extra").toInt(); - tafasir.append( - QSharedPointer::create(name, file, isText, isExtra)); + tafasir.append(Tafsir(name, file, isText, isExtra)); } } } @@ -41,11 +40,23 @@ Tafsir::Tafsir(QString display, QString filename, bool isText, bool isExtra) { } +bool +Tafsir::operator==(const Tafsir& v2) const +{ + return this->filename() == v2.filename(); +} + +bool +Tafsir::operator!=(const Tafsir& v2) const +{ + return this->filename() != v2.filename(); +} + bool Tafsir::isAvailable() const { - const QDir& baseDir = - isExtra() ? *DirManager::downloadsDir : *DirManager::assetsDir; + const QDir& baseDir = isExtra() ? DirManager::getInstance().downloadsDir() + : DirManager::getInstance().assetsDir(); return baseDir.exists("tafasir/" + filename()); } diff --git a/src/types/tafsir.h b/src/types/tafsir.h index 8455b494..05c00e93 100644 --- a/src/types/tafsir.h +++ b/src/types/tafsir.h @@ -10,7 +10,10 @@ class Tafsir : public Content { public: static void populateTafasir(); - static QList> tafasir; + static QList tafasir; + + bool operator==(const Tafsir& v2) const; + bool operator!=(const Tafsir& v2) const; explicit Tafsir(QString display, QString filename, bool isText, bool isExtra); const bool isText() const; diff --git a/src/types/translation.cpp b/src/types/translation.cpp index 45238b0f..4d0236f1 100644 --- a/src/types/translation.cpp +++ b/src/types/translation.cpp @@ -5,7 +5,7 @@ #include #include -QList> Translation::translations; +QList Translation::translations; void Translation::populateTranslations() @@ -22,8 +22,7 @@ Translation::populateTranslations() QString name = reader.attributes().value("name").toString(); QString file = reader.attributes().value("file").toString(); bool extra = reader.attributes().value("extra").toInt(); - translations.append( - QSharedPointer::create(name, file, extra)); + translations.append(Translation(name, file, extra)); } } } @@ -37,10 +36,22 @@ Translation::Translation(QString display, QString filename, bool isExtra) { } +bool +Translation::operator==(const Translation& v2) const +{ + return this->filename() == v2.filename(); +} + +bool +Translation::operator!=(const Translation& v2) const +{ + return this->filename() != v2.filename(); +} + bool Translation::isAvailable() const { - const QDir& baseDir = - isExtra() ? *DirManager::downloadsDir : *DirManager::assetsDir; + const QDir& baseDir = isExtra() ? DirManager::getInstance().downloadsDir() + : DirManager::getInstance().assetsDir(); return baseDir.exists("translations/" + filename()); } diff --git a/src/types/translation.h b/src/types/translation.h index 1106bd3c..e4fd91d2 100644 --- a/src/types/translation.h +++ b/src/types/translation.h @@ -10,7 +10,10 @@ class Translation : public Content { public: static void populateTranslations(); - static QList> translations; + static QList translations; + + bool operator==(const Translation& v2) const; + bool operator!=(const Translation& v2) const; explicit Translation(QString display, QString filename, bool isExtra); bool isAvailable() const; diff --git a/src/types/verse.cpp b/src/types/verse.cpp index a8417a02..eacc0182 100644 --- a/src/types/verse.cpp +++ b/src/types/verse.cpp @@ -29,10 +29,10 @@ Verse::id(int surah, int verse) return id; } -QSharedPointer -Verse::current() +Verse& +Verse::getCurrent() { - static QSharedPointer current = QSharedPointer::create(1, 1, 1); + static Verse current(1, 1, 1); return current; } @@ -135,7 +135,7 @@ Verse::next(bool basmalah) return *this; } - Verse v(m_quranDb->verseById(id(m_surah, m_number) + 1)); + Verse v(m_quranDb.verseById(id(m_surah, m_number) + 1)); if (v.number() == 1 && v.surah() != 9 && v.surah() != 1 && basmalah) v.setNumber(0); @@ -154,7 +154,7 @@ Verse::prev(bool basmalah) if (!m_number) m_number = 1; - return Verse(m_quranDb->verseById(id(m_surah, m_number) - 1)); + return Verse(m_quranDb.verseById(id(m_surah, m_number) - 1)); } void diff --git a/src/types/verse.h b/src/types/verse.h index 4a01d40c..0586941f 100644 --- a/src/types/verse.h +++ b/src/types/verse.h @@ -3,7 +3,6 @@ #include #include -#include /** * @brief Verse class represents a single quran verse @@ -17,7 +16,7 @@ class Verse static const QList verseCount; static const int surahVerseCount(int surah); static int id(int surah, int verse); - static QSharedPointer current(); + static Verse& getCurrent(); static QList fromList(QList> lst); Verse(); @@ -47,8 +46,7 @@ class Verse void setNumber(int newNumber); private: - const QSharedPointer m_settings = Settings::settings; - QSharedPointer m_quranDb = QuranDb::current(); + const QuranDb& m_quranDb = QuranDb::getInstance(); int m_page = -1; ///< verse page int m_surah = -1; ///< verse surah number diff --git a/src/utils/configuration.cpp b/src/utils/configuration.cpp new file mode 100644 index 00000000..e26edba6 --- /dev/null +++ b/src/utils/configuration.cpp @@ -0,0 +1,125 @@ +#include "configuration.h" +#include "dirmanager.h" +#include +#include +#include +#include + +Configuration& +Configuration::getInstance() +{ + static Configuration config; + return config; +} + +Configuration::Configuration() + : m_settings( + DirManager::getInstance().configDir().filePath("qurancompanion.conf"), + QSettings::IniFormat) +{ + m_themeId = m_settings.value("Theme").toInt(); + m_qcfVersion = m_settings.value("Reader/QCF").toInt(); + m_language = qvariant_cast(m_settings.value("Language")); + m_readerMode = qvariant_cast(m_settings.value("Reader/Mode")); + m_darkMode = m_themeId == 2; +} + +void +Configuration::checkGroups() +{ + for (int i = 0; i < 2; i++) + checkConfGroup(i); +} + +void +Configuration::checkConfGroup(int gId) +{ + switch (gId) { + case 0: + m_settings.setValue("Language", + m_settings.value("Language", (int)QLocale::English)); + m_settings.setValue("Theme", m_settings.value("Theme", 0)); + m_settings.setValue("VOTD", m_settings.value("VOTD", true)); + m_settings.setValue("MissingFileWarning", + m_settings.value("MissingFileWarning", true)); + break; + case 1: + m_settings.beginGroup("Reader"); + m_settings.setValue("Mode", m_settings.value("Mode", 0)); + m_settings.setValue("FGHighlight", m_settings.value("FGHighlight", 1)); + m_settings.setValue("Khatmah", m_settings.value("Khatmah", 0)); + m_settings.setValue("AdaptiveFont", + m_settings.value("AdaptiveFont", true)); + m_settings.setValue("QCF1Size", m_settings.value("QCF1Size", 22)); + m_settings.setValue("QCF2Size", m_settings.value("QCF2Size", 20)); + m_settings.setValue("QCF", m_settings.value("QCF", 1)); + m_settings.setValue("VerseType", m_settings.value("VerseType", 0)); + m_settings.setValue("VerseFontSize", + m_settings.value("VerseFontSize", 20)); + m_settings.setValue("Tafsir", m_settings.value("Tafsir", 6)); + m_settings.setValue("Translation", m_settings.value("Translation", 5)); + m_settings.setValue( + "SideContentFont", + m_settings.value("SideContentFont", QFont("Expo Arabic", 14))); + m_settings.endGroup(); + break; + } +} + +void +Configuration::loadUiTranslation() +{ + if (m_language == QLocale::English) + return; + + QString code = QLocale::languageToCode(m_language); + QTranslator *translation = new QTranslator(qApp), + *qtBase = new QTranslator(qApp); + + if (translation->load(":/i18n/qc_" + code + ".qm")) { + qInfo() << translation->language() << "translation loaded"; + qInfo() << "base translation:" << qtBase->load(":/base/" + code + ".qm"); + qApp->installTranslator(translation); + qApp->installTranslator(qtBase); + } else { + qWarning() << code + " translation not loaded!"; + delete translation; + delete qtBase; + } +} + +QSettings& +Configuration::settings() +{ + return m_settings; +} + +int +Configuration::themeId() const +{ + return m_themeId; +} + +bool +Configuration::darkMode() const +{ + return m_darkMode; +} + +int +Configuration::qcfVersion() const +{ + return m_qcfVersion; +} + +QLocale::Language +Configuration::language() const +{ + return m_language; +} + +Configuration::ReaderMode +Configuration::readerMode() const +{ + return m_readerMode; +} diff --git a/src/utils/configuration.h b/src/utils/configuration.h new file mode 100644 index 00000000..00d1eb67 --- /dev/null +++ b/src/utils/configuration.h @@ -0,0 +1,51 @@ +#ifndef CONFIGURATION_H +#define CONFIGURATION_H + +#include +#include +#include + +class Configuration +{ +public: + enum VerseType + { + Qcf, + Uthmanic, + Annotated + }; + /** + * @brief ReaderMode enum represents the available modes for the Quran reader + * in MainWindow + */ + enum ReaderMode + { + SinglePage, ///< Single Quran page, side panel is used for displaying verses + ///< with translation + DoublePage ///< Two Quran pages, both panels are used to display Quran + ///< pages, no translation + }; + + static Configuration& getInstance(); + void checkConfGroup(int gId); + void loadUiTranslation(); + void checkGroups(); + + QSettings& settings(); + int themeId() const; + bool darkMode() const; + int qcfVersion() const; + QLocale::Language language() const; + ReaderMode readerMode() const; + +private: + Configuration(); + int m_themeId; + bool m_darkMode; + int m_qcfVersion; + QLocale::Language m_language; + QSettings m_settings; + ReaderMode m_readerMode; +}; + +#endif // CONFIGURATION_H diff --git a/src/utils/dirmanager.cpp b/src/utils/dirmanager.cpp index 38b713f3..310171e6 100644 --- a/src/utils/dirmanager.cpp +++ b/src/utils/dirmanager.cpp @@ -2,42 +2,103 @@ #include #include -QSharedPointer DirManager::fontsDir; -QSharedPointer DirManager::assetsDir; -QSharedPointer DirManager::basmallahDir; -QSharedPointer DirManager::downloadsDir = QSharedPointer::create( - QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); -QSharedPointer DirManager::configDir = QSharedPointer::create( - QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); +DirManager& +DirManager::getInstance() +{ + static DirManager dirmanager; + return dirmanager; +} + +DirManager::DirManager() +{ + m_downloadsDir.setPath( + QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)); + m_configDir.setPath( + QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation)); + m_assetsDir.setPath(QApplication::applicationDirPath() + QDir::separator() + + "assets"); + m_fontsDir.setPath(m_assetsDir.absoluteFilePath("fonts")); + m_basmallahDir.setPath(QApplication::applicationDirPath() + + QDir::separator() + "bismillah"); + + // config & downloads + if (!m_configDir.exists("QuranCompanion")) + m_configDir.mkpath("QuranCompanion"); + m_configDir.cd("QuranCompanion"); + + if (!m_downloadsDir.exists("QuranCompanion")) + m_downloadsDir.mkpath("QuranCompanion"); + m_downloadsDir.cd("QuranCompanion"); + + if (!m_downloadsDir.exists("recitations")) + m_downloadsDir.mkpath("recitations"); + + if (!m_downloadsDir.exists("QCFV2")) + m_downloadsDir.mkpath("QCFV2"); + + if (!m_downloadsDir.exists("tafasir")) + m_downloadsDir.mkpath("tafasir"); + + if (!m_downloadsDir.exists("translations")) + m_downloadsDir.mkpath("translations"); +} void -DirManager::setup() +DirManager::setFontsDir(const QDir& newFontsDir) { + m_fontsDir = newFontsDir; +} - assetsDir = QSharedPointer::create(QApplication::applicationDirPath() + - QDir::separator() + "assets"); - fontsDir = QSharedPointer::create(assetsDir->absoluteFilePath("fonts")); - basmallahDir = QSharedPointer::create( - QApplication::applicationDirPath() + QDir::separator() + "bismillah"); +void +DirManager::setConfigDir(const QDir& newConfigDir) +{ + m_configDir = newConfigDir; +} - // config & downloads - if (!configDir->exists("QuranCompanion")) - configDir->mkpath("QuranCompanion"); - configDir->cd("QuranCompanion"); +void +DirManager::setAssetsDir(const QDir& newAssetsDir) +{ + m_assetsDir = newAssetsDir; +} + +void +DirManager::setDownloadsDir(const QDir& newDownloadsDir) +{ + m_downloadsDir = newDownloadsDir; +} + +void +DirManager::setBasmallahDir(const QDir& newBasmallahDir) +{ + m_basmallahDir = newBasmallahDir; +} - if (!downloadsDir->exists("QuranCompanion")) - downloadsDir->mkpath("QuranCompanion"); - downloadsDir->cd("QuranCompanion"); +const QDir& +DirManager::fontsDir() const +{ + return m_fontsDir; +} - if (!downloadsDir->exists("recitations")) - downloadsDir->mkpath("recitations"); +const QDir& +DirManager::configDir() const +{ + return m_configDir; +} - if (!downloadsDir->exists("QCFV2")) - downloadsDir->mkpath("QCFV2"); +const QDir& +DirManager::assetsDir() const +{ + return m_assetsDir; +} - if (!downloadsDir->exists("tafasir")) - downloadsDir->mkpath("tafasir"); +const QDir& +DirManager::downloadsDir() const +{ + return m_downloadsDir; +} - if (!downloadsDir->exists("translations")) - downloadsDir->mkpath("translations"); +const QDir& +DirManager::basmallahDir() const +{ + return m_basmallahDir; } diff --git a/src/utils/dirmanager.h b/src/utils/dirmanager.h index 83209c9c..1575716c 100644 --- a/src/utils/dirmanager.h +++ b/src/utils/dirmanager.h @@ -7,12 +7,27 @@ class DirManager { public: - static void setup(); - static QSharedPointer fontsDir; - static QSharedPointer configDir; - static QSharedPointer assetsDir; - static QSharedPointer downloadsDir; - static QSharedPointer basmallahDir; + static DirManager& getInstance(); + + void setFontsDir(const QDir& newFontsDir); + void setConfigDir(const QDir& newConfigDir); + void setAssetsDir(const QDir& newAssetsDir); + void setDownloadsDir(const QDir& newDownloadsDir); + void setBasmallahDir(const QDir& newBasmallahDir); + + const QDir& fontsDir() const; + const QDir& configDir() const; + const QDir& assetsDir() const; + const QDir& downloadsDir() const; + const QDir& basmallahDir() const; + +private: + DirManager(); + QDir m_fontsDir; + QDir m_configDir; + QDir m_assetsDir; + QDir m_downloadsDir; + QDir m_basmallahDir; }; #endif // DIRMANAGER_H diff --git a/src/utils/fontmanager.cpp b/src/utils/fontmanager.cpp index 9ded1977..f78da54a 100644 --- a/src/utils/fontmanager.cpp +++ b/src/utils/fontmanager.cpp @@ -1,23 +1,41 @@ #include "fontmanager.h" -#include "dirmanager.h" +#include "configuration.h" #include #include -QString FontManager::qcfFontPrefix = "QCF_P"; +FontManager& +FontManager::getInstance() +{ + static FontManager fontmanager; + return fontmanager; +} + +FontManager::FontManager() + : m_dirMgr(DirManager::getInstance()) + , m_config(Configuration::getInstance()) +{ +} + +void +FontManager::loadFonts() +{ + loadUiFonts(); + loadQcf(); +} void FontManager::loadQcf() { QFontDatabase::addApplicationFont( - DirManager::fontsDir->filePath("QCFV1/QCF_BSML.ttf")); - switch (Settings::qcfVersion) { + m_dirMgr.fontsDir().filePath("QCFV1/QCF_BSML.ttf")); + switch (m_config.qcfVersion()) { case 1: - DirManager::fontsDir->cd("QCFV1"); + m_dirMgr.setFontsDir(m_dirMgr.fontsDir().absoluteFilePath("QCFV1")); + m_qcfFontPrefix = "QCF_P"; break; case 2: - DirManager::fontsDir->setPath(DirManager::downloadsDir->absolutePath() + - "/QCFV2"); - qcfFontPrefix = "QCF2"; + m_dirMgr.setFontsDir(m_dirMgr.downloadsDir().absolutePath() + "/QCFV2"); + m_qcfFontPrefix = "QCF2"; break; } @@ -25,14 +43,13 @@ FontManager::loadQcf() for (int i = 1; i < 605; i++) { QString fontName = pageFontname(i) + ".ttf"; - if (Settings::qcfVersion == 2 && !DirManager::fontsDir->exists(fontName)) { - Settings::settings->setValue("Reader/QCF", 1); - Settings::settings->sync(); - qFatal() << DirManager::fontsDir->filePath(fontName) + if (m_config.qcfVersion() == 2 && !m_dirMgr.fontsDir().exists(fontName)) { + m_config.settings().setValue("Reader/QCF", 1); + m_config.settings().sync(); + qFatal() << m_dirMgr.fontsDir().filePath(fontName) << " font file not found, fallback to QCF v1"; } else - QFontDatabase::addApplicationFont( - DirManager::fontsDir->filePath(fontName)); + QFontDatabase::addApplicationFont(m_dirMgr.fontsDir().filePath(fontName)); } } @@ -41,7 +58,7 @@ FontManager::loadUiFonts() { // ui fonts foreach (const QFileInfo& font, - DirManager::fontsDir->entryInfoList(QDir::Files)) + m_dirMgr.fontsDir().entryInfoList(QDir::Files)) QFontDatabase::addApplicationFont(font.absoluteFilePath()); // set default UI fonts to use QStringList uiFonts; @@ -50,39 +67,39 @@ FontManager::loadUiFonts() qApp->setFont(QFont(uiFonts, qApp->font().pointSize())); } +bool +FontManager::qcfExists() +{ + QString filename = "QCFV2/QCF2%0.ttf"; + for (int i = 1; i <= 604; i++) { + if (!m_dirMgr.downloadsDir().exists( + filename.arg(QString::number(i).rightJustified(3, '0')))) + return false; + } + + return true; +} + QString FontManager::pageFontname(int page) { - return qcfFontPrefix + QString::number(page).rightJustified(3, '0'); + return m_qcfFontPrefix + QString::number(page).rightJustified(3, '0'); } QString -FontManager::verseFontname(Settings::VerseType type, int page) +FontManager::verseFontname(Configuration::VerseType type, int page) { QString fontname; switch (type) { - case Settings::Qcf: + case Configuration::Qcf: fontname = pageFontname(page); break; - case Settings::Uthmanic: + case Configuration::Uthmanic: fontname = "kfgqpc_hafs_uthmanic _script"; break; - case Settings::Annotated: + case Configuration::Annotated: fontname = "Emine"; break; } return fontname; } - -bool -FontManager::qcfExists() -{ - QString filename = "QCFV2/QCF2%0.ttf"; - for (int i = 1; i <= 604; i++) { - if (!DirManager::downloadsDir->exists( - filename.arg(QString::number(i).rightJustified(3, '0')))) - return false; - } - - return true; -} diff --git a/src/utils/fontmanager.h b/src/utils/fontmanager.h index 8509eaf2..5cc2edc7 100644 --- a/src/utils/fontmanager.h +++ b/src/utils/fontmanager.h @@ -1,24 +1,26 @@ #ifndef FONTMANAGER_H #define FONTMANAGER_H -#include "settings.h" +#include "configuration.h" +#include "dirmanager.h" #include class FontManager { public: - static QString qcfFontPrefix; - static QString pageFontname(int page); - static QString verseFontname(Settings::VerseType type, int page); - static void loadQcf(); - static void loadUiFonts(); - /** - * @brief check if QCF2 font files exist - * - * @return true - all 604 QCF2 files are found - * @return false - one of the files is missing - */ - static bool qcfExists(); + static FontManager& getInstance(); + QString pageFontname(int page); + QString verseFontname(Configuration::VerseType type, int page); + void loadFonts(); + bool qcfExists(); + +private: + FontManager(); + void loadQcf(); + void loadUiFonts(); + Configuration& m_config; + DirManager& m_dirMgr; + QString m_qcfFontPrefix; }; #endif // FONTMANAGER_H diff --git a/src/utils/jsondataexporter.cpp b/src/utils/jsondataexporter.cpp index 6e415b96..f24cd63d 100644 --- a/src/utils/jsondataexporter.cpp +++ b/src/utils/jsondataexporter.cpp @@ -7,7 +7,7 @@ void JsonDataExporter::exportBookmarks() { QJsonArray bookmarks; - QList all = m_bookmarksDb->bookmarkedVerses(); + QList all = m_bookmarksDb.bookmarkedVerses(); foreach (const Verse& v, all) { bookmarks.append(verseJson(v)); } @@ -19,11 +19,11 @@ void JsonDataExporter::exportKhatmah() { QJsonArray khatmah; - QList ids = m_bookmarksDb->getAllKhatmah(); + QList ids = m_bookmarksDb.getAllKhatmah(); foreach (const int id, ids) { Verse v; - m_bookmarksDb->loadVerse(id, v); - QString name = m_bookmarksDb->getKhatmahName(id); + m_bookmarksDb.loadVerse(id, v); + QString name = m_bookmarksDb.getKhatmahName(id); khatmah.append(khatmahJson({ name, v })); } @@ -34,7 +34,7 @@ void JsonDataExporter::exportThoughts() { QJsonArray thoughts; - QList> all = m_bookmarksDb->allThoughts(); + QList> all = m_bookmarksDb.allThoughts(); for (const QPair& item : all) { thoughts.append(thoughtJson(item)); } diff --git a/src/utils/jsondataexporter.h b/src/utils/jsondataexporter.h index 2d4b93a0..05c42f79 100644 --- a/src/utils/jsondataexporter.h +++ b/src/utils/jsondataexporter.h @@ -18,7 +18,7 @@ class JsonDataExporter : public UserDataExporter bool save(); private: - QSharedPointer m_bookmarksDb = BookmarksDb::current(); + BookmarksDb& m_bookmarksDb = BookmarksDb::getInstance(); QJsonObject verseJson(const Verse& v); QJsonObject khatmahJson(const QPair& entry); QJsonObject thoughtJson(const QPair& entry); diff --git a/src/utils/jsondataimporter.cpp b/src/utils/jsondataimporter.cpp index 76dbbd43..a6a9cea9 100644 --- a/src/utils/jsondataimporter.cpp +++ b/src/utils/jsondataimporter.cpp @@ -12,8 +12,8 @@ JsonDataImporter::importBookmarks() QJsonArray arr = m_fileObj.value("bookmarks").toArray(); foreach (const QJsonValue& item, arr) { Verse bookmark = verseFromJson(item.toObject()); - if (!m_bookmarksDb->isBookmarked(bookmark)) - m_bookmarksDb->addBookmark(bookmark, true); + if (!m_bookmarksDb.isBookmarked(bookmark)) + m_bookmarksDb.addBookmark(bookmark, true); } } @@ -25,7 +25,7 @@ JsonDataImporter::importKhatmah() QJsonArray arr = m_fileObj.value("khatmah").toArray(); foreach (const QJsonValue& item, arr) { QPair khatmah = khatmahFromJson(item.toObject()); - m_bookmarksDb->addKhatmah(khatmah.second, khatmah.first); + m_bookmarksDb.addKhatmah(khatmah.second, khatmah.first); } } @@ -37,7 +37,7 @@ JsonDataImporter::importThoughts() QJsonArray arr = m_fileObj.value("thoughts").toArray(); foreach (const QJsonValue& item, arr) { QPair thought = thoughtFromJson(item.toObject()); - m_bookmarksDb->saveThoughts(thought.first, thought.second); + m_bookmarksDb.saveThoughts(thought.first, thought.second); } } diff --git a/src/utils/jsondataimporter.h b/src/utils/jsondataimporter.h index 434a1ed7..2ba9158b 100644 --- a/src/utils/jsondataimporter.h +++ b/src/utils/jsondataimporter.h @@ -19,7 +19,7 @@ class JsonDataImporter : public UserDataImporter bool read() override; private: - QSharedPointer m_bookmarksDb = BookmarksDb::current(); + BookmarksDb& m_bookmarksDb = BookmarksDb::getInstance(); bool validArray(const QString key); bool validVerse(const QJsonObject& obj); bool validKhatmah(const QJsonObject& obj); diff --git a/src/utils/settings.cpp b/src/utils/settings.cpp deleted file mode 100644 index 0e5a8797..00000000 --- a/src/utils/settings.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#include "settings.h" -#include -#include -#include -#include -#include - -int Settings::themeId = 0; -bool Settings::darkMode = false; -int Settings::qcfVersion = 1; -Settings::ReaderMode Settings::readerMode; -QLocale::Language Settings::language; -QSharedPointer Settings::settings; - -void -Settings::setup() -{ - settings = QSharedPointer::create( - DirManager::configDir->filePath("qurancompanion.conf"), - QSettings::IniFormat); - - themeId = settings->value("Theme").toInt(); - qcfVersion = settings->value("Reader/QCF").toInt(); - language = qvariant_cast(settings->value("Language")); - readerMode = qvariant_cast(settings->value("Reader/Mode")); - - checkGroups(); -} - -void -Settings::checkGroups() -{ - for (int i = 0; i < 2; i++) - checkConfGroup(i); -} - -void -Settings::checkConfGroup(int gId) -{ - switch (gId) { - case 0: - settings->setValue("Language", - settings->value("Language", (int)QLocale::English)); - settings->setValue("Theme", settings->value("Theme", 0)); - settings->setValue("VOTD", settings->value("VOTD", true)); - settings->setValue("MissingFileWarning", - settings->value("MissingFileWarning", true)); - break; - case 1: - settings->beginGroup("Reader"); - settings->setValue("Mode", settings->value("Mode", 0)); - settings->setValue("FGHighlight", settings->value("FGHighlight", 1)); - settings->setValue("Khatmah", settings->value("Khatmah", 0)); - settings->setValue("AdaptiveFont", settings->value("AdaptiveFont", true)); - settings->setValue("QCF1Size", settings->value("QCF1Size", 22)); - settings->setValue("QCF2Size", settings->value("QCF2Size", 20)); - settings->setValue("QCF", settings->value("QCF", 1)); - settings->setValue("VerseType", settings->value("VerseType", 0)); - settings->setValue("VerseFontSize", settings->value("VerseFontSize", 20)); - settings->setValue("Tafsir", settings->value("Tafsir", 6)); - settings->setValue("Translation", settings->value("Translation", 5)); - settings->setValue( - "SideContentFont", - settings->value("SideContentFont", QFont("Expo Arabic", 14))); - settings->endGroup(); - break; - } -} - -void -Settings::loadQmFiles() -{ - if (language == QLocale::English) - return; - - QString code = QLocale::languageToCode(language); - QTranslator *translation = new QTranslator(qApp), - *qtBase = new QTranslator(qApp); - - if (translation->load(":/i18n/qc_" + code + ".qm")) { - qInfo() << translation->language() << "translation loaded"; - qInfo() << "base translation:" << qtBase->load(":/base/" + code + ".qm"); - qApp->installTranslator(translation); - qApp->installTranslator(qtBase); - } else { - qWarning() << code + " translation not loaded!"; - delete translation; - delete qtBase; - } -} diff --git a/src/utils/settings.h b/src/utils/settings.h deleted file mode 100644 index 532cd506..00000000 --- a/src/utils/settings.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef SETTINGS_H -#define SETTINGS_H - -#include -#include -#include - -class Settings -{ -public: - enum VerseType - { - Qcf, - Uthmanic, - Annotated - }; - /** - * @brief ReaderMode enum represents the available modes for the Quran reader - * in MainWindow - */ - enum ReaderMode - { - SinglePage, ///< Single Quran page, side panel is used for displaying verses - ///< with translation - DoublePage ///< Two Quran pages, both panels are used to display Quran - ///< pages, no translation - }; - - static int themeId; - static bool darkMode; - static int qcfVersion; - static QLocale::Language language; - static QSharedPointer settings; - static ReaderMode readerMode; - static void setup(); - static void checkGroups(); - static void checkConfGroup(int gId); - static void loadQmFiles(); -}; - -#endif // SETTINGS_H diff --git a/src/utils/shortcuthandler.cpp b/src/utils/shortcuthandler.cpp index c1f36f8d..825238de 100644 --- a/src/utils/shortcuthandler.cpp +++ b/src/utils/shortcuthandler.cpp @@ -7,10 +7,19 @@ #include #include #include -#include using std::make_pair; -QMap ShortcutHandler::shortcutsDescription; +ShortcutHandler& +ShortcutHandler::getInstance() +{ + static ShortcutHandler handler; + return handler; +} + +ShortcutHandler::ShortcutHandler() + : m_config(Configuration::getInstance()) +{ +} void ShortcutHandler::populateDescriptionMap() @@ -19,7 +28,7 @@ ShortcutHandler::populateDescriptionMap() if (!shortcuts.open(QIODevice::ReadOnly)) qCritical("Couldn't Open Shortcuts XML"); - Settings::settings->beginGroup("Shortcuts"); + m_config.settings().beginGroup("Shortcuts"); QXmlStreamReader reader(&shortcuts); while (!reader.atEnd() && !reader.hasError()) { QXmlStreamReader::TokenType token = reader.readNext(); @@ -31,36 +40,23 @@ ShortcutHandler::populateDescriptionMap() qApp->translate("SettingsDialog", reader.attributes().value("description").toLatin1()); - shortcutsDescription.insert(key, desc); - if (!Settings::settings->contains(key)) - Settings::settings->setValue(key, defBind); + m_shortcutsDescription.insert(key, desc); + if (!m_config.settings().contains(key)) + m_config.settings().setValue(key, defBind); } } } - Settings::settings->endGroup(); + m_config.settings().endGroup(); shortcuts.close(); } -QSharedPointer -ShortcutHandler::current() -{ - static QSharedPointer handler = - QSharedPointer::create(); - return handler; -} - -ShortcutHandler::ShortcutHandler(QObject* parent) - : QObject(parent) -{ -} - void ShortcutHandler::createShortcuts(QObject* context) { - foreach (const QString& key, shortcutsDescription.keys()) { + foreach (const QString& key, m_shortcutsDescription.keys()) { QKeySequence seq = qvariant_cast( - Settings::settings->value("Shortcuts/" + key)); + m_config.settings().value("Shortcuts/" + key)); m_shortcuts.insert(key, new QShortcut(seq, context)); } m_shortcuts.value("TogglePlayback")->setContext(Qt::ApplicationShortcut); @@ -114,6 +110,11 @@ ShortcutHandler::setupConnections() void ShortcutHandler::shortcutChanged(QString key) { - m_shortcuts.value(key)->setKey( - qvariant_cast(Settings::settings->value("Shortcuts/" + key))); + m_shortcuts.value(key)->setKey(qvariant_cast( + m_config.settings().value("Shortcuts/" + key))); +} + +const QMap &ShortcutHandler::shortcutsDescription() const +{ + return m_shortcutsDescription; } diff --git a/src/utils/shortcuthandler.h b/src/utils/shortcuthandler.h index 9245c0c6..6cf39387 100644 --- a/src/utils/shortcuthandler.h +++ b/src/utils/shortcuthandler.h @@ -6,6 +6,7 @@ #ifndef SHORTCUTHANDLER_H #define SHORTCUTHANDLER_H +#include "configuration.h" #include #include #include @@ -22,17 +23,12 @@ class ShortcutHandler : public QObject { Q_OBJECT public: - static QMap shortcutsDescription; - static void populateDescriptionMap(); - static QSharedPointer current(); - /** - * @brief class constructor - * @param parent - pointer to parent widget that will recieve the shortcut - * events - */ - explicit ShortcutHandler(QObject* parent = nullptr); + static ShortcutHandler& getInstance(); + void populateDescriptionMap(); void createShortcuts(QObject* context); + const QMap& shortcutsDescription() const; + public slots: /** * @brief slot to reload the key sequence for the shortcut with the given key @@ -68,6 +64,13 @@ public slots: void openAdvancedCopy(); private: + Configuration& m_config; + /** + * @brief class constructor + * @param parent - pointer to parent widget that will recieve the shortcut + * events + */ + ShortcutHandler(); /** * @brief connect different QShortcut signals to their * corresponding signal in ShortcutHandler @@ -78,6 +81,7 @@ public slots: * settings name */ QHash m_shortcuts; + QMap m_shortcutsDescription; }; #endif // SHORTCUTHANDLER_H diff --git a/src/utils/stylemanager.cpp b/src/utils/stylemanager.cpp index 372da4dd..e35affe0 100644 --- a/src/utils/stylemanager.cpp +++ b/src/utils/stylemanager.cpp @@ -1,12 +1,20 @@ #include "stylemanager.h" -#include "settings.h" #include #include #include #include -QDir StyleManager::themeResources; -QSharedPointer StyleManager::awesome; +StyleManager& +StyleManager::getInstance() +{ + static StyleManager stylemanager; + return stylemanager; +} + +StyleManager::StyleManager() + : m_config(Configuration::getInstance()) +{ +} void StyleManager::loadTheme() @@ -15,26 +23,23 @@ StyleManager::loadTheme() QPalette themeColors; QFile styles, palette; - switch (Settings::themeId) { + switch (m_config.themeId()) { case 0: - themeResources.setPath(":/resources/light/"); - styles.setFileName(themeResources.filePath("light.qss")); - palette.setFileName(themeResources.filePath("light.xml")); - Settings::darkMode = false; + m_themeResources.setPath(":/resources/light/"); + styles.setFileName(m_themeResources.filePath("light.qss")); + palette.setFileName(m_themeResources.filePath("light.xml")); break; case 1: - themeResources.setPath(":/resources/light/"); - styles.setFileName(themeResources.filePath("sepia.qss")); - palette.setFileName(themeResources.filePath("sepia.xml")); - Settings::darkMode = false; + m_themeResources.setPath(":/resources/light/"); + styles.setFileName(m_themeResources.filePath("sepia.qss")); + palette.setFileName(m_themeResources.filePath("sepia.xml")); break; case 2: - themeResources.setPath(":/resources/dark/"); - styles.setFileName(themeResources.filePath("dark.qss")); - palette.setFileName(themeResources.filePath("dark.xml")); - Settings::darkMode = true; + m_themeResources.setPath(":/resources/dark/"); + styles.setFileName(m_themeResources.filePath("dark.qss")); + palette.setFileName(m_themeResources.filePath("dark.xml")); break; } @@ -75,6 +80,18 @@ StyleManager::loadTheme() styles.close(); } - awesome = QSharedPointer::create(); - awesome->initFontAwesome(); + m_awesome = new fa::QtAwesome(qApp); + m_awesome->initFontAwesome(); +} + +fa::QtAwesome& +StyleManager::awesome() +{ + return *m_awesome; +} + +const QDir& +StyleManager::themeResources() const +{ + return m_themeResources; } diff --git a/src/utils/stylemanager.h b/src/utils/stylemanager.h index 3ffa8064..ba03b9f7 100644 --- a/src/utils/stylemanager.h +++ b/src/utils/stylemanager.h @@ -2,15 +2,25 @@ #define STYLEMANAGER_H #include +#include #include #include +#include "configuration.h" class StyleManager { public: - static QSharedPointer awesome; - static QDir themeResources; - static void loadTheme(); + static StyleManager& getInstance(); + + void loadTheme(); + fa::QtAwesome& awesome(); + const QDir& themeResources() const; + +private: + StyleManager(); + const Configuration& m_config; + QPointer m_awesome; + QDir m_themeResources; }; #endif // STYLEMANAGER_H diff --git a/src/utils/verseplayer.cpp b/src/utils/verseplayer.cpp index 07105921..aa452f70 100644 --- a/src/utils/verseplayer.cpp +++ b/src/utils/verseplayer.cpp @@ -9,10 +9,14 @@ VersePlayer::VersePlayer(QObject* parent, int reciterIdx) : QMediaPlayer(parent) , m_reciter(reciterIdx) , m_output(new QAudioOutput(this)) + , m_activeVerse(Verse::getCurrent()) + , m_reciterDir( + DirManager::getInstance().downloadsDir().absoluteFilePath("recitations")) + , m_reciters(Reciter::reciters) { setAudioOutput(m_output); - m_reciterDir.cd(m_recitersList.at(m_reciter)->baseDirName()); + m_reciterDir.cd(m_reciters.at(m_reciter).baseDirName()); loadActiveVerse(); } @@ -57,12 +61,12 @@ VersePlayer::setPlayerVolume(qreal volume) } QString -VersePlayer::constructVerseFilename(const QSharedPointer v) +VersePlayer::constructVerseFilename(const Verse& v) { // construct verse mp3 filename e.g. 002005.mp3 QString filename; - filename.append(QString::number(v->surah()).rightJustified(3, '0')); - filename.append(QString::number(v->number()).rightJustified(3, '0')); + filename.append(QString::number(v.surah()).rightJustified(3, '0')); + filename.append(QString::number(v.number()).rightJustified(3, '0')); filename.append(".mp3"); return filename; @@ -78,13 +82,13 @@ VersePlayer::playCurrentVerse() bool VersePlayer::changeReciter(int reciterIdx) { - if (m_activeVerse->number() == 0) - m_activeVerse->setNumber(1); + if (m_activeVerse.number() == 0) + m_activeVerse.setNumber(1); stop(); if (reciterIdx != m_reciter) { m_reciterDir.cdUp(); - m_reciterDir.cd(m_recitersList.at(reciterIdx)->baseDirName()); + m_reciterDir.cd(m_reciters.at(reciterIdx).baseDirName()); m_reciter = reciterIdx; } @@ -97,7 +101,7 @@ VersePlayer::setVerseFile(const QString& newVerseFilename) if (!m_reciterDir.exists(newVerseFilename)) { setSource(QUrl()); qDebug() << "file " + newVerseFilename + " is missing."; - emit missingVerseFile(m_reciter, m_activeVerse->surah()); + emit missingVerseFile(m_reciter, m_activeVerse.surah()); return false; } @@ -110,8 +114,8 @@ VersePlayer::setVerseFile(const QString& newVerseFilename) bool VersePlayer::loadActiveVerse() { - if (m_activeVerse->number() == 0) { - setSource(QUrl::fromLocalFile(m_recitersList.at(m_reciter)->basmallahPath())); + if (m_activeVerse.number() == 0) { + setSource(QUrl::fromLocalFile(m_reciters.at(m_reciter).basmallahPath())); return true; } @@ -121,7 +125,7 @@ VersePlayer::loadActiveVerse() QString VersePlayer::reciterName() const { - return m_recitersList.at(m_reciter)->displayName(); + return m_reciters.at(m_reciter).displayName(); } QAudioOutput* diff --git a/src/utils/verseplayer.h b/src/utils/verseplayer.h index 9a33a014..b1c6ab29 100644 --- a/src/utils/verseplayer.h +++ b/src/utils/verseplayer.h @@ -39,7 +39,7 @@ class VersePlayer : public QMediaPlayer * @param v - ::Verse to get the filename of * @return QString of the filename */ - QString constructVerseFilename(const QSharedPointer v); + QString constructVerseFilename(const Verse& v); /** * @brief load the verse mp3 from the current reciter directory and set the * m_verseFile variable @@ -116,9 +116,9 @@ public slots: void missingVerseFile(int reciterIdx, int surah); private: - QSharedPointer m_activeVerse = Verse::current(); - QDir m_reciterDir = DirManager::downloadsDir->absoluteFilePath("recitations"); - const QList>& m_recitersList = Reciter::reciters; + Verse& m_activeVerse; + QDir m_reciterDir; + const QList& m_reciters; /** * @brief boolean indicating whether the player is on or off, 'on' implies * that playback should continue in case of verse change diff --git a/src/widgets/betaqaviewer.cpp b/src/widgets/betaqaviewer.cpp index 490f00d4..63792630 100644 --- a/src/widgets/betaqaviewer.cpp +++ b/src/widgets/betaqaviewer.cpp @@ -10,6 +10,7 @@ BetaqaViewer::BetaqaViewer(QWidget* parent) , ui(new Ui::BetaqaViewer) , m_shadowEffect(new QGraphicsDropShadowEffect(this)) , m_sizeAnim(new QPropertyAnimation(this, "size")) + , m_betaqatDb(BetaqatDb::getInstance()) { this->hide(); ui->setupUi(this); @@ -37,7 +38,7 @@ void BetaqaViewer::showSurah(int surah) { if (surah != m_surah) { - ui->betaqaTextBrowser->setHtml(m_betaqatDb->getBetaqa(surah)); + ui->betaqaTextBrowser->setHtml(m_betaqatDb.getBetaqa(surah)); m_surah = surah; } diff --git a/src/widgets/betaqaviewer.h b/src/widgets/betaqaviewer.h index 3fd00052..017da5d6 100644 --- a/src/widgets/betaqaviewer.h +++ b/src/widgets/betaqaviewer.h @@ -30,7 +30,7 @@ public slots: private: Ui::BetaqaViewer* ui; - QSharedPointer m_betaqatDb = BetaqatDb::current(); + BetaqatDb& m_betaqatDb; int m_surah = -1; QPointer m_shadowEffect; diff --git a/src/widgets/notificationpopup.cpp b/src/widgets/notificationpopup.cpp index 662302e0..f4a1787c 100644 --- a/src/widgets/notificationpopup.cpp +++ b/src/widgets/notificationpopup.cpp @@ -9,10 +9,10 @@ using namespace fa; NotificationPopup::NotificationPopup(QWidget* parent) - : QFrame{ parent } - , m_iconWidget{ new QLabel(this) } - , m_textWidget{ new QLabel(this) } - , m_opacityEffect{ new QGraphicsOpacityEffect(this) } + : QFrame(parent) + , m_iconWidget(new QLabel(this)) + , m_textWidget(new QLabel(this)) + , m_opacityEffect(new QGraphicsOpacityEffect(this)) { setObjectName("Popup"); this->setGraphicsEffect(m_opacityEffect); @@ -127,7 +127,8 @@ NotificationPopup::setNotificationIcon(NotificationType type) break; } - m_iconWidget->setFont(StyleManager::awesome->font(faStyle, 18)); + m_iconWidget->setFont( + StyleManager::getInstance().awesome().font(faStyle, 18)); m_iconWidget->setText(ico); } diff --git a/src/widgets/notificationpopup.h b/src/widgets/notificationpopup.h index d70b830e..262e2f8f 100644 --- a/src/widgets/notificationpopup.h +++ b/src/widgets/notificationpopup.h @@ -70,13 +70,9 @@ private slots: * @param message - QString of message to show * @param icon - NotificationPopup::Action entry */ - void notify(NotificationType icon, QString message); + void notify(NotificationType icon, QString message); private: - QList>& m_recitersList = Reciter::reciters; - QList>& m_tafasir = Tafsir::tafasir; - QList>& m_translations = - Translation::translations; /** * @brief connects signals and slots for different UI * components and shortcuts. diff --git a/src/widgets/quranpagebrowser.cpp b/src/widgets/quranpagebrowser.cpp index e90e2cba..1b5e9843 100644 --- a/src/widgets/quranpagebrowser.cpp +++ b/src/widgets/quranpagebrowser.cpp @@ -8,13 +8,16 @@ #include #include #include -#include using namespace fa; QuranPageBrowser::QuranPageBrowser(QWidget* parent, int initPage) : QTextBrowser(parent) , m_highlighter(new QTextCursor(document())) , m_highlightColor(QBrush(qApp->palette().color(QPalette::Highlight))) + , m_config(Configuration::getInstance()) + , m_styleMgr(StyleManager::getInstance()) + , m_quranDb(QuranDb::getInstance()) + , m_glyphsDb(GlyphsDb::getInstance()) { setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setTextInteractionFlags(Qt::TextInteractionFlag::LinksAccessibleByMouse); @@ -22,7 +25,7 @@ QuranPageBrowser::QuranPageBrowser(QWidget* parent, int initPage) createActions(); updateFontSize(); - m_pageFont = FontManager::pageFontname(initPage); + m_pageFont = FontManager::getInstance().getInstance().pageFontname(initPage); m_pageFormat.setAlignment(Qt::AlignCenter); m_pageFormat.setNonBreakableLines(true); m_pageFormat.setLayoutDirection(Qt::RightToLeft); @@ -44,7 +47,8 @@ void QuranPageBrowser::updateFontSize() { m_fontSize = - m_settings->value("Reader/QCF" + QString::number(m_qcfVer) + "Size", 22) + m_config.settings() + .value("Reader/QCF" + QString::number(m_config.qcfVersion()) + "Size", 22) .toInt(); highlightVerse(m_highlightedIdx); } @@ -101,7 +105,7 @@ QuranPageBrowser::surahFrame(int surah) QString frmText; frmText.append("ﰦ"); frmText.append("ﮌ"); - frmText.append(m_glyphsDb->getSurahNameGlyph(surah)); + frmText.append(m_glyphsDb.getSurahNameGlyph(surah)); // draw on top of the image the surah name text QPainter p(&baseImage); @@ -109,7 +113,7 @@ QuranPageBrowser::surahFrame(int surah) p.setFont(QFont("QCF_BSML", 77)); p.drawText(baseImage.rect(), Qt::AlignCenter, frmText); - if (Settings::darkMode) + if (m_config.darkMode()) baseImage.invertPixels(); return baseImage; @@ -133,14 +137,14 @@ QuranPageBrowser::setHref(QTextCursor* cursor, int to, QString url) QString QuranPageBrowser::pageHeader(int page) { - m_headerData = m_quranDb->pageMetadata(page); + m_headerData = m_quranDb.pageMetadata(page); QString suraHeader, jozzHeader; suraHeader.append("سورة "); - suraHeader.append(m_quranDb->surahName(m_headerData.first, true)); + suraHeader.append(m_quranDb.surahName(m_headerData.first, true)); suraHeader.append("$"); jozzHeader.append("الجزء "); - jozzHeader.append(m_glyphsDb->getJuzGlyph(m_headerData.second)); + jozzHeader.append(m_glyphsDb.getJuzGlyph(m_headerData.second)); return suraHeader + jozzHeader; } @@ -157,17 +161,19 @@ QuranPageBrowser::constructPage(int pageNo, bool forceCustomSize) m_verseCoordinates.clear(); this->document()->clear(); - m_pageFont = FontManager::pageFontname(pageNo); + m_pageFont = FontManager::getInstance().pageFontname(pageNo); QTextCursor textCursor(this->document()); m_currPageHeader = this->pageHeader(m_page); - m_currPageLines = m_glyphsDb->getPageLines(m_page); + m_currPageLines = m_glyphsDb.getPageLines(m_page); // automatic font adjustment check - if (!forceCustomSize && m_settings->value("Reader/AdaptiveFont").toBool()) { + if (!forceCustomSize && + m_config.settings().value("Reader/AdaptiveFont").toBool()) { m_fontSize = this->bestFitFontSize(); - m_settings->setValue("Reader/QCF" + QString::number(m_qcfVer) + "Size", - m_fontSize); + m_config.settings().setValue( + "Reader/QCF" + QString::number(m_config.qcfVersion()) + "Size", + m_fontSize); } m_pageLineSize = this->calcPageLineSize(m_currPageLines); @@ -178,7 +184,7 @@ QuranPageBrowser::constructPage(int pageNo, bool forceCustomSize) QBrush(qApp->palette().color(QPalette::PlaceholderText))); // smaller header font size for long juz > 10 - if (m_qcfVer == 1 && pageNo >= 202) + if (m_config.qcfVersion() == 1 && pageNo >= 202) m_headerTextFormat.setFontPointSize(std::max(4, m_fontSize - 8)); else m_headerTextFormat.setFontPointSize(m_fontSize - 6); @@ -188,6 +194,7 @@ QuranPageBrowser::constructPage(int pageNo, bool forceCustomSize) setHref(&textCursor, 1, "#F" + QString::number(m_headerData.first)); } + qDebug() << "PARENT SIZE: " << parentWidget()->height(); parentWidget()->setMinimumWidth(m_pageLineSize.width() + 70); // page lines drawing @@ -211,7 +218,7 @@ QuranPageBrowser::constructPage(int pageNo, bool forceCustomSize) prevAnchor += 2; } else if (l.contains("bsml")) { QImage bsml(":/resources/basmalah.png"); - if (Settings::darkMode) + if (m_config.darkMode()) bsml.invertPixels(); textCursor.insertBlock(m_pageFormat, m_bodyTextFormat); @@ -281,7 +288,7 @@ QuranPageBrowser::resetHighlight() { QTextCharFormat tcf; if (m_fgHighlight) - tcf.setForeground(Settings::darkMode ? Qt::white : Qt::black); + tcf.setForeground(m_config.darkMode() ? Qt::white : Qt::black); else tcf.setBackground(Qt::transparent); @@ -369,18 +376,17 @@ QuranPageBrowser::createActions() m_actAddBookmark = new QAction(tr("Add Bookmark"), this); m_actRemBookmark = new QAction(tr("Remove Bookmark"), this); m_actZoomIn->setIcon( - StyleManager::awesome->icon(fa_solid, fa_magnifying_glass_plus)); + m_styleMgr.awesome().icon(fa_solid, fa_magnifying_glass_plus)); m_actZoomOut->setIcon( - StyleManager::awesome->icon(fa_solid, fa_magnifying_glass_minus)); - m_actPlay->setIcon(StyleManager::awesome->icon(fa_solid, fa_play)); - m_actSelect->setIcon(StyleManager::awesome->icon(fa_solid, fa_hand_pointer)); - m_actTafsir->setIcon(StyleManager::awesome->icon(fa_solid, fa_book_open)); - m_actTranslation->setIcon(StyleManager::awesome->icon(fa_solid, fa_language)); - m_actThoughts->setIcon(StyleManager::awesome->icon(fa_solid, fa_comment)); - m_actCopy->setIcon(StyleManager::awesome->icon(fa_solid, fa_clipboard)); - m_actAddBookmark->setIcon( - StyleManager::awesome->icon(fa_regular, fa_bookmark)); - m_actRemBookmark->setIcon(StyleManager::awesome->icon(fa_solid, fa_bookmark)); + m_styleMgr.awesome().icon(fa_solid, fa_magnifying_glass_minus)); + m_actPlay->setIcon(m_styleMgr.awesome().icon(fa_solid, fa_play)); + m_actSelect->setIcon(m_styleMgr.awesome().icon(fa_solid, fa_hand_pointer)); + m_actTafsir->setIcon(m_styleMgr.awesome().icon(fa_solid, fa_book_open)); + m_actTranslation->setIcon(m_styleMgr.awesome().icon(fa_solid, fa_language)); + m_actThoughts->setIcon(m_styleMgr.awesome().icon(fa_solid, fa_comment)); + m_actCopy->setIcon(m_styleMgr.awesome().icon(fa_solid, fa_clipboard)); + m_actAddBookmark->setIcon(m_styleMgr.awesome().icon(fa_regular, fa_bookmark)); + m_actRemBookmark->setIcon(m_styleMgr.awesome().icon(fa_solid, fa_bookmark)); connect( m_actZoomIn, &QAction::triggered, this, &QuranPageBrowser::actionZoomIn); connect( @@ -405,8 +411,8 @@ void QuranPageBrowser::actionZoomIn() { m_fontSize++; - m_settings->setValue("Reader/QCF" + QString::number(m_qcfVer) + "Size", - m_fontSize); + m_config.settings().setValue( + "Reader/QCF" + QString::number(m_config.qcfVersion()) + "Size", m_fontSize); constructPage(m_page, true); highlightVerse(m_highlightedIdx); } @@ -415,8 +421,8 @@ void QuranPageBrowser::actionZoomOut() { m_fontSize--; - m_settings->setValue("Reader/QCF" + QString::number(m_qcfVer) + "Size", - m_fontSize); + m_config.settings().setValue( + "Reader/QCF" + QString::number(m_config.qcfVersion()) + "Size", m_fontSize); constructPage(m_page, true); highlightVerse(m_highlightedIdx); } @@ -426,7 +432,7 @@ QuranPageBrowser::updateHighlightLayer() { int old = m_highlightedIdx; resetHighlight(); - m_fgHighlight = m_settings->value("Reader/FGHighlight").toBool(); + m_fgHighlight = m_config.settings().value("Reader/FGHighlight").toBool(); QColor hc = m_highlightColor.color(); if (m_fgHighlight) diff --git a/src/widgets/quranpagebrowser.h b/src/widgets/quranpagebrowser.h index 17ef26b5..50e4e083 100644 --- a/src/widgets/quranpagebrowser.h +++ b/src/widgets/quranpagebrowser.h @@ -19,7 +19,8 @@ #include #include #include -#include +#include +#include /** * @brief QuranPageBrowser class is a modified QTextBrowser for displaying a @@ -145,10 +146,10 @@ public slots: #endif private: - const int m_qcfVer = Settings::qcfVersion; - QSharedPointer m_quranDb = QuranDb::current(); - QSharedPointer m_glyphsDb = GlyphsDb::current(); - QSharedPointer const m_settings = Settings::settings; + Configuration& m_config; + StyleManager& m_styleMgr; + const QuranDb& m_quranDb; + const GlyphsDb& m_glyphsDb; /** * @brief utility for creating menu actions for interacting with the widget */ From 2d5afe8ff0edf9b422e40ffcffe535d3c2131179 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Fri, 1 Mar 2024 14:54:27 +0200 Subject: [PATCH 35/51] feat: update translation template file --- dist/translations/qc_template.ts | 610 ++++++++++++++++++------------- 1 file changed, 351 insertions(+), 259 deletions(-) diff --git a/dist/translations/qc_template.ts b/dist/translations/qc_template.ts index 8e403c28..486163f6 100644 --- a/dist/translations/qc_template.ts +++ b/dist/translations/qc_template.ts @@ -224,212 +224,211 @@ - + Preferences - + General - + Theme - + Light - + Sepia - + Dark - + Language - + Audio output device - - + + Features - + Daily verse - + Missing recitation warning - + Reader - + Quran page - + Foreground Highlight - + Adaptive font size - + Reader mode - + Single page - + Double page - + Page font - + QCF V1 - - - + + + QCF V2 - - - + + + size - + Verse font - + QCF - + Uthmanic - + Uthmanic (annotated) - + Side content - + Font Family - - - Tafsir + + + Translation - - - Translation + + Shortcuts - - Shortcuts + + Tafsir - + Description - + Key - - - - + + + + Restart required - + Application theme was changed, restart now? - + Application language was changed, restart now? - + Reading mode was changed, restart now? - + Restart is required to load new quran font, restart now? - + Apply - + Cancel @@ -558,8 +557,8 @@ - - + + Quran Companion @@ -569,270 +568,260 @@ - + Edit - + File - + Help - - + + Navigation - + Juz - + Page - + Verse - + Search surah - + Preferences - + Download manager - + Exit - + Find - + Check for updates - + Bookmarks - + About Quran Companion - - + + About Qt - + Tafsir - + Verse of the day - + + Khatmah - + Advanced copy - + Toggle reader view - - Khatmah + + Import - - Default + + Export - - There are currently no updates available. + + Player Controls - - - - Update info + + Default - - Updates available, do you want to open the update tool? + + Now playing: - - Updates info + + Surah - - Updates are available, use the maintainance tool to install the latest updates. + + + + Files Missing - - Now playing: + + The selected font files are missing, would you like to download it? - - Surah + + The selected tafsir is missing, would you like to download it? - - Recitation not found + + The selected translation is missing, would you like to download it? - The recitation files for the current surah is missing, would you like to download it? - - - - - - Files Missing - - - - - The selected font files are missing, would you like to download it? + Recitation not found - - The selected tafsir is missing, would you like to download it? + + The recitation files for the current surah is missing, would you like to download it? AboutDialog - + About Quran Companion - + Quran Companion - + Version - + About - + A free, open-source Quran reader & player - + Useful Links - + Translators - + Credits - + Recitations - + Tafsir/Translations - + Surah Cards - + Libraries - + Licensed under the - + Waqf General Public License - + Project Homepage - + Report a bug/Request a feature - + Contribute to translations @@ -840,111 +829,185 @@ BookmarksDialog - + Bookmarks - + next - + Left - + previous - + Right - + No bookmarks available. Start bookmarking verses to see them here. - + Go to verse - + Remove - - + + Surah: - - + + Verse: - + All + + BookmarksNotifier + + + Verse added to bookmarks + + + + + Verse removed from bookmarks + + + + + ContentDialog + + + Content + + + + + Tafsir + + + + + Translation + + + + + Thoughts + + + + + next + + + + + Left + + + + + previous + + + + + Right + + + + + Surah: + + + + + Verse: + + + CopyDialog - + Advanced Copy - + Surah - + From - + To - + Invalid range - + The entered verse range is invalid + + CopyNotifier + + + Verse text copied to clipboard + + + DownloadManager - + bytes - + KB - + MB @@ -952,165 +1015,173 @@ DownloaderDialog - + Download Manager - + Add to queue - + Downloads - + clear - + stop - + Name - + Number - + Extras - + Additional files - + // Surah: - + Downloading: - + /sec - + Download Completed - + Download Failed - KhatmahDialog + ImportExportDialog - - Khatmah Dialog + + Data Selection - - Current Khatmah: + + Bookmarks - - Default + + Khatmah - - Start a new khatmah + + Thoughts - - Set as active + + The following error occured during import - - Remove + + + Error - - Surah: + + The following error occured during export + + + JobNotifier - - Verse: + + Download Completed - - Khatmah + + Download Failed - NotificationPopup + KhatmahDialog - - Download Completed + + Khatmah Dialog - - - QCF V2 + + Current Khatmah: - - Download Failed + + Default - - Verse added to bookmarks + + Start a new khatmah - - Verse removed from bookmarks + + Set as active - - Verse text copied to clipboard + + Remove - - You are running the latest version + + Surah: - - Update available + + Verse: + + + + + Khatmah @@ -1122,45 +1193,68 @@ + + QFileDialog + + + Open File + + + + + Save File + + + QuranPageBrowser - + Zoom In - + Zoom Out - + Copy Verse - + Select - + Play - + Tafsir - + + Translation + + + + + Thoughts + + + + Add Bookmark - + Remove Bookmark @@ -1181,87 +1275,87 @@ SearchDialog - + Verse search - + Search - + Find - + Search selected surahs only - + Whole word - + next - + Left - + previous - + Right - + Filter - + Pages - + From - + To - + Surahs - + Search results - + Surah: - + Verse: @@ -1282,84 +1376,82 @@ SystemTray - + Play/Pause recitation - + Show window - + Hide window - + Preferences - + Check for updates - + About - + Exit - TafsirDialog - - - Tafsir - - + UpdateNotifier - - next + + Update available - - Left + + You are running the latest version + + + VerseDialog - - previous + + Verse Of The Day + + + VersionChecker - - Right + + There are currently no updates available. - - Surah: + + Update info - - Verse: + + Updates available, do you want to open the update tool? - - - VerseDialog - - Verse Of The Day + + Updates info From 99c6bb9c29197de399d8a151737bb2d5e595e50d Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Fri, 1 Mar 2024 15:03:26 +0200 Subject: [PATCH 36/51] refactor: capitalization fix --- dist/translations/qc_template.ts | 2 +- src/core/mainwindow.ui | 2 +- src/dialogs/downloaderdialog.cpp | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dist/translations/qc_template.ts b/dist/translations/qc_template.ts index 486163f6..282b8683 100644 --- a/dist/translations/qc_template.ts +++ b/dist/translations/qc_template.ts @@ -687,7 +687,7 @@ - Player Controls + Player controls diff --git a/src/core/mainwindow.ui b/src/core/mainwindow.ui index e8660c46..c479be11 100644 --- a/src/core/mainwindow.ui +++ b/src/core/mainwindow.ui @@ -539,7 +539,7 @@ true - Player Controls + Player controls diff --git a/src/dialogs/downloaderdialog.cpp b/src/dialogs/downloaderdialog.cpp index 4c09d036..b92bd76e 100644 --- a/src/dialogs/downloaderdialog.cpp +++ b/src/dialogs/downloaderdialog.cpp @@ -18,7 +18,7 @@ DownloaderDialog::DownloaderDialog(QWidget* parent, JobManager* manager) , m_quranDb(QuranDb::getInstance()) , m_reciters(Reciter::reciters) , m_tafasir(Tafsir::tafasir) - , m_translations(Translation::translations) + , m_translations(Translation::translations) { ui->setupUi(this); @@ -127,7 +127,7 @@ DownloaderDialog::populateTreeModel() m_treeModel.invisibleRootItem()->appendRow(translation); // -- translations for (int i = 0; i < m_translations.size(); i++) { - const Translation& tr = m_translations.at(i); + const Translation& tr = m_translations.at(i); if (!tr.isExtra()) continue; QStandardItem* item = new QStandardItem(tr.displayName()); @@ -229,7 +229,7 @@ DownloaderDialog::addTaskProgress(QSharedPointer job) prgFrm->setObjectName(objName); QBoxLayout* downInfo; - if (m_lang == QLocale::Arabic) + if (m_config.language() == QLocale::Arabic) downInfo = new QBoxLayout(QBoxLayout::RightToLeft, prgFrm); else downInfo = new QHBoxLayout(prgFrm); From 1810844e2e6fa121b55bf286e93c35fc47e748e7 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:21:50 +0200 Subject: [PATCH 37/51] fix: shortcut and ui control syncing --- src/core/mainwindow.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 8d5d7a62..b55f1ad0 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -145,7 +145,8 @@ MainWindow::setupShortcuts() m_shortcutHandler.createShortcuts(this); connect( &m_shortcutHandler, &ShortcutHandler::togglePlayerControls, this, [this]() { - actionPlayerControlsToggled(!m_playerControls->isVisible()); + ui->actionPlayerControls->setChecked( + !ui->actionPlayerControls->isChecked()); }); for (const auto& connection : { From ae81a80bde4e1e3904f84826161c4a16d17de17a Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sat, 2 Mar 2024 12:47:32 +0200 Subject: [PATCH 38/51] New Crowdin updates (#52) * Update Arabic translation * Update Turkish translation * Update Arabic translation * Update Turkish translation * Update Arabic translation --- dist/translations/qc_ar.ts | 676 +++++++++++++++++++++---------------- dist/translations/qc_tr.ts | 676 +++++++++++++++++++++---------------- 2 files changed, 768 insertions(+), 584 deletions(-) diff --git a/dist/translations/qc_ar.ts b/dist/translations/qc_ar.ts index 00da3fb2..55e655a1 100644 --- a/dist/translations/qc_ar.ts +++ b/dist/translations/qc_ar.ts @@ -224,212 +224,211 @@ تفهيم القرآن - الإنجليزية - + Preferences الإعدادات - + General عام - + Theme المظهر - + Light فاتح - + Sepia بني - + Dark داكن - + Language اللغة - + Audio output device جهاز إخراج الصوت - - + + Features المميزات - + Daily verse الآية اليومية - + Missing recitation warning تحذير التلاوات المفقودة - + Reader القارئ - + Quran page صفحة القرآن - + Foreground Highlight تظليل النص - + Adaptive font size حجم خط تلقائي - + Reader mode وضع القراءة - + Single page صفحة - + Double page صفحتين - + Page font خط الصفحة - + QCF V1 خط 1 - - - + + + QCF V2 خط 2 - - - + + + size الحجم - + Verse font خط الآيات - + QCF خط الصفحة - + Uthmanic عثماني - + Uthmanic (annotated) عثماني (مرمز) - + Side content المحتوى الجانبي - + Font Family نوع الخط - - - Tafsir - التفسير - - - - + + Translation الترجمة - + Shortcuts الاختصارات - + + Tafsir + التفسير + + + Description الوصف - + Key المفتاح - - - - + + + + Restart required مطلوب إعادة التشغيل - + Application theme was changed, restart now? تم تغيير مظهر التطبيق، هل تريد إعادة التشغيل الآن؟ - + Application language was changed, restart now? تم تغيير لغة التطبيق، هل تريد إعادة التشغيل الآن؟ - + Reading mode was changed, restart now? تم تغيير وضع القراءة، هل تريد إعادة التشغيل الآن؟ - + Restart is required to load new quran font, restart now? إعادة التشغيل مطلوبة لتحميل خط القرآن الجديد، هل تريد إعادة التشغيل الآن؟ - + Apply طبّق - + Cancel ألغِ @@ -558,8 +557,8 @@ - - + + Quran Companion رفيق القرآن @@ -569,270 +568,260 @@ عرض - + Edit تحرير - + File ملف - + Help مساعدة - - + + Navigation التصفح - + Juz الجزء - + Page الصفحة - + Verse الآية - + Search surah بحث السور - + Preferences الإعدادات - + Download manager مدير التحميلات - + Exit خروج - + Find البحث - + Check for updates التحقق من وجود تحديثات - + Bookmarks العلامات - + About Quran Companion عن رفيق القرآن - - + + About Qt عن كيوت - + Tafsir التفسير - + Verse of the day آية اليوم - + + Khatmah الختمات - + Advanced copy النسخ المتقدم - + Toggle reader view تبديل طريقة عرض القارئ - - Khatmah - ختمه + + Import + استيراد - - Default - افتراضي + + Export + تصدير - - There are currently no updates available. - لا يوجد تحديثات متوفرة حاليا. + + Player controls + التحكم بالمشغل - - - - Update info - معلومات التحديث - - - - Updates available, do you want to open the update tool? - هناك تحديثات متاحة، هل تود تشغيل مدير التحديثات؟ - - - - Updates info - معلومات التحديث - - - - Updates are available, use the maintainance tool to install the latest updates. - التحديثات متاحة، استخدم أداة الصيانة لتثبيت أحدث التحديثات. + + Default + افتراضي - + Now playing: يقرأ الآن: - + Surah سورة - - Recitation not found - التلاوة غير موجودة - - - - The recitation files for the current surah is missing, would you like to download it? - ملفات التلاوة الخاصة بالسورة الحالية غير متوفرة، هل تود الذَّهاب إلى صفحة التحميل؟ - - - - + + + Files Missing الملفات مفقودة - + The selected font files are missing, would you like to download it? ملفات الخط المحدد مفقودة، هل ترغب في تحميلها؟ - + The selected tafsir is missing, would you like to download it? التفسير المحدد مفقود، هل ترغب في تحميله؟ + + + The selected translation is missing, would you like to download it? + الترجمة المحددة مفقود، هل ترغب في تحميله؟ + + + + Recitation not found + التلاوة غير موجودة + + + + The recitation files for the current surah is missing, would you like to download it? + ملفات التلاوة الخاصة بالسورة الحالية غير متوفرة، هل تود الذَّهاب إلى صفحة التحميل؟ + AboutDialog - + About Quran Companion عن رفيق القرآن - + Quran Companion رفيق القرآن - + Version إصدار - + About عن البرنامَج - + A free, open-source Quran reader & player قارئ ومشغل للقرآن الكريم مجاني و مفتوح المصدر - + Useful Links روابط مفيدة - + Translators المترجمون - + Credits شكر وتقدير - + Recitations التلاوات - + Tafsir/Translations التفسير/الترجمات - + Surah Cards بطاقات السور - + Libraries المكتبات - + Licensed under the مرخص بموجب - + Waqf General Public License رخصة وقف العامة - + Project Homepage صفحة المشروع - + Report a bug/Request a feature التبليغ عن خلل / طلب ميزة - + Contribute to translations ساهم في الترجمة @@ -840,111 +829,185 @@ BookmarksDialog - + Bookmarks العلامات - + next التالي - + Left يسار - + previous السابق - + Right يمين - + No bookmarks available. Start bookmarking verses to see them here. لا توجد علامات متاحة. أضف علامات على الآيات لتراها هنا. - + Go to verse انتقل إلى الآية - + Remove إزالة - - + + Surah: سورة: - - + + Verse: آية: - + All الكل + + BookmarksNotifier + + + Verse added to bookmarks + تم إضافة علامة + + + + Verse removed from bookmarks + تم إزالة العلامة + + + + ContentDialog + + + Content + المحتوى + + + + Tafsir + التفسير + + + + Translation + الترجمة + + + + Thoughts + خواطر + + + + next + التالي + + + + Left + يسار + + + + previous + السابق + + + + Right + يمين + + + + Surah: + سورة: + + + + Verse: + آية: + + CopyDialog - + Advanced Copy النسخ المتقدم - + Surah سورة - + From من - + To إلى - + Invalid range نطاق غير صالح - + The entered verse range is invalid نطاق الآيات الذي تم إدخاله غير صالح + + CopyNotifier + + + Verse text copied to clipboard + تم نسخ نص الآية إلى الحافظة + + DownloadManager - + bytes بايت - + KB كب - + MB مب @@ -952,72 +1015,124 @@ DownloaderDialog - + Download Manager مدير التحميلات - + Add to queue إضافة تحميل - + Downloads التحميلات - + clear محو - + stop إيقاف - + Name الاسم - + Number الرقم - + Extras إضافات - + Additional files ملفات إضافية - + // Surah: // سورة: - + Downloading: جاري تحميل: - + /sec - + + Download Completed + تم التحميل + + + + Download Failed + فشل التحميل + + + + ImportExportDialog + + + Data Selection + اختيار البيانات + + + + Bookmarks + العلامات + + + + Khatmah + الختمات + + + + Thoughts + خواطر + + + + The following error occured during import + حدث الخطأ التالي أثناء الاستيراد + + + + + Error + خطأ + + + + The following error occured during export + حدث الخطأ التالي أثناء التصدير + + + + JobNotifier + + Download Completed تم التحميل - + Download Failed فشل التحميل @@ -1025,95 +1140,51 @@ KhatmahDialog - + Khatmah Dialog الختمات - + Current Khatmah: الختمة الحالية: - + Default افتراضي - + Start a new khatmah إنشاء ختمة جديدة - + Set as active تفعيل - + Remove إزالة - + Surah: سورة: - + Verse: آية: - + Khatmah ختمه - - NotificationPopup - - - Download Completed - تم التحميل - - - - - QCF V2 - خط 2 - - - - Download Failed - فشل التحميل - - - - Verse added to bookmarks - تم إضافة علامة - - - - Verse removed from bookmarks - تم إزالة العلامة - - - - Verse text copied to clipboard - تم نسخ نص الآية إلى الحافظة - - - - You are running the latest version - لديك أحدث إصدار - - - - Update available - تحديث متاح - - PlayerControls @@ -1122,45 +1193,68 @@ القارئ + + QFileDialog + + + Open File + فتح ملف + + + + Save File + حفظ الملف + + QuranPageBrowser - + Zoom In تكبير - + Zoom Out تصغير - + Copy Verse نسخ نص الآية - + Select اختر - + Play قراءة - + Tafsir التفسير - + + Translation + الترجمة + + + + Thoughts + خواطر + + + Add Bookmark اضافة علامة - + Remove Bookmark إزالة العلامة @@ -1181,87 +1275,87 @@ SearchDialog - + Verse search بحث الآيات - + Search البحث - + Find البحث - + Search selected surahs only البحث في السور المختارة فقط - + Whole word كلمة كاملة - + next التالي - + Left يسار - + previous السابق - + Right يمين - + Filter تصفية - + Pages الصفحات - + From من - + To إلى - + Surahs السُور - + Search results ناتج بحث - + Surah: سورة: - + Verse: آية: @@ -1282,85 +1376,83 @@ SystemTray - + Play/Pause recitation تشغيل/إيقاف التلاوة - + Show window إظهار النافذة - + Hide window إخفاء النافذة - + Preferences الإعدادات - + Check for updates التحقق من وجود تحديثات - + About عن البرنامَج - + Exit خروج - TafsirDialog + UpdateNotifier - - Tafsir - التفسير - - - - next - التالي + + Update available + تحديث متاح - - Left - يسار + + You are running the latest version + لديك أحدث إصدار + + + VerseDialog - - previous - السابق + + Verse Of The Day + آية اليوم + + + VersionChecker - - Right - يمين + + There are currently no updates available. + لا يوجد تحديثات متوفرة حاليا. - - Surah: - سورة: + + Update info + معلومات التحديث - - Verse: - آية: + + Updates available, do you want to open the update tool? + هناك تحديثات متاحة، هل تود تشغيل مدير التحديثات؟ - - - VerseDialog - - Verse Of The Day - آية اليوم + + Updates info + معلومات التحديث diff --git a/dist/translations/qc_tr.ts b/dist/translations/qc_tr.ts index 9af011aa..cceeb4a0 100644 --- a/dist/translations/qc_tr.ts +++ b/dist/translations/qc_tr.ts @@ -224,212 +224,211 @@ Tafheem ul-Quran - English - + Preferences Ayarlar - + General Genel - + Theme Tema - + Light Açık - + Sepia Sepya - + Dark Koyu - + Language Dil - + Audio output device Ses Çıkış Cihazı - - + + Features Özellikler - + Daily verse Günlük ayet - + Missing recitation warning Eksik okuyucu uyarısı - + Reader Okuyucu - + Quran page Kuran sayfası - + Foreground Highlight Ön Plan Vurgusu - + Adaptive font size Uyarlanabilir yazı tipi boyutu - + Reader mode Okuyucu modu - + Single page Tek sayfa - + Double page Çift sayfa - + Page font Page font - + QCF V1 QCF V1 - - - + + + QCF V2 QCF V2 - - - + + + size size - + Verse font Verse font - + QCF QCF - + Uthmanic Uthmanic - + Uthmanic (annotated) Uthmanic (annotated) - + Side content Yan içerik - + Font Family Yazı Tipi Ailesi - - - Tafsir - Tefsir - - - - + + Translation Çeviri - + Shortcuts Kısayollar - + + Tafsir + Tefsir + + + Description Açıklama - + Key Tuş - - - - + + + + Restart required Yeniden başlatma gerekli - + Application theme was changed, restart now? Uygulama teması değiştirildi, şimdi yeniden başlatılsın mı? - + Application language was changed, restart now? Uygulama dili değiştirildi, şimdi yeniden başlatılsın mı? - + Reading mode was changed, restart now? Okuma modu değiştirildi, şimdi yeniden başlatılsın mı? - + Restart is required to load new quran font, restart now? Yeni Kuran yazı tipini yüklemek için yeniden başlatma gerekiyor, şimdi yeniden başlatılsın mı? - + Apply Uygula - + Cancel Vazgeç @@ -558,8 +557,8 @@ - - + + Quran Companion Kura-an'ı Kerim Arkadaşı @@ -569,270 +568,260 @@ Görünüm - + Edit Düzenle - + File Dosya - + Help Yardım - - + + Navigation Gezinme - + Juz Cüz - + Page Sayfa - + Verse Ayet - + Search surah Sure ara - + Preferences Ayarlar - + Download manager İndirme yöneticisi - + Exit Çıkış yap - + Find Bul - + Check for updates Güncellemeleri kontrol et - + Bookmarks Yer işaretleri - + About Quran Companion Kura-an'ı Kerim Arkadaşı hakkında - - + + About Qt Qt Hakkında - + Tafsir Tefsir - + Verse of the day Günün Ayeti - + + Khatmah Hatim - + Advanced copy Gelişmiş kopyalama - + Toggle reader view Toggle reader view - - Khatmah - Hatim + + Import + Import - - Default - Varsayılan + + Export + Export - - There are currently no updates available. - Yeni bir güncelleme yok. + + Player controls + Player controls - - - - Update info - Güncelleme bilgisi - - - - Updates available, do you want to open the update tool? - Güncellemeler mevcut, güncelleme aracını açmak ister misiniz? - - - - Updates info - Güncelleme bilgisi - - - - Updates are available, use the maintainance tool to install the latest updates. - Güncellemeler mevcuttur, en son güncellemeleri yüklemek için bakım aracını kullanın. + + Default + Varsayılan - + Now playing: Şimdi oynatılıyor: - + Surah Sure - - Recitation not found - Kıraat bulunamadı - - - - The recitation files for the current surah is missing, would you like to download it? - Güncel Surenin kıraat dosyaları eksik, indirmek ister misiniz? - - - - + + + Files Missing Files Missing - + The selected font files are missing, would you like to download it? The selected font files are missing, would you like to download it? - + The selected tafsir is missing, would you like to download it? The selected tafsir is missing, would you like to download it? + + + The selected translation is missing, would you like to download it? + The selected translation is missing, would you like to download it? + + + + Recitation not found + Kıraat bulunamadı + + + + The recitation files for the current surah is missing, would you like to download it? + Güncel Surenin kıraat dosyaları eksik, indirmek ister misiniz? + AboutDialog - + About Quran Companion Kura-an'ı Kerim Arkadaşı hakkında - + Quran Companion Kura-an'ı Kerim Arkadaşı - + Version Version - + About About - + A free, open-source Quran reader & player A free, open-source Quran reader & player - + Useful Links Useful Links - + Translators Translators - + Credits Credits - + Recitations Recitations - + Tafsir/Translations Tafsir/Translations - + Surah Cards Surah Cards - + Libraries Libraries - + Licensed under the Licensed under the - + Waqf General Public License Waqf General Public License - + Project Homepage Project Homepage - + Report a bug/Request a feature Report a bug/Request a feature - + Contribute to translations Contribute to translations @@ -840,111 +829,185 @@ BookmarksDialog - + Bookmarks Yer işaretleri - + next next - + Left Sol - + previous previous - + Right Sağ - + No bookmarks available. Start bookmarking verses to see them here. Yer işareti yok. Ayetleri burada görmek için yer işareti eklemeye başlayın. - + Go to verse Ayete git - + Remove Kaldır - - + + Surah: Sure: - - + + Verse: Ayet: - + All Tamamı + + BookmarksNotifier + + + Verse added to bookmarks + Verse added to bookmarks + + + + Verse removed from bookmarks + Verse removed from bookmarks + + + + ContentDialog + + + Content + Content + + + + Tafsir + Tefsir + + + + Translation + Çeviri + + + + Thoughts + Thoughts + + + + next + next + + + + Left + Sol + + + + previous + previous + + + + Right + Sağ + + + + Surah: + Sure: + + + + Verse: + Ayet: + + CopyDialog - + Advanced Copy Gelişmiş Kopyalama - + Surah Sure - + From Kimden - + To Alıcı - + Invalid range Geçersiz aralık - + The entered verse range is invalid Girilen Ayet aralığı geçersiz + + CopyNotifier + + + Verse text copied to clipboard + Verse text copied to clipboard + + DownloadManager - + bytes bayt - + KB KB - + MB MB @@ -952,72 +1015,124 @@ DownloaderDialog - + Download Manager İndirme yöneticisi - + Add to queue Kuyruğa ekle - + Downloads İndirilenler - + clear temizle - + stop dur - + Name Ad - + Number Numara - + Extras Extras - + Additional files Additional files - + // Surah: // Sure: - + Downloading: İndiriliyor: - + /sec /sn - + + Download Completed + İndirme tamamlandı + + + + Download Failed + İndirme Başarısız Oldu + + + + ImportExportDialog + + + Data Selection + Data Selection + + + + Bookmarks + Yer işaretleri + + + + Khatmah + Hatim + + + + Thoughts + Thoughts + + + + The following error occured during import + The following error occured during import + + + + + Error + Error + + + + The following error occured during export + The following error occured during export + + + + JobNotifier + + Download Completed İndirme tamamlandı - + Download Failed İndirme Başarısız Oldu @@ -1025,95 +1140,51 @@ KhatmahDialog - + Khatmah Dialog Hatim iletişim kutusu - + Current Khatmah: Şu anki hatim: - + Default Varsayılan - + Start a new khatmah Yeni hatime başla - + Set as active Etkin olarak ayarla - + Remove Kaldır - + Surah: Sure: - + Verse: Ayet: - + Khatmah Hatim - - NotificationPopup - - - Download Completed - İndirme tamamlandı - - - - - QCF V2 - QCF V2 - - - - Download Failed - İndirme Başarısız Oldu - - - - Verse added to bookmarks - Ayet yer işaretlerine eklendi - - - - Verse removed from bookmarks - Ayet yer işaretlerinden silindi - - - - Verse text copied to clipboard - Ayet metni panoya kopyalandı - - - - You are running the latest version - En son sürümü çalıştırıyorsunuz - - - - Update available - Güncelleme var - - PlayerControls @@ -1122,45 +1193,68 @@ Reciter + + QFileDialog + + + Open File + Open File + + + + Save File + Save File + + QuranPageBrowser - + Zoom In Yakınlaştır - + Zoom Out Uzaklaştır - + Copy Verse Ayeti kopyala - + Select Seç - + Play Oynat - + Tafsir Tefsir - + + Translation + Çeviri + + + + Thoughts + Thoughts + + + Add Bookmark Yer işareti Ekle - + Remove Bookmark Yer imini kaldır @@ -1181,87 +1275,87 @@ SearchDialog - + Verse search Ayet ara - + Search Ara - + Find Bul - + Search selected surahs only Yalnızca seçilen Sureleri ara - + Whole word Tüm kelime - + next next - + Left Sol - + previous previous - + Right Sağ - + Filter Süzgeç - + Pages Sayfalar - + From Kimden - + To Alıcı - + Surahs Sureler - + Search results Arama sonuçları - + Surah: Sure: - + Verse: Ayet: @@ -1282,85 +1376,83 @@ SystemTray - + Play/Pause recitation Play/Pause recitation - + Show window Show window - + Hide window Hide window - + Preferences Ayarlar - + Check for updates Güncellemeleri kontrol et - + About About - + Exit Çıkış yap - TafsirDialog + UpdateNotifier - - Tafsir - Tefsir - - - - next - next + + Update available + Update available - - Left - Sol + + You are running the latest version + You are running the latest version + + + VerseDialog - - previous - previous + + Verse Of The Day + Günün Ayeti + + + VersionChecker - - Right - Sağ + + There are currently no updates available. + There are currently no updates available. - - Surah: - Sure: + + Update info + Update info - - Verse: - Ayet: + + Updates available, do you want to open the update tool? + Updates available, do you want to open the update tool? - - - VerseDialog - - Verse Of The Day - Günün Ayeti + + Updates info + Updates info From 4bc68a57f6368abdced3c6dd0cdeba8a944448a5 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sat, 2 Mar 2024 12:49:54 +0200 Subject: [PATCH 39/51] fix: import only valid entries --- src/dialogs/importexportdialog.cpp | 5 +++-- src/utils/jsondataimporter.cpp | 12 ++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/dialogs/importexportdialog.cpp b/src/dialogs/importexportdialog.cpp index 6ac9db20..749672e8 100644 --- a/src/dialogs/importexportdialog.cpp +++ b/src/dialogs/importexportdialog.cpp @@ -32,9 +32,10 @@ ImportExportDialog::selectImports(QString filepath) { m_mode = Mode::Import; m_importer->setFile(filepath); - if (m_importer->read()) + if (m_importer->read()) { setCheckedImports(); - open(); + open(); + } } void diff --git a/src/utils/jsondataimporter.cpp b/src/utils/jsondataimporter.cpp index a6a9cea9..00d60f01 100644 --- a/src/utils/jsondataimporter.cpp +++ b/src/utils/jsondataimporter.cpp @@ -12,8 +12,10 @@ JsonDataImporter::importBookmarks() QJsonArray arr = m_fileObj.value("bookmarks").toArray(); foreach (const QJsonValue& item, arr) { Verse bookmark = verseFromJson(item.toObject()); - if (!m_bookmarksDb.isBookmarked(bookmark)) - m_bookmarksDb.addBookmark(bookmark, true); + if (bookmark.page() < 1 || bookmark.surah() < 1 || bookmark.number() < 1) + continue; + if (!m_bookmarksDb.isBookmarked(bookmark)) + m_bookmarksDb.addBookmark(bookmark, true); } } @@ -25,6 +27,7 @@ JsonDataImporter::importKhatmah() QJsonArray arr = m_fileObj.value("khatmah").toArray(); foreach (const QJsonValue& item, arr) { QPair khatmah = khatmahFromJson(item.toObject()); + if (!khatmah.first.isEmpty()) m_bookmarksDb.addKhatmah(khatmah.second, khatmah.first); } } @@ -37,6 +40,7 @@ JsonDataImporter::importThoughts() QJsonArray arr = m_fileObj.value("thoughts").toArray(); foreach (const QJsonValue& item, arr) { QPair thought = thoughtFromJson(item.toObject()); + if (!thought.second.isEmpty()) m_bookmarksDb.saveThoughts(thought.first, thought.second); } } @@ -126,7 +130,7 @@ QPair JsonDataImporter::khatmahFromJson(const QJsonObject& obj) { QString name; - Verse v; + Verse v(1, 1, 1); if (!validKhatmah(obj)) return { name, v }; @@ -138,7 +142,7 @@ JsonDataImporter::khatmahFromJson(const QJsonObject& obj) QPair JsonDataImporter::thoughtFromJson(const QJsonObject& obj) { - Verse v; + Verse v(1, 1, 1); QString text; if (!validThought(obj)) return { v, text }; From de9f5c23b8e51bba497e13ea81b02c915a21df74 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sat, 2 Mar 2024 13:07:02 +0200 Subject: [PATCH 40/51] fix: use explicit context for notifiers translated text --- src/notifiers/bookmarksnotifier.cpp | 7 +++++-- src/notifiers/copynotifier.cpp | 4 +++- src/notifiers/jobnotifier.cpp | 7 +++++-- src/notifiers/updatenotifier.cpp | 7 +++++-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/notifiers/bookmarksnotifier.cpp b/src/notifiers/bookmarksnotifier.cpp index 8bb04297..25840399 100644 --- a/src/notifiers/bookmarksnotifier.cpp +++ b/src/notifiers/bookmarksnotifier.cpp @@ -1,4 +1,5 @@ #include "bookmarksnotifier.h" +#include BookmarksNotifier::BookmarksNotifier(QObject* parent) { @@ -8,13 +9,15 @@ BookmarksNotifier::BookmarksNotifier(QObject* parent) void BookmarksNotifier::notifyAdded() { - QString msg = tr("Verse added to bookmarks"); + QString msg = + qApp->translate("BookmarksNotifier", "Verse added to bookmarks"); emit notify(bookmarkAdd, msg); } void BookmarksNotifier::notifyRemoved() { - QString msg = tr("Verse removed from bookmarks"); + QString msg = + qApp->translate("BookmarksNotifier", "Verse removed from bookmarks"); emit notify(bookmarkRemove, msg); } diff --git a/src/notifiers/copynotifier.cpp b/src/notifiers/copynotifier.cpp index 333afab3..e253ee0a 100644 --- a/src/notifiers/copynotifier.cpp +++ b/src/notifiers/copynotifier.cpp @@ -1,4 +1,5 @@ #include "copynotifier.h" +#include CopyNotifier::CopyNotifier(QObject* parent) { @@ -8,6 +9,7 @@ CopyNotifier::CopyNotifier(QObject* parent) void CopyNotifier::copied() { - QString msg = tr("Verse text copied to clipboard"); + QString msg = + qApp->translate("CopyNotifier", "Verse text copied to clipboard"); emit notify(copiedText, msg); } diff --git a/src/notifiers/jobnotifier.cpp b/src/notifiers/jobnotifier.cpp index fecdc905..7c0cfa4d 100644 --- a/src/notifiers/jobnotifier.cpp +++ b/src/notifiers/jobnotifier.cpp @@ -1,4 +1,5 @@ #include "jobnotifier.h" +#include JobNotifier::JobNotifier(QObject* parent) { @@ -8,13 +9,15 @@ JobNotifier::JobNotifier(QObject* parent) void JobNotifier::notifyCompleted(QPointer job) { - QString msg = tr("Download Completed") + ": " + job->name(); + QString msg = + qApp->translate("JobNotifier", "Download Completed") + ": " + job->name(); emit notify(success, msg); } void JobNotifier::notifyFailed(QPointer job) { - QString msg = tr("Download Failed") + ": " + job->name(); + QString msg = + qApp->translate("JobNotifier", "Download Failed") + ": " + job->name(); emit notify(fail, msg); } diff --git a/src/notifiers/updatenotifier.cpp b/src/notifiers/updatenotifier.cpp index e44f8789..bd94c5ee 100644 --- a/src/notifiers/updatenotifier.cpp +++ b/src/notifiers/updatenotifier.cpp @@ -1,17 +1,20 @@ #include "updatenotifier.h" +#include UpdateNotifier::UpdateNotifier(QObject* parent) {} void UpdateNotifier::notifyUpdate(QString version) { - QString msg = tr("Update available") + ": " + version; + QString msg = + qApp->translate("UpdateNotifier", "Update available") + ": " + version; emit notify(updateInfo, msg); } void UpdateNotifier::notifyLatest() { - QString msg = tr("You are running the latest version"); + QString msg = + qApp->translate("UpdateNotifier", "You are running the latest version"); emit notify(success, msg); } From 0bdd0e058ea18d782de882b1efea6ab53987d855 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sat, 2 Mar 2024 13:07:31 +0200 Subject: [PATCH 41/51] fix: highlight current verse after hiding player --- src/core/mainwindow.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index b55f1ad0..d97fd136 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -779,8 +779,10 @@ void MainWindow::actionPlayerControlsToggled(bool checked) { m_playerControls->setVisible(checked); - if (m_config.settings().value("Reader/AdaptiveFont").toBool()) + if (m_config.settings().value("Reader/AdaptiveFont").toBool()) { m_reader->redrawQuranPage(); + m_reader->highlightCurrentVerse(); + } } void From bdb3aa0e4e8fa91b52c36aa217079c91762c07c4 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sat, 2 Mar 2024 15:20:05 +0200 Subject: [PATCH 42/51] fix: fix download job name alignment --- src/dialogs/downloaderdialog.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dialogs/downloaderdialog.cpp b/src/dialogs/downloaderdialog.cpp index b92bd76e..1e385cd4 100644 --- a/src/dialogs/downloaderdialog.cpp +++ b/src/dialogs/downloaderdialog.cpp @@ -242,6 +242,7 @@ DownloaderDialog::addTaskProgress(QSharedPointer job) downSpeed->setAlignment(Qt::AlignRight); downInfo->addWidget(lbTitle); + downInfo->addStretch(1); downInfo->addWidget(downSpeed); prgFrm->layout()->addItem(downInfo); From 9b9244327d0528ff36b30564f4804ae1863d5942 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sat, 2 Mar 2024 17:20:15 +0200 Subject: [PATCH 43/51] docs: update documentation for core, databases, and dialogs --- src/core/mainwindow.h | 109 +++++++++++++++++++++++++-------- src/core/playercontrols.h | 12 ++++ src/core/quranreader.h | 56 +++++++++++++---- src/database/betaqatdb.h | 17 +++++ src/database/bookmarksdb.h | 68 +++++++++++++++----- src/database/qurandb.h | 14 ++--- src/dialogs/bookmarksdialog.h | 12 ++-- src/dialogs/contentdialog.h | 6 +- src/dialogs/downloaderdialog.h | 6 +- src/dialogs/khatmahdialog.h | 2 +- src/dialogs/searchdialog.h | 6 +- 11 files changed, 232 insertions(+), 76 deletions(-) diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 0a9b53c7..a04a7b6d 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -43,8 +43,9 @@ QT_END_NAMESPACE /** * @class MainWindow - * @brief MainWindow class is responsible for the reader interface, audio - * controls, navigation, and opening other dialogs + * @brief MainWindow class is the driver class the connects all the other + * components and is responsible for the navigation dock and menubar + * functionality */ class MainWindow : public QMainWindow { @@ -60,15 +61,13 @@ class MainWindow : public QMainWindow public slots: /** - * @brief currentVerseChanged - * - * MODIFIED + * @brief slot for adjusting UI elements to reflect the current verse + * information */ void currentVerseChanged(); /** - * @brief currentSurahChanged - * - * MODIFIED + * @brief slot for reseting the verses combobox and + * highlighting the active surah in the navigation dock */ void currentSurahChanged(); /** @@ -118,7 +117,7 @@ private slots: /** * @brief display warning message box in case that recitation files are * missing - * @param reciterIdx - ::Globals::recitersList index for the reciter + * @param reciterIdx - ::Reciter::reciters index for the reciter * @param surah */ void missingRecitationFileWarn(int reciterIdx, int surah); @@ -128,14 +127,12 @@ private slots: void missingQCF(); /** * @brief display a warning messagebox when a tafsir db file is not found - * @param idx - index of tafsir in Globals::tafasirList + * @param idx - index of tafsir in Tafsir::tafasir */ void missingTafsir(int idx); /** - * @brief missingTranslation - * @param idx - * - * MODIFIED + * @brief display a warning messagebox when a translation db file is not found + * @param idx - index of translation in Translation::translations */ void missingTranslation(int idx); /** @@ -150,7 +147,7 @@ private slots: */ void updateTrayTooltip(QMediaPlayer::PlaybackState state); /** - * @brief adds the current ::Verse to the bookmarks + * @brief adds the current Verse to the bookmarks */ void bookmarkCurrent(); /** @@ -178,7 +175,7 @@ private slots: */ void actionAdvancedCopyTriggered(); /** - * @brief open the ContentDialog for the current ::Verse + * @brief open the ContentDialog for the current Verse */ void actionTafsirTriggered(); /** @@ -190,10 +187,9 @@ private slots: */ void actionSearchTriggered(); /** - * @brief actionPlayerControlsToggled - * @param checked - * - * MODIFIED + * @brief callback for the checkbox to toggle the visibility of the player + * controls + * @param checked - boolean representing check state of ui checkbox */ void actionPlayerControlsToggled(bool checked); /** @@ -205,11 +201,11 @@ private slots: */ void actionAboutQttriggered(); /** - * @brief move to the next ::Verse relative to m_currVerse + * @brief move to the next Verse relative to m_currVerse */ void nextVerse(); /** - * @brief move to the previous ::Verse relative to m_currVerse + * @brief move to the previous Verse relative to m_currVerse */ void prevVerse(); /** @@ -253,18 +249,48 @@ private slots: * @brief toggles visiblity of the navigation dock */ void toggleNavDock(); + /** + * @brief callback function for importing user data from file + */ void importUserData(); + /** + * @brief callback function for exporting user data to file + */ void exportUserData(); private: Ui::MainWindow* ui; + /** + * @brief reference to the shared current verse instance + */ Verse& m_currVerse; + /** + * @brief reference to the singleton Configuration instance + */ Configuration& m_config; + /** + * @brief reference to the singleton ShortcutHandler instance + */ ShortcutHandler& m_shortcutHandler; + /** + * @brief reference to the singleton BookmarksDb instance + */ BookmarksDb& m_bookmarksDb; + /** + * @brief reference to the singleton QuranDb instance + */ const QuranDb& m_quranDb; + /** + * @brief reference to the singleton TranslationDb instance + */ const TranslationDb& m_translationDb; + /** + * @brief reference to the static QList of available reciters + */ const QList& m_reciters; + /** + * @brief reference to the static QList of available tafasir + */ const QList& m_tafasir; /** * @brief initalizes different parts used by the app @@ -275,7 +301,7 @@ private slots: */ void loadIcons(); /** - * @brief set the current ::Verse from settings + * @brief set the current Verse from settings */ void loadVerse(); /** @@ -287,12 +313,36 @@ private slots: * shortcuts */ void setupConnections(); + /** + * @brief connect VersePlayer signals to their corresponding slots in + * MainWindow + */ void connectPlayer(); + /** + * @brief connect SystemTray signals to their corresponding slots in + * MainWindow + */ void connectTray(); + /** + * @brief connect QuranReader specific signals to their corresponding slots + */ void connectReader(); + /** + * @brief connect Controls/Navigation specific signals to their corresponding + * slots + */ void connectControls(); + /** + * @brief connect SettingsDialog signals to their corresponding slots + */ void connectSettings(); + /** + * @brief connect menubar actions to their corresponding slots + */ void connectMenubar(); + /** + * @brief register the different notifiers to the NotificationPopup widget + */ void connectNotifiers(); /** * @brief initialize the surahs QListView in the side dock and select the @@ -306,7 +356,7 @@ private slots: void setupMenubarButton(); /** * @brief sync the surahs QListView in the navigation dock to match the - * currently active ::Verse in the VersePlayer + * currently active Verse in the VersePlayer * @return QModelIndex of the currently selected surah */ QModelIndex syncSelectedSurah(); @@ -418,12 +468,19 @@ private slots: * @brief pointer to the votd dialog */ QPointer m_verseDlg; + /** + * @brief pointer to the FileSelector dialog used for selecting files for + * import/export + */ QPointer m_selectorDlg; + /** + * @brief pointer to dialog used for selecting which user data parts to + * import/export + */ QPointer m_importExportDlg; /** - * @brief pointer to the QProcess instance of the maintainence tool that - * checks for updates + * @brief pointer to VersionChecker instance */ QPointer m_versionChecker; /** diff --git a/src/core/playercontrols.h b/src/core/playercontrols.h index 1f8d4895..1c26e245 100644 --- a/src/core/playercontrols.h +++ b/src/core/playercontrols.h @@ -74,8 +74,17 @@ private slots: private: Ui::PlayerControls* ui; + /** + * @brief reference to the shared current verse instance + */ const Verse& m_currVerse; + /** + * @brief reference to the singleton Configuration instance + */ Configuration& m_config; + /** + * @brief reference to the static QList of available reciters + */ const QList& m_reciters; /** * @brief load icons for different UI elements @@ -93,6 +102,9 @@ private slots: * @brief pointer to VersePlayer instance */ QPointer m_player; + /** + * @brief pointer to the QuranReader instance + */ QPointer m_reader; }; diff --git a/src/core/quranreader.h b/src/core/quranreader.h index 5d34b8ce..cd117251 100644 --- a/src/core/quranreader.h +++ b/src/core/quranreader.h @@ -22,12 +22,17 @@ class QuranReader : public QWidget Q_OBJECT public: + /** + * @brief class constructor + * @param parent - pointer to parent widget + * @param player - pointer to VersePlayer instance + */ explicit QuranReader(QWidget* parent, VersePlayer* player); ~QuranReader(); public slots: /** - * @brief highlight the currently active ::Verse m_currVerse in the + * @brief highlight the currently active Verse m_currVerse in the * active QuranPageBrowser and the side panel depending on the ::ReaderMode */ void highlightCurrentVerse(); @@ -92,9 +97,7 @@ public slots: */ void setVerseToStartOfPage(); /** - * @brief updatePageFontSize - * - * MODIFIED + * @brief slot for updating the page font size of all quran pages */ void updatePageFontSize(); @@ -112,7 +115,7 @@ private slots: * @brief callback function for clicking verses in the QuranPageBrowser that * takes actions based on the chosen option in the menu * @param hrefUrl - "#idx" where idx is the verse index relative to the start - * of the page (=index in the page ::Verse QList) + * of the page (=index in the page Verse QList) */ void verseAnchorClicked(const QUrl& hrefUrl); /** @@ -141,14 +144,42 @@ private slots: private: Ui::QuranReader* ui; + /** + * @brief reference to the shared current verse instance + */ Verse& m_currVerse; + /** + * @brief reference to the singleton Configuration instance + */ Configuration& m_config; + /** + * @brief reference to the singleton TafsirDb instance + */ TafsirDb& m_tafsirDb; + /** + * @brief reference to the singleton TranslationDb instance + */ TranslationDb& m_translationDb; + /** + * @brief reference to the singleton BookmarksDb instance + */ BookmarksDb& m_bookmarksDb; + /** + * @brief reference to the singleton QuranDb instance + */ QuranDb& m_quranDb; + /** + * @brief reference to the singleton GlyphsDb instance + */ const GlyphsDb& m_glyphsDb; + /** + * @brief reference to the static QList of available tafasir + */ const QList& m_tafasir; + /** + * @brief connects signals and slots for different UI components and + * shortcuts + */ void setupConnections(); /** * @brief load icons for different UI elements @@ -193,7 +224,7 @@ private slots: */ void selectVerse(int browserIdx, int IdxInPage); /** - * @brief updates the list that contains::Verse instances for verses in the + * @brief updates the list that containsVerse instances for verses in the * current page */ void updatePageVerseInfoList(); @@ -201,28 +232,28 @@ private slots: * @brief QScrollArea used in single page mode to display verses & * translation */ - QPointer m_scrlVerseByVerse = nullptr; + QPointer m_scrlVerseByVerse; /** * @brief pointer to currently active QuranPageBrowser instance, must be one * of the values in m_quranBrowsers array */ - QPointer m_activeQuranBrowser = nullptr; + QPointer m_activeQuranBrowser; /** * @brief array of QuranPageBrowser instances used in different modes, index 0 * is used in both modes */ - QPointer m_quranBrowsers[2]{}; + QPointer m_quranBrowsers[2]; /** * @brief QList of QFrame pointers to VerseFrame elements in the single page * mode side panel */ QList> m_verseFrameList; /** - * @brief pointer to the currently active page ::Verse list + * @brief pointer to the currently active page Verse list */ const QList* m_activeVList; /** - * @brief array of 2 QLists of ::Verse instances for the verses in the + * @brief array of 2 QLists of Verse instances for the verses in the * displayed page(s), index 0 is used in both reader modes */ QList m_vLists[2]; @@ -230,6 +261,9 @@ private slots: * @brief pointer to the currently highlighted VerseFrame in the side panel */ QPointer m_highlightedFrm; + /** + * @brief pointer to the VersePlayer instance + */ QPointer m_player; /** * @brief QFont used in the side panel translation diff --git a/src/database/betaqatdb.h b/src/database/betaqatdb.h index 6ec78cfb..d3d7d078 100644 --- a/src/database/betaqatdb.h +++ b/src/database/betaqatdb.h @@ -12,8 +12,19 @@ class BetaqatDb , QSqlDatabase { public: + /** + * @brief get a reference to the single class instance + * @return reference to the static class instance + */ static BetaqatDb& getInstance(); + /** + * @brief sets and opens the sqlite database file + */ void open(); + /** + * @brief getter for the type of the connection + * @return - DbConnection::Betaqat + */ Type type(); /** * @brief get the surah card (betaqa) content @@ -24,7 +35,13 @@ class BetaqatDb private: BetaqatDb(); + /** + * @brief reference to the singleton Configuration instance + */ const Configuration& m_config; + /** + * @brief reference to the app assets directory + */ const QDir& m_assetsDir; }; diff --git a/src/database/bookmarksdb.h b/src/database/bookmarksdb.h index 8344a418..08b0eb91 100644 --- a/src/database/bookmarksdb.h +++ b/src/database/bookmarksdb.h @@ -17,13 +17,24 @@ class BookmarksDb , QSqlDatabase { public: + /** + * @brief get a reference to the single class instance + * @return reference to the static class instance + */ static BookmarksDb& getInstance(); + /** + * @brief sets and opens the sqlite database file + */ void open(); + /** + * @brief getter for the type of the connection + * @return - DbConnection::Bookmarks + */ Type type(); /** - * @brief sets the given ::Verse as the last position reached in the current + * @brief sets the given Verse as the last position reached in the current * active khatmah - * @param v - ::Verse reached in khatmah + * @param v - Verse reached in khatmah */ bool saveActiveKhatmah(const Verse& verse); /** @@ -38,15 +49,15 @@ class BookmarksDb QString getKhatmahName(const int id) const; /** * @brief gets the last position saved for the khatmah with the id given and - * stores the position in the ::Verse v + * stores the position in the Verse v * @return boolean indicating a successful operation (false in case of error * and in case id does not exist) */ bool loadVerse(const int khatmahId, Verse& verse) const; /** * @brief add a new khatmah/replace khatmah with given id with position of - * ::Verse v - * @param v - ::Verse to set as the khatmah position + * Verse v + * @param v - Verse to set as the khatmah position * @param name - new khatmah name * @param id - id of khatmah to replace, -1 means do not replace (default: -1) * @return id of newly added khatmah or id parameter if defined @@ -68,41 +79,46 @@ class BookmarksDb */ void removeKhatmah(const int id) const; /** - * @brief gets a QList of ::Verse instances representing the bookmarked verse + * @brief gets a QList of Verse instances representing the bookmarked verse * within the given sura (default gets all) * @param surahIdx - sura number (-1 returns all bookmarks) * @return QList of bookmarked verses */ QList bookmarkedVerses(int surahIdx = -1) const; /** - * @brief checks whether the given ::Verse is bookmarked - * @param vInfo - ::Verse instance to check + * @brief checks whether the given Verse is bookmarked + * @param vInfo - Verse instance to check * @return boolean */ bool isBookmarked(const Verse& verse) const; /** - * @brief add the given ::Verse to bookmarks - * @param vInfo - ::Verse instance to add + * @brief add the given Verse to bookmarks + * @param vInfo - Verse instance to add * @return boolean */ bool addBookmark(const Verse& verse, bool silent); /** - * @brief remove the given ::Verse from bookmarks - * @param vInfo - ::Verse instance to remove + * @brief remove the given Verse from bookmarks + * @param vInfo - Verse instance to remove * @return boolean indicating successful removal */ bool removeBookmark(const Verse& verse, bool silent); /** - * MODIFIED + * @brief save thoughts for the verse given + * @param verse - Verse to save the thoughts for + * @param text - QString of the thoughts text */ void saveThoughts(Verse& verse, const QString& text); /** - * MODIFIED + * @brief get the user stored thoughts for the verse given + * @param verse - Verse to get thoughts for + * @return QString of the thought text */ QString getThoughts(const Verse& verse) const; /** - * @brief allThoughts - * @return + * @brief get all user stored thoughts + * @return QList of thoughts represented as QPair(s) of Verse and QString of + * the thought text */ QList> allThoughts() const; /** @@ -110,14 +126,34 @@ class BookmarksDb * @param id - id of the active khatmah */ void setActiveKhatmah(const int id); + /** + * @brief getter for m_activeKhatmah + * @return int - id of active khatmah + */ int activeKhatmah() const; + /** + * @brief returns the address of the class notifier + * @return - pointer to BookmarksNotifier instance + */ const BookmarksNotifier* notifier() const; private: BookmarksDb(); + /** + * @brief reference to the singleton QuranDb instance + */ const QuranDb& m_quranDb; + /** + * @brief reference to the singleton Configuration instance + */ const Configuration& m_config; + /** + * @brief reference to the app configuration directory + */ const QDir& m_configDir; + /** + * @brief notifer instance used for sending notifications + */ BookmarksNotifier m_notifier; /** * @brief integer id of the current active khatmah diff --git a/src/database/qurandb.h b/src/database/qurandb.h index ea9e6b65..26fb05c5 100644 --- a/src/database/qurandb.h +++ b/src/database/qurandb.h @@ -44,9 +44,9 @@ class QuranDb */ int getJuzOfPage(const int page) const; /** - * @brief gets a QList of ::Verse instances for the page verses + * @brief gets a QList of Verse instances for the page verses * @param page - Quran page number - * @return QList of ::Verse instances + * @return QList of Verse instances */ QList> verseInfoList(const int page) const; /** @@ -70,10 +70,10 @@ class QuranDb */ QString surahName(const int sIdx, bool ar = false) const; /** - * @brief get the verse with the corresponding id and return it as a ::Verse + * @brief get the verse with the corresponding id and return it as a Verse * instance * @param id - verse id - * @return ::Verse instance + * @return Verse instance */ QList verseById(const int id) const; /** @@ -95,7 +95,7 @@ class QuranDb * @param searchText - text to search for * @param surahs - QList of surah numbers to search in * @param whole - boolean value to search for whole words only - * @return QList of ::Verse instances representing the search results + * @return QList of Verse instances representing the search results */ QList> searchSurahs(QString searchText, const QList surahs, @@ -105,14 +105,14 @@ class QuranDb * @param searchText - text to search for * @param range - array of start & end page numbers * @param whole - boolean value to indicate search for whole words only - * @return QList of ::Verse instances representing the search results + * @return QList of Verse instances representing the search results */ QList> searchVerses(QString searchText, const int range[2] = new int[2]{ 1, 604 }, const bool whole = false) const; /** * @brief gets a random verse from the Quran - * @return QPair of ::Verse instance and verse text + * @return QPair of Verse instance and verse text */ QList randomVerse() const; /** diff --git a/src/dialogs/bookmarksdialog.h b/src/dialogs/bookmarksdialog.h index 64753ba5..3750aa07 100644 --- a/src/dialogs/bookmarksdialog.h +++ b/src/dialogs/bookmarksdialog.h @@ -74,19 +74,19 @@ class BookmarksDialog : public QDialog * @fn void navigateToVerse(Verse v) * @brief Emitted when 'go to verse' button is clicked to signal the * navigation and selection of a verse. - * @param v - ::Verse to navigate to + * @param v - Verse to navigate to */ void navigateToVerse(const Verse& v); public slots: /** - * @brief generates ::Verse object from the name of the Frame containing the + * @brief generates Verse object from the name of the Frame containing the * navigation buttons. Then emits BookmarksDialog::navigateToVerse(Verse). */ void btnGoToVerse(); /** - * @brief generates ::Verse object from the name of the Frame containing the - * navigation buttons then deletes the corresponding ::Verse from the + * @brief generates Verse object from the name of the Frame containing the + * navigation buttons then deletes the corresponding Verse from the * bookmarks database. * @details After removing the bookmark from the database, the corresponding * frame is removed. If there are no more bookmarks in this surah, the default @@ -144,11 +144,11 @@ private slots: */ int m_shownSurah = 0; /** - * @brief ::Verse QList for all bookmarked verses. + * @brief Verse QList for all bookmarked verses. */ QList m_allBookmarked; /** - * @brief ::Verse QList for the shown verses (all or for a specific surah). + * @brief Verse QList for the shown verses (all or for a specific surah). */ QList m_shownVerses; /** diff --git a/src/dialogs/contentdialog.h b/src/dialogs/contentdialog.h index 1ef9f351..477d9029 100644 --- a/src/dialogs/contentdialog.h +++ b/src/dialogs/contentdialog.h @@ -48,8 +48,8 @@ class ContentDialog : public QDialog public slots: /** - * @brief open ContentDialog with the shown verse set to the given ::Verse - * @param v - ::Verse to show the tafsir of + * @brief open ContentDialog with the shown verse set to the given Verse + * @param v - Verse to show the tafsir of */ void showVerseTafsir(const Verse& v); void showVerseTranslation(const Verse& v); @@ -188,7 +188,7 @@ private slots: */ int m_fontSZ; /** - * @brief ::Verse instance representing the shown verse. + * @brief Verse instance representing the shown verse. */ Verse m_shownVerse; /** diff --git a/src/dialogs/downloaderdialog.h b/src/dialogs/downloaderdialog.h index 091b9286..8c4556e3 100644 --- a/src/dialogs/downloaderdialog.h +++ b/src/dialogs/downloaderdialog.h @@ -146,7 +146,7 @@ private slots: void addTaskProgress(QSharedPointer job); /** * @brief enqueue a surah to download - * @param reciter - ::Globals::recitersList index for the reciter whose + * @param reciter - ::Reciter::reciters index for the reciter whose * recitations are being downloaded * @param surah - number of surah to download */ @@ -154,7 +154,7 @@ private slots: /** * @brief Adds the combination of reciter & surah to the currently active * downloads QHash. - * @param reciter - ::Globals::recitersList index for the reciter whose + * @param reciter - ::Reciter::reciters index for the reciter whose * recitations are being downloaded * @param surah - surah number */ @@ -162,7 +162,7 @@ private slots: /** * @brief Removes the surah from the reciters currently active * downloads. - * @param reciter - ::Globals::recitersList index for the reciter whose + * @param reciter - ::Reciter::reciters index for the reciter whose * recitations are being downloaded * @param surah - surah number */ diff --git a/src/dialogs/khatmahdialog.h b/src/dialogs/khatmahdialog.h index 6e4c4db3..13ffad17 100644 --- a/src/dialogs/khatmahdialog.h +++ b/src/dialogs/khatmahdialog.h @@ -38,7 +38,7 @@ class KhatmahDialog : public QDialog /** * @fn void navigateToVerse(Verse v) * @brief Emitted when changing the active khatmah - * @param v - ::Verse to navigate to + * @param v - Verse to navigate to */ void navigateToVerse(const Verse& v); diff --git a/src/dialogs/searchdialog.h b/src/dialogs/searchdialog.h index 364eee49..671a803a 100644 --- a/src/dialogs/searchdialog.h +++ b/src/dialogs/searchdialog.h @@ -49,7 +49,7 @@ public slots: void getResults(); /** * @brief Slot that is called when one of the result verse labels is clicked. - * Extracts a ::Verse from the emitting object name and emits + * Extracts a Verse from the emitting object name and emits * SearchDialog::navigateToVerse(Verse) signal. */ void verseClicked(); @@ -73,7 +73,7 @@ public slots: * @fn void navigateToVerse(Verse v) * @brief Emitted when a search result is clicked to signal the * navigation and selection of that verse. - * @param v - ::Verse to navigate to + * @param v - Verse to navigate to */ void navigateToVerse(const Verse& v); @@ -121,7 +121,7 @@ private slots: */ QList> m_lbLst; /** - * @brief ::Verse QList for the current search results. + * @brief Verse QList for the current search results. */ QList m_currResults; /** From 463d7776f76ac92007d4725df771d66b35622b1f Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sun, 3 Mar 2024 23:13:39 +0200 Subject: [PATCH 44/51] fix: add missing notification style settings --- src/widgets/notificationpopup.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/widgets/notificationpopup.cpp b/src/widgets/notificationpopup.cpp index f4a1787c..56f5b97e 100644 --- a/src/widgets/notificationpopup.cpp +++ b/src/widgets/notificationpopup.cpp @@ -138,6 +138,7 @@ NotificationPopup::notify(NotificationType type, QString message) QFontMetrics fm(m_textWidget->fontMetrics()); m_textWidget->setText(message); setNotificationIcon(type); + setStyle(type); resize(fm.size(Qt::TextSingleLine, message).width() + 50, 40); adjustLocation(); From a50b3dcb8e3c5de7a50bda9b5310ee607f313704 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sun, 3 Mar 2024 23:14:26 +0200 Subject: [PATCH 45/51] refactor: move versetype setting to configuration class --- src/dialogs/bookmarksdialog.cpp | 4 ++-- src/dialogs/contentdialog.cpp | 18 ++++++++---------- src/dialogs/copydialog.cpp | 5 +++-- src/dialogs/searchdialog.cpp | 4 ++-- src/utils/configuration.cpp | 12 ++++++++++++ src/utils/configuration.h | 3 +++ 6 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/dialogs/bookmarksdialog.cpp b/src/dialogs/bookmarksdialog.cpp index 10ed7d9e..823db12f 100644 --- a/src/dialogs/bookmarksdialog.cpp +++ b/src/dialogs/bookmarksdialog.cpp @@ -112,7 +112,7 @@ BookmarksDialog::loadBookmarks(int surah) for (int i = m_startIdx; i < end; i++) { const Verse& verse = m_shownVerses.at(i); QString fontName = FontManager::getInstance().verseFontname( - m_quranDb.verseType(), verse.page()); + m_config.verseType(), verse.page()); QFrame* frame = new QFrame(ui->scrlBookmarks); frame->setProperty("bookmark", true); @@ -139,7 +139,7 @@ BookmarksDialog::loadBookmarks(int surah) m_quranDb.surahNames().at(verse.surah() - 1) + " - " + tr("Verse: ") + QString::number(verse.number()); QString glyphs = - m_quranDb.verseType() == Configuration::Qcf + m_config.verseType() == Configuration::Qcf ? m_glpyhsDb.getVerseGlyphs(verse.surah(), verse.number()) : m_quranDb.verseText(verse.surah(), verse.number()); diff --git a/src/dialogs/contentdialog.cpp b/src/dialogs/contentdialog.cpp index 1a258b46..0e542d65 100644 --- a/src/dialogs/contentdialog.cpp +++ b/src/dialogs/contentdialog.cpp @@ -111,7 +111,7 @@ ContentDialog::showVerseThoughts(const Verse& v) } void -ContentDialog::setSideFont() +ContentDialog::loadSideFont() { QFont sideFont = qvariant_cast(m_config.settings().value("Reader/SideContentFont")); @@ -129,11 +129,11 @@ ContentDialog::setShownVerse(const Verse& newShownVerse) " - " + tr("Verse: ") + QString::number(m_shownVerse.number()); QString glyphs = - m_quranDb.verseType() == Configuration::Qcf + m_config.verseType() == Configuration::Qcf ? m_glyphsDb.getVerseGlyphs(m_shownVerse.surah(), m_shownVerse.number()) : m_quranDb.verseText(m_shownVerse.surah(), m_shownVerse.number()); QString fontFamily = FontManager::getInstance().getInstance().verseFontname( - m_quranDb.verseType(), m_shownVerse.page()); + m_config.verseType(), m_shownVerse.page()); ui->lbVerseInfo->setText(title); ui->lbVerseText->setWordWrap(true); @@ -212,7 +212,7 @@ void ContentDialog::loadContent(Mode mode) { m_internalLoading = true; - setSideFont(); + loadSideFont(); ui->cmbType->setCurrentIndex((int)mode); updateContentComboBox(mode); switch (mode) { @@ -253,9 +253,8 @@ void ContentDialog::cmbLoadTafasir() { for (int i = 0; i < m_tafasir.size(); i++) { - const ::Tafsir& t = m_tafasir.at(i); - if (t.isAvailable()) - ui->cmbContent->addItem(t.displayName(), i); + if (m_tafasir.at(i).isAvailable()) + ui->cmbContent->addItem(m_tafasir.at(i).displayName(), i); } int idx = ui->cmbContent->findData(m_tafsir); @@ -266,9 +265,8 @@ void ContentDialog::cmbLoadTranslations() { for (int i = 0; i < m_translations.size(); i++) { - const ::Translation& tr = m_translations[i]; - if (tr.isAvailable()) - ui->cmbContent->addItem(tr.displayName(), i); + if (m_translations.at(i).isAvailable()) + ui->cmbContent->addItem(m_translations.at(i).displayName(), i); } int idx = ui->cmbContent->findData(m_translation); diff --git a/src/dialogs/copydialog.cpp b/src/dialogs/copydialog.cpp index b45913af..a6294da8 100644 --- a/src/dialogs/copydialog.cpp +++ b/src/dialogs/copydialog.cpp @@ -60,14 +60,15 @@ CopyDialog::copyRange() void CopyDialog::show() { - ui->cmbCopyFrom->clear(); - ui->cmbCopyTo->clear(); ui->lbCopySurahName->setText(m_quranDb.surahName(m_currVerse.surah())); + ui->cmbCopyFrom->clear(); + ui->cmbCopyTo->clear(); for (int i = 1; i <= m_currVerse.surahCount(); i++) { ui->cmbCopyFrom->addItem(QString::number(i)); ui->cmbCopyTo->addItem(QString::number(i)); } + int idx = m_currVerse.number() ? m_currVerse.number() - 1 : 0; ui->cmbCopyFrom->setCurrentIndex(idx); ui->cmbCopyTo->setCurrentIndex(idx); diff --git a/src/dialogs/searchdialog.cpp b/src/dialogs/searchdialog.cpp index 209d3e8c..b3057f5e 100644 --- a/src/dialogs/searchdialog.cpp +++ b/src/dialogs/searchdialog.cpp @@ -116,7 +116,7 @@ SearchDialog::showResults() for (int i = m_startResult; i < endIdx; i++) { Verse v = m_currResults.at(i); QString fontName = - FontManager::getInstance().verseFontname(m_quranDb.verseType(), v.page()); + FontManager::getInstance().verseFontname(m_config.verseType(), v.page()); VerseFrame* vFrame = new VerseFrame(ui->srclResults); QLabel* lbInfo = new QLabel(vFrame); @@ -124,7 +124,7 @@ SearchDialog::showResults() QString info = tr("Surah: ") + m_quranDb.surahNames().at(v.surah() - 1) + " - " + tr("Verse: ") + QString::number(v.number()); - QString glyphs = m_quranDb.verseType() == Configuration::Qcf + QString glyphs = m_config.verseType() == Configuration::Qcf ? m_glyphsDb.getVerseGlyphs(v.surah(), v.number()) : m_quranDb.verseText(v.surah(), v.number()); diff --git a/src/utils/configuration.cpp b/src/utils/configuration.cpp index e26edba6..c817f589 100644 --- a/src/utils/configuration.cpp +++ b/src/utils/configuration.cpp @@ -123,3 +123,15 @@ Configuration::readerMode() const { return m_readerMode; } + +Configuration::VerseType +Configuration::verseType() const +{ + return m_verseType; +} + +void +Configuration::setVerseType(VerseType newVerseType) +{ + m_verseType = newVerseType; +} diff --git a/src/utils/configuration.h b/src/utils/configuration.h index 00d1eb67..5bd840ad 100644 --- a/src/utils/configuration.h +++ b/src/utils/configuration.h @@ -37,6 +37,8 @@ class Configuration int qcfVersion() const; QLocale::Language language() const; ReaderMode readerMode() const; + VerseType verseType() const; + void setVerseType(VerseType newVerseType); private: Configuration(); @@ -46,6 +48,7 @@ class Configuration QLocale::Language m_language; QSettings m_settings; ReaderMode m_readerMode; + VerseType m_verseType; }; #endif // CONFIGURATION_H From 8a77532db34706f30140d174530d3b0489988f11 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Sun, 3 Mar 2024 23:14:40 +0200 Subject: [PATCH 46/51] docs: document more dialogs --- src/core/playercontrols.h | 5 ++ src/core/quranreader.cpp | 10 +-- src/core/quranreader.h | 5 ++ src/database/betaqatdb.h | 4 ++ src/database/bookmarksdb.h | 5 ++ src/database/glyphsdb.h | 21 ++++++ src/database/qurandb.cpp | 14 +--- src/database/qurandb.h | 33 +++++---- src/database/tafsirdb.h | 29 +++++++- src/database/translationdb.h | 31 ++++++-- src/dialogs/aboutdialog.h | 12 ++++ src/dialogs/bookmarksdialog.h | 15 +++- src/dialogs/contentdialog.h | 126 +++++++++++++++++++-------------- src/dialogs/copydialog.h | 32 ++++++++- src/dialogs/downloaderdialog.h | 18 ++--- 15 files changed, 258 insertions(+), 102 deletions(-) diff --git a/src/core/playercontrols.h b/src/core/playercontrols.h index 1c26e245..97c957a9 100644 --- a/src/core/playercontrols.h +++ b/src/core/playercontrols.h @@ -10,6 +10,11 @@ namespace Ui { class PlayerControls; } +/** + * @class PlayerControls + * @brief PlayerControls class provides a widget for controlling the + * playback of audio verses from the Quran. + */ class PlayerControls : public QWidget { Q_OBJECT diff --git a/src/core/quranreader.cpp b/src/core/quranreader.cpp index 7ec1dd34..90aa9a29 100644 --- a/src/core/quranreader.cpp +++ b/src/core/quranreader.cpp @@ -164,13 +164,13 @@ QuranReader::updateSideFont() void QuranReader::updateVerseType() { - VerseType type = - qvariant_cast(m_config.settings().value("Reader/VerseType")); + Configuration::VerseType type = qvariant_cast( + m_config.settings().value("Reader/VerseType")); m_versesFont.setFamily( FontManager::getInstance().verseFontname(type, m_currVerse.page())); m_versesFont.setPointSize( m_config.settings().value("Reader/VerseFontSize").toInt()); - m_quranDb.setVerseType(type); + m_config.setVerseType(type); } void @@ -223,7 +223,7 @@ QuranReader::addSideContent() QLabel* contentLb; VerseFrame* verseContFrame; QString prevLbContent, currLbContent, glyphs; - if (m_quranDb.verseType() == Configuration::Qcf) + if (m_config.verseType() == Configuration::Qcf) m_versesFont.setFamily( FontManager::getInstance().pageFontname(m_currVerse.page())); @@ -235,7 +235,7 @@ QuranReader::addSideContent() verseContFrame = new VerseFrame(m_scrlVerseByVerse->widget()); verselb = new ClickableLabel(verseContFrame); contentLb = new QLabel(verseContFrame); - glyphs = m_quranDb.verseType() == Configuration::Qcf + glyphs = m_config.verseType() == Configuration::Qcf ? m_glyphsDb.getVerseGlyphs(verse->surah(), verse->number()) : m_quranDb.verseText(verse->surah(), verse->number()); diff --git a/src/core/quranreader.h b/src/core/quranreader.h index cd117251..d2a1e531 100644 --- a/src/core/quranreader.h +++ b/src/core/quranreader.h @@ -17,6 +17,11 @@ namespace Ui { class QuranReader; } +/** + * @class QuranReader + * @brief The QuranReader class is responsible for displaying the Quran page(s) + * and the side content + */ class QuranReader : public QWidget { Q_OBJECT diff --git a/src/database/betaqatdb.h b/src/database/betaqatdb.h index d3d7d078..c52edf91 100644 --- a/src/database/betaqatdb.h +++ b/src/database/betaqatdb.h @@ -7,6 +7,10 @@ #include #include +/** + * @class BetaqatDb + * @brief The BetaqatDb class represents a connection to the betaqat db file + */ class BetaqatDb : public DbConnection , QSqlDatabase diff --git a/src/database/bookmarksdb.h b/src/database/bookmarksdb.h index 08b0eb91..bf734d86 100644 --- a/src/database/bookmarksdb.h +++ b/src/database/bookmarksdb.h @@ -12,6 +12,11 @@ #include #include +/** + * @class BookmarksDb + * @brief The BookmarksDb class represents a connection to the bookmarks db file + * used for storing user data (bookmarks,khatmah,thoughts) + */ class BookmarksDb : public DbConnection , QSqlDatabase diff --git a/src/database/glyphsdb.h b/src/database/glyphsdb.h index d6885c55..9dafd8ab 100644 --- a/src/database/glyphsdb.h +++ b/src/database/glyphsdb.h @@ -8,13 +8,28 @@ #include #include +/** + * @class GlyphsDb + * @brief The GlyphsDb class represents a connection to the glyphs db file + */ class GlyphsDb : public DbConnection , QSqlDatabase { public: + /** + * @brief get a reference to the single class instance + * @return reference to the static class instance + */ static GlyphsDb& getInstance(); + /** + * @brief sets and opens the sqlite database file + */ void open(); + /** + * @brief getter for the type of the connection + * @return - DbConnection::Glyphs + */ Type type(); /** * @brief get Quran page QCF glyphs separated as lines @@ -45,7 +60,13 @@ class GlyphsDb private: GlyphsDb(); + /** + * @brief reference to the singleton Configuration instance + */ Configuration& m_config; + /** + * @brief reference to the app assets directory + */ const QDir& m_assetsDir; }; diff --git a/src/database/qurandb.cpp b/src/database/qurandb.cpp index f538467a..5b2ea980 100644 --- a/src/database/qurandb.cpp +++ b/src/database/qurandb.cpp @@ -129,7 +129,7 @@ QString QuranDb::verseText(const int sIdx, const int vIdx) const { QSqlQuery dbQuery(*this); - if (m_verseType == VerseType::Annotated) + if (m_config.verseType() == Configuration::Annotated) dbQuery.prepare("SELECT aya_text_annotated FROM verses_v1 WHERE sura_no=:s " "AND aya_no=:v"); else @@ -332,18 +332,6 @@ QuranDb::randomVerse() const dbQuery.value(2).toInt() }; } -void -QuranDb::setVerseType(VerseType newVerseType) -{ - m_verseType = newVerseType; -} - -VerseType -QuranDb::verseType() const -{ - return m_verseType; -} - QStringList QuranDb::surahNames() const { diff --git a/src/database/qurandb.h b/src/database/qurandb.h index 26fb05c5..3c4e2605 100644 --- a/src/database/qurandb.h +++ b/src/database/qurandb.h @@ -7,15 +7,29 @@ #include #include #include -typedef Configuration::VerseType VerseType; +/** + * @class QuranDb + * @brief The QuranDb class represents a connection to the quran db file + */ class QuranDb : public DbConnection , QSqlDatabase { public: + /** + * @brief get a reference to the single class instance + * @return reference to the static class instance + */ static QuranDb& getInstance(); + /** + * @brief sets and opens the sqlite database file + */ void open(); + /** + * @brief getter for the type of the connection + * @return - DbConnection::Quran + */ Type type(); /** * @brief gets the surah number and juz number of the first verse in the page, @@ -115,16 +129,6 @@ class QuranDb * @return QPair of Verse instance and verse text */ QList randomVerse() const; - /** - * @brief setVerseType - * @param newVerseType - */ - void setVerseType(VerseType newVerseType); - /** - * @brief verseType - * @return - */ - VerseType verseType() const; /** * @brief surahNames * @return @@ -133,9 +137,14 @@ class QuranDb private: QuranDb(); + /** + * @brief reference to the singleton Configuration instance + */ Configuration& m_config; + /** + * @brief reference to the app assets directory + */ const QDir& m_assetsDir; - VerseType m_verseType; /** * @brief QList of sura names (Arabic if UI language is Arabic, Otherwise * English) diff --git a/src/database/tafsirdb.h b/src/database/tafsirdb.h index 30315620..5489a4c0 100644 --- a/src/database/tafsirdb.h +++ b/src/database/tafsirdb.h @@ -10,14 +10,30 @@ #include #include +/** + * @class TafsirDb + * @brief The TafsirDb class represents a connection to the currently selected + * tafsir db + */ class TafsirDb : public DbConnection , QSqlDatabase { public: + /** + * @brief get a reference to the single class instance + * @return reference to the static class instance + */ static TafsirDb& getInstance(); + /** + * @brief sets and opens the sqlite database file + */ void open(); + /** + * @brief getter for the type of the connection + * @return - DbConnection::Tafsir + */ Type type(); /** * @brief set tafsir to the one in the settings, update the selected db @@ -38,17 +54,26 @@ class TafsirDb QString getTafsir(const int sIdx, const int vIdx); /** * @brief getter for m_currTafsir - * @return the currently set DBManager::Tafasir + * @return pointer to the currently selected Tafasir */ const ::Tafsir* currTafsir() const; private: TafsirDb(); + /** + * @brief reference to the singleton Configuration instance + */ Configuration& m_config; + /** + * @brief reference to the singleton DirManager instance + */ const DirManager& m_dirMgr; + /** + * @brief reference to the static QList of available tafasir + */ const QList<::Tafsir>& m_tafasir; /** - * @brief the current active DBManager::Tafasir + * @brief pointer to the currently selected Tafsir */ const ::Tafsir* m_currTafsir; /** diff --git a/src/database/translationdb.h b/src/database/translationdb.h index a88e220f..4a42e34b 100644 --- a/src/database/translationdb.h +++ b/src/database/translationdb.h @@ -11,14 +11,30 @@ #include #include +/** + * @class TranslationDb + * @brief The TranslationDb class represents a connection to the currently + * selected translation db + */ class TranslationDb : public DbConnection , QSqlDatabase { Q_OBJECT public: + /** + * @brief get a reference to the single class instance + * @return reference to the static class instance + */ static TranslationDb& getInstance(); + /** + * @brief sets and opens the sqlite database file + */ void open(); + /** + * @brief getter for the type of the connection + * @return - DbConnection::Translation + */ Type type(); /** * @brief sets the active translation @@ -34,10 +50,8 @@ class TranslationDb */ QString getTranslation(const int sIdx, const int vIdx) const; /** - * @brief currTranslation - * @return - * - * MODIFIED + * @brief getter for m_currTr + * @return pointer to the currently selected translation */ const ::Translation* currTranslation() const; @@ -49,8 +63,17 @@ public slots: private: TranslationDb(); + /** + * @brief reference to the singleton Configuration instance + */ Configuration& m_config; + /** + * @brief reference to the singleton DirManager instance + */ const DirManager& m_dirMgr; + /** + * @brief reference to the static QList of available translations + */ const QList<::Translation>& m_translations; /** * @brief the current active DBManager::Translation diff --git a/src/dialogs/aboutdialog.h b/src/dialogs/aboutdialog.h index 89019b99..8618d818 100644 --- a/src/dialogs/aboutdialog.h +++ b/src/dialogs/aboutdialog.h @@ -9,16 +9,28 @@ namespace Ui { class AboutDialog; } +/** + * @class AboutDialog + * @brief AboutDialog class is used for displaying information about the + * application + */ class AboutDialog : public QDialog { Q_OBJECT public: + /** + * @brief class constructor + * @param parent - pointer to parent widget + */ explicit AboutDialog(QWidget* parent = nullptr); ~AboutDialog(); private: Ui::AboutDialog* ui; + /** + * @brief reference to the singleton Configuration instance + */ const Configuration& m_config; }; diff --git a/src/dialogs/bookmarksdialog.h b/src/dialogs/bookmarksdialog.h index 3750aa07..f8bd941e 100644 --- a/src/dialogs/bookmarksdialog.h +++ b/src/dialogs/bookmarksdialog.h @@ -25,8 +25,7 @@ class BookmarksDialog; /** * @brief BookmarksDialog is the interface for interacting with saved bookmarks. * @details Bookmarks allow the user to save a verse for easier & faster - * access. Bookmarks are represented as entries in a sqlite database. Thus, a - * DBManager instance is required to create an instance of BookmarksDialog. + * access. Bookmarks are represented as entries in a sqlite database. */ class BookmarksDialog : public QDialog { @@ -119,9 +118,21 @@ private slots: private: Ui::BookmarksDialog* ui; + /** + * @brief reference to the singleton Configuration instance + */ const Configuration& m_config; + /** + * @brief reference to the singleton GlyphsDb instance + */ const GlyphsDb& m_glpyhsDb; + /** + * @brief reference to the singleton QuranDb instance + */ const QuranDb& m_quranDb; + /** + * @brief reference to the singleton BookmarksDb instance + */ BookmarksDb& m_bookmarksDb; /** * @brief connects signals and slots for different UI diff --git a/src/dialogs/contentdialog.h b/src/dialogs/contentdialog.h index 477d9029..d76a4138 100644 --- a/src/dialogs/contentdialog.h +++ b/src/dialogs/contentdialog.h @@ -22,11 +22,8 @@ class ContentDialog; } /** - * @brief ContentDialog interface for reading Quran tafsir. - * @details Tafsir is shown for a single verse at a time. Navigation between - * verses is independant of the main Quran reader navigation for easier - * navigation. Tafsir is displayed using the side content font set in the - * Settings::settings. + * @brief ContentDialog is used to display any additional content (tafsir, + * translation, thoughts). */ class ContentDialog : public QDialog { @@ -48,11 +45,22 @@ class ContentDialog : public QDialog public slots: /** - * @brief open ContentDialog with the shown verse set to the given Verse + * @brief open ContentDialog with the shown verse set to the given Verse in + * Mode::Tafsir * @param v - Verse to show the tafsir of */ void showVerseTafsir(const Verse& v); + /** + * @brief open ContentDialog with the shown verse set to the given Verse in + * Mode::Translation + * @param v - Verse to show the translation of + */ void showVerseTranslation(const Verse& v); + /** + * @brief open ContentDialog with the shown verse set to the given Verse in + * Mode::Thoughts + * @param v - Verse to show the thoughts of + */ void showVerseThoughts(const Verse& v); protected: @@ -68,120 +76,133 @@ public slots: private slots: /** - * @brief contentChanged - * - * MODIFIED + * @brief callback for calling the appropriate method when the selected item + * in the secondary combobox changes */ void contentChanged(); /** - * @brief typeChanged - * - * MODIFIED + * @brief callback for changing the ContentDialog::Mode when the selected mode + * in the primary combobox changes */ void typeChanged(); /** - * @brief increment the ContentDialog::m_shownVerse and load the new verse + * @brief increment the m_shownVerse and load the new verse * tafsir. */ void btnNextClicked(); /** - * @brief decrement the ContentDialog::m_shownVerse and load the new verse + * @brief decrement the m_shownVerse and load the new verse * tafsir. */ void btnPrevClicked(); private: Ui::ContentDialog* ui; + /** + * @brief reference to the singleton Configuration instance + */ Configuration& m_config; + /** + * @brief reference to the singleton TafsirDb instance + */ TafsirDb& m_tafsirDb; + /** + * @brief reference to the singleton TranslationDb instance + */ TranslationDb& m_translationDb; + /** + * @brief reference to the singleton BookmarksDb instance + */ BookmarksDb& m_bookmarksDb; + /** + * @brief reference to the singleton QuranDb instance + */ const QuranDb& m_quranDb; + /** + * @brief reference to the singleton GlyphsDb instance + */ const GlyphsDb& m_glyphsDb; + /** + * @brief reference to the static QList of available tafasir + */ const QList<::Tafsir>& m_tafasir; + /** + * @brief reference to the static QList of available translations + */ const QList<::Translation>& m_translations; /** - * @brief setSideFont - * - * MODIFIED + * @brief loads the QFont selected for the side content */ - void setSideFont(); + void loadSideFont(); /** * @brief connects signals and slots for different UI * components and shortcuts. */ void setupConnections(); /** - * @brief setter member for ContentDialog::m_shownVerse + * @brief setter member for m_shownVerse * @param newShownVerse */ void setShownVerse(const Verse& newShownVerse); /** - * @brief loadContent - * @param mode - * - * MODIFIED + * @brief loads the content of Mode given for the currently set Verse + * @param mode - Mode of the content shown */ void loadContent(Mode mode); /** - * @brief updateContentComboBox - * - * MODIFIED + * @brief updates the second combobox according to the given Mode */ void updateContentComboBox(Mode mode); /** - * @brief cmbLoadTafasir - * - * MODIFIED + * @brief load all available tafasir in the secondary combobox */ void cmbLoadTafasir(); /** - * @brief cmbLoadTranslations - * - * MODIFIED + * @brief load all available translations in the secondary combobox */ void cmbLoadTranslations(); /** - * @brief tafsirChanged - * - * MODIFIED + * @brief callback function for changing the selected tafsir in the secondary + * combobox */ void tafsirChanged(); /** - * @brief translationChanged - * - * MODIFIED + * @brief callback function for changing the selected translation in the + * secondary combobox */ void translationChanged(); /** - * @brief loads the info and tafsir of ContentDialog::m_shownVerse - * - * MODIFIED + * @brief loads the tafsir for the currently shown verse in the QTextEdit + * widget */ void loadVerseTafsir(); /** - * @brief loadVerseTranslation - * - * MODIFIED + * @brief loads the selected translation for the currently shown verse in the + * QTextEdit widget */ void loadVerseTranslation(); /** - * @brief loadVerseThoughts - * - * MODIFIED + * @brief loads the user stored thoughts for the currently shown verse in the + * QTextEdit widget and enables editing */ void loadVerseThoughts(); /** - * MODIFIED + * @brief saves the user stored thoughts for the currently shown verse in the + * QTextEdit widget and disables editing */ void saveVerseThoughts(); /** - * @brief m_currMode - * - * MODIFIED + * @brief the current Mode of the ContentDialog */ Mode m_currMode; + /** + * @brief index of the currently shown Tafsir in Tafsir::tafasir + */ int m_tafsir; + /** + * @brief index of the currently shown Translation in + * Translation::translations + */ int m_translation; /** * @brief fixed font size for the verse text displayed above the tafsir. @@ -192,7 +213,8 @@ private slots: */ Verse m_shownVerse; /** - * @brief m_internalLoading + * @brief boolean to indicate internal loading of content in the secondary + * combobox */ bool m_internalLoading; }; diff --git a/src/dialogs/copydialog.h b/src/dialogs/copydialog.h index bca0d435..08056137 100644 --- a/src/dialogs/copydialog.h +++ b/src/dialogs/copydialog.h @@ -12,15 +12,32 @@ namespace Ui { class CopyDialog; } - +/** + * @class CopyDialog + * @brief The CopyDialog class is used for selecting a range of verses within + * the current surah to copy to clipboard or copying the current Verse + */ class CopyDialog : public QDialog { Q_OBJECT public: + /** + * @brief class consrtuctor + * @param parent - pointer to parent widget + */ explicit CopyDialog(QWidget* parent); ~CopyDialog(); + /** + * @brief sets the currently active surah name in the corresponding QLabel and + * fills the range selection comboboxes before showing the CopyDialog + */ void show(); + /** + * @brief getter for the address of the CopyNotifier used for sending + * notifications + * @return pointer to CopyNotifier instance + */ const CopyNotifier* notifier() const; public slots: @@ -38,9 +55,22 @@ private slots: private: Ui::CopyDialog* ui; + /** + * @brief reference to the singleton QuranDb instance + */ const QuranDb& m_quranDb; + /** + * @brief reference to the shared current verse instance + */ const Verse& m_currVerse; + /** + * @brief pointer to QIntValidator used for validating the selected range of + * verses + */ QPointer m_verseValidator; + /** + * @brief CopyNotifier instance used for sending notifications + */ CopyNotifier m_notifier; }; diff --git a/src/dialogs/downloaderdialog.h b/src/dialogs/downloaderdialog.h index 8c4556e3..7b7b4358 100644 --- a/src/dialogs/downloaderdialog.h +++ b/src/dialogs/downloaderdialog.h @@ -39,10 +39,10 @@ class DownloaderDialog : public QDialog public: /** - * @brief Class constructor + * @brief class contructor * @param parent - pointer to parent widget - * @param downloader - pointer to a DownloadManager instance - * @param dbMan - pointer to DBManager instance + * @param manager - pointer to the JobManager instance used for managing + * DownloadJob(s) */ explicit DownloaderDialog(QWidget* parent = nullptr, JobManager* manager = nullptr); @@ -50,22 +50,18 @@ class DownloaderDialog : public QDialog public slots: /** - * @brief Adds the currently selected surahs in the QTreeView to the + * @brief Adds the currently selected jobs in the QTreeView to the * DownloadManager download queue. - * @details Adding surah to download queue is done through getting the surah - * length through the database instance, then adding download tasks for every - * verse in the surah selected. */ void addToQueue(); /** - * @brief Sets the currently active download - * task progress bar in order to update displayed info. + * @brief Sets the currently active download task progress bar in order to + * update displayed info. * @details This is typically the first incomplete bar in the dialog. */ void setCurrentBar(); /** - * @brief slot to delete all download tasks / - * progress bars from dialog + * @brief slot to delete all download tasks/progress bars from dialog */ void downloadAborted(); /** From 1c236ec67efc1b3a132854e35df572f78d737531 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:45:32 +0200 Subject: [PATCH 47/51] fix: bug when config file does not exist --- src/main.cpp | 1 - src/utils/configuration.cpp | 7 ++++--- src/widgets/quranpagebrowser.cpp | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 09468453..0a352158 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -36,7 +36,6 @@ main(int argc, char* argv[]) Logger::startLogger(DirManager::getInstance().configDir().absolutePath()); Logger::attach(); - Configuration::getInstance().checkGroups(); Configuration::getInstance().loadUiTranslation(); ShortcutHandler::getInstance().populateDescriptionMap(); StyleManager::getInstance().loadTheme(); diff --git a/src/utils/configuration.cpp b/src/utils/configuration.cpp index c817f589..4a69c689 100644 --- a/src/utils/configuration.cpp +++ b/src/utils/configuration.cpp @@ -13,10 +13,11 @@ Configuration::getInstance() } Configuration::Configuration() - : m_settings( - DirManager::getInstance().configDir().filePath("qurancompanion.conf"), - QSettings::IniFormat) + : m_settings(DirManager::getInstance().configDir().absoluteFilePath( + "qurancompanion.conf"), + QSettings::IniFormat) { + checkGroups(); m_themeId = m_settings.value("Theme").toInt(); m_qcfVersion = m_settings.value("Reader/QCF").toInt(); m_language = qvariant_cast(m_settings.value("Language")); diff --git a/src/widgets/quranpagebrowser.cpp b/src/widgets/quranpagebrowser.cpp index 1b5e9843..d9760dd0 100644 --- a/src/widgets/quranpagebrowser.cpp +++ b/src/widgets/quranpagebrowser.cpp @@ -194,7 +194,6 @@ QuranPageBrowser::constructPage(int pageNo, bool forceCustomSize) setHref(&textCursor, 1, "#F" + QString::number(m_headerData.first)); } - qDebug() << "PARENT SIZE: " << parentWidget()->height(); parentWidget()->setMinimumWidth(m_pageLineSize.width() + 70); // page lines drawing From e52f2cce5a5c5cc457f833ddd42e3e4d8588312a Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Tue, 5 Mar 2024 16:20:25 +0200 Subject: [PATCH 48/51] refactor: move download progressbar value changing into downloadprogressbar class --- src/dialogs/downloaderdialog.cpp | 10 ++++------ src/widgets/downloadprogressbar.cpp | 29 +++++++++++++++++++++++++++-- src/widgets/downloadprogressbar.h | 7 ++++++- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/dialogs/downloaderdialog.cpp b/src/dialogs/downloaderdialog.cpp index 1e385cd4..90cfb4cf 100644 --- a/src/dialogs/downloaderdialog.cpp +++ b/src/dialogs/downloaderdialog.cpp @@ -369,12 +369,8 @@ DownloaderDialog::downloadCompleted(QSharedPointer finished) QSharedPointer sj = finished.dynamicCast(); removeFromDownloading(sj->reciter(), sj->surah()); } - if (finished->type() == DownloadJob::TafsirFile || - finished->type() == DownloadJob::TranslationFile) { - m_currentBar->setValue(1); - m_currentBar->setMaximum(1); - m_currentBar->setFormat("1 / 1"); - } + + m_currentBar->finished(); m_finishedFrames.append(m_frameLst.front()); m_frameLst.pop_front(); setCurrentBar(); @@ -396,6 +392,8 @@ DownloaderDialog::topTaskDownloadError(QSharedPointer failed) QSharedPointer sj = failed.dynamicCast(); removeFromDownloading(sj->reciter(), sj->surah()); } + + m_currentBar->failed(); m_finishedFrames.append(m_frameLst.front()); m_frameLst.pop_front(); setCurrentBar(); diff --git a/src/widgets/downloadprogressbar.cpp b/src/widgets/downloadprogressbar.cpp index 17546a71..6696b103 100644 --- a/src/widgets/downloadprogressbar.cpp +++ b/src/widgets/downloadprogressbar.cpp @@ -10,17 +10,20 @@ DownloadProgressBar::DownloadProgressBar(QWidget* parent, Type type, int max) : QProgressBar(parent) + , m_type(type) { setStyling(downloading); setMaximum(max); setValue(0); - if (type == DownloadJob::TafsirFile || type == DownloadJob::TranslationFile) + if (m_type == DownloadJob::TafsirFile || + m_type == DownloadJob::TranslationFile) setFormat("%v / %m " + qApp->translate("DownloadManager", "KB")); else setFormat("%v / %m"); } -void DownloadProgressBar::updateProgress(QSharedPointer job) +void +DownloadProgressBar::updateProgress(QSharedPointer job) { if (maximum() != job->total()) setMaximum(job->total()); @@ -28,6 +31,28 @@ void DownloadProgressBar::updateProgress(QSharedPointer job) setValue(job->completed()); } +void +DownloadProgressBar::finished() +{ + if (m_type == DownloadJob::TafsirFile || + m_type == DownloadJob::TranslationFile) { + setValue(1); + setMaximum(1); + } + setFormat("%v / %m"); +} + +void +DownloadProgressBar::failed() +{ + if (m_type == DownloadJob::TafsirFile || + m_type == DownloadJob::TranslationFile) { + setValue(0); + setMaximum(1); + } + setFormat("%v / %m"); +} + void DownloadProgressBar::setStyling(State downState) { diff --git a/src/widgets/downloadprogressbar.h b/src/widgets/downloadprogressbar.h index 8e68871f..cbf2c2ce 100644 --- a/src/widgets/downloadprogressbar.h +++ b/src/widgets/downloadprogressbar.h @@ -36,8 +36,13 @@ class DownloadProgressBar : public QProgressBar }; public slots: - void updateProgress(QSharedPointer job); void setStyling(State); + void updateProgress(QSharedPointer job); + void finished(); + void failed(); + +private: + Type m_type; }; #endif // DOWNLOADPROGRESSBAR_H From 8b3bd09f78c58296434b60a262872841d2b96dd1 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Tue, 5 Mar 2024 16:47:36 +0200 Subject: [PATCH 49/51] release: update sources --- CHANGELOG.md | 34 ++++++++++++++-------------------- README-AR.md | 2 +- README.md | 2 +- VERSION | 2 +- src/main.cpp | 2 +- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba2c1cbd..e09ca31b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,27 +2,21 @@ ### التحديثات 💭 -- تصليح بعض الأعطال -- إضافة خطوط مختلفة للآيات -- إضافة نص الآيات المرمز ([#31](https://github.com/0xzer0x/quran-companion/pull/31#issuecomment-1872679733)) -- إزالة ملفات خط QCF 2 من البرنامج و إضافة إمكانية تحميله (تقليص حجم البرنامج) -- إزالة ملفات التفاسير و الترجمات من البرنامج و إضافة إمكانية تحمليها (تقليص حجم البرنامج) -- إضافة خاصية بطاقات السور (الضغط على إطار السورة/اسم السورة في أعلى الصفحة) -- إضافة اختصار لإخفاء التحكم في المشغل من الواجهة الأساسية -- استبدال حجم القارئ المتغير بخاصية تغيير وضع القراءة -- إضافة تفسير الجلالين (إنجليزية) -- نافذة "عن البرنامج" أفضل +- تصليح بعض الأعطال (#49) +- إضافة ترجمة بكتال - إنجليزية (#46) +- تعطيل التشغيل التلقائي عند الذهاب لسورة (#50) +- إضافة خاصية _خواطر_ (#44) +- نافذة التفسير أصبحت تشمل كل أنواع المحتوى (تفاسير، ترجمة، خواطر) (#43 ,#47) +- نقل تغيير إعدادات التفسير لنافذة المحتوى +- إضافة خاصية استيراد/تصدير بيانات المستخدم ### What's Changed 💭 -- Bugfixes -- Added different fonts for displaying verses -- Added annotated Hafs verse text ([#31](https://github.com/0xzer0x/quran-companion/pull/31#issuecomment-1872679733)) -- Move QCF 2 font files out of application bundle and added option to download it (Reduced bundle size) -- Move tafsir and translation files out of the application bundle and added option to download any one of them (Reduced bundle size) -- Added Surah card functionality (Accessed by clicking the Surah frame/clicking the Surah name in the page header) -- Added new shortcut to hide player controls -- Replaced the dynamic resizing of reader panels with panel toggling functionality -- Added Tafsir Al-Jalalayn (English) -- Better "About" dialog \ No newline at end of file +- Bugfixes (#49) +- Added Pickthall - English translation (#46) +- Disabled Auto-play when navigating to a Surah (#50) +- Added _Thoughts_ feature (#44) +- Changed tafsir dialog to display all available content types (tafsir, translation, thoughts) (#43 ,#47) +- Move tafsir setting to content dialog +- Added user data import/export functionality \ No newline at end of file diff --git a/README-AR.md b/README-AR.md index 7b935965..228db921 100644 --- a/README-AR.md +++ b/README-AR.md @@ -133,7 +133,7 @@ Download Flatpak - + Download AppImage diff --git a/README.md b/README.md index 2f79b543..cf0da5e9 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ The application can be downloaded in any of the available packages (snap, flatpa Download Flatpak - + Download AppImage diff --git a/VERSION b/VERSION index 0495c4a8..e8ea05db 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.3 +1.2.4 diff --git a/src/main.cpp b/src/main.cpp index 0a352158..e5f82773 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,7 +28,7 @@ main(int argc, char* argv[]) QApplication a(argc, argv); QApplication::setApplicationName("Quran Companion"); QApplication::setOrganizationName("0xzer0x"); - QApplication::setApplicationVersion("1.2.3"); + QApplication::setApplicationVersion("1.2.4"); QSplashScreen splash(QPixmap(":/resources/splash.png")); splash.show(); From cb32537b4243fe46de82f90a0316d03b047a5625 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Tue, 5 Mar 2024 17:21:45 +0200 Subject: [PATCH 50/51] release: ramadan kareem --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e09ca31b..ecfdc3ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## Ramadan Kareem - رمضان كريم 🕌 +
### التحديثات 💭 From c66ff92d913bbe193f6c8be52d82c253c0b46f77 Mon Sep 17 00:00:00 2001 From: Youssef Fathy <32334265+0xzer0x@users.noreply.github.com> Date: Tue, 5 Mar 2024 17:35:44 +0200 Subject: [PATCH 51/51] release: update snapcraft.yaml and xdg metainfo files --- ...io.github._0xzer0x.qurancompanion.metainfo.xml | 15 ++++++++++++++- snap/snapcraft.yaml | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/dist/xdg/io.github._0xzer0x.qurancompanion.metainfo.xml b/dist/xdg/io.github._0xzer0x.qurancompanion.metainfo.xml index 3e0e2dc5..bf06db88 100644 --- a/dist/xdg/io.github._0xzer0x.qurancompanion.metainfo.xml +++ b/dist/xdg/io.github._0xzer0x.qurancompanion.metainfo.xml @@ -6,7 +6,7 @@ MIT LGPL-3.0-or-later - + Youssef Fathy @@ -44,6 +44,19 @@ + + +
    +
  • Bugfixes
  • +
  • Added Pickthall - English translation
  • +
  • Disabled Auto-play when navigating to a Surah
  • +
  • Added Thoughts feature
  • +
  • Changed tafsir dialog to display all available content types (tafsir, translation, thoughts)
  • +
  • Move tafsir setting to content dialog
  • +
  • Added user data import/export functionality
  • +
+
+
    diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index eb253ef3..3d81f402 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: quran-companion base: core22 -version: '1.2.3' +version: '1.2.4' title: Quran Companion website: https://github.com/0xzer0x/quran-companion source-code: https://github.com/0xzer0x/quran-companion