diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 6f49345..d7d9ec6 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -137,7 +137,7 @@ "monitor-directory-tooltip":"All ZIM files in this directory will be automatically added to the library.", "next-tab":"Move to next tab", "previous-tab":"Move to previous tab", - "cancel-download": "Cancel Download", + "cancel-download": "Cancel download", "cancel-download-text": "Are you sure you want to cancel the download of {{ZIM}}?", "delete-book": "Delete book", "delete-book-text": "Are you sure you want to delete {{ZIM}}?", diff --git a/src/contentmanager.cpp b/src/contentmanager.cpp index 26f323d..c64fff4 100644 --- a/src/contentmanager.cpp +++ b/src/contentmanager.cpp @@ -77,10 +77,10 @@ ContentManager::ContentManager(Library* library, kiwix::Downloader* downloader, setLanguages(); } -QList> ContentManager::getBooksList() +ContentManager::BookInfoList ContentManager::getBooksList() { const auto bookIds = getBookIds(); - QList> bookList; + BookInfoList bookList; QStringList keys = {"title", "tags", "date", "id", "size", "description", "faviconUrl"}; QIcon bookIcon; for (auto bookId : bookIds) { @@ -107,8 +107,8 @@ void ContentManager::onCustomContextMenu(const QPoint &point) QAction menuCancelBook(gt("cancel-download"), this); QAction menuOpenFolder(gt("open-folder"), this); - if (bookNode->isDownloading()) { - if (bookNode->getDownloadInfo().paused) { + if (const auto download = bookNode->getDownloadState()) { + if (download->getDownloadInfo().paused) { contextMenu.addAction(&menuResumeBook); } else { contextMenu.addAction(&menuPauseBook); @@ -216,9 +216,9 @@ void ContentManager::setLanguages() } #define ADD_V(KEY, METH) {if(key==KEY) values.insert(key, QString::fromStdString((b->METH())));} -QMap ContentManager::getBookInfos(QString id, const QStringList &keys) +ContentManager::BookInfo ContentManager::getBookInfos(QString id, const QStringList &keys) { - QMap values; + BookInfo values; const kiwix::Book* b = [=]()->const kiwix::Book* { try { return &mp_library->getBookById(id); @@ -681,13 +681,13 @@ void ContentManager::updateLibrary() { } catch (std::runtime_error&) {} } -#define CATALOG_URL "library.kiwix.org" void ContentManager::updateRemoteLibrary(const QString& content) { QtConcurrent::run([=]() { QMutexLocker locker(&remoteLibraryLocker); mp_remoteLibrary = kiwix::Library::create(); kiwix::Manager manager(mp_remoteLibrary); - manager.readOpds(content.toStdString(), CATALOG_URL); + const auto catalogUrl = m_remoteLibraryManager.getCatalogHost(); + manager.readOpds(content.toStdString(), catalogUrl.toStdString()); emit(this->booksChanged()); emit(this->pendingRequest(false)); }); diff --git a/src/contentmanager.h b/src/contentmanager.h index 84a75ba..f4d3b14 100644 --- a/src/contentmanager.h +++ b/src/contentmanager.h @@ -17,9 +17,13 @@ class ContentManager : public QObject Q_PROPERTY(QStringList downloadIds READ getDownloadIds NOTIFY downloadsChanged) Q_PROPERTY(bool isLocal MEMBER m_local READ isLocal WRITE setLocal NOTIFY localChanged) -public: +public: // types typedef QList> LanguageList; typedef QList> FilterList; + typedef ContentManagerModel::BookInfo BookInfo; + typedef ContentManagerModel::BookInfoList BookInfoList; + +public: // functions explicit ContentManager(Library* library, kiwix::Downloader *downloader, QObject *parent = nullptr); virtual ~ContentManager() {} @@ -51,7 +55,7 @@ private: QStringList getBookIds(); void eraseBookFilesFromComputer(const QString dirPath, const QString filename, const bool moveToTrash); - QList> getBooksList(); + BookInfoList getBooksList(); ContentManagerModel *managerModel; QMutex remoteLibraryLocker; void setCategories(); @@ -71,7 +75,7 @@ signals: public slots: QStringList getTranslations(const QStringList &keys); - QMap getBookInfos(QString id, const QStringList &keys); + BookInfo getBookInfos(QString id, const QStringList &keys); void openBook(const QString& id); QMap updateDownloadInfos(QString id, const QStringList& keys); QString downloadBook(const QString& id); diff --git a/src/contentmanagerdelegate.cpp b/src/contentmanagerdelegate.cpp index 4d7583a..8944026 100644 --- a/src/contentmanagerdelegate.cpp +++ b/src/contentmanagerdelegate.cpp @@ -177,8 +177,8 @@ void ContentManagerDelegate::paint(QPainter *painter, const QStyleOptionViewItem } QStyleOptionViewItem eOpt = option; if (index.column() == 5) { - if (node->isDownloading()) { - auto downloadInfo = node->getDownloadInfo(); + if (const auto downloadState = node->getDownloadState()) { + auto downloadInfo = downloadState->getDownloadInfo(); showDownloadProgress(painter, r, downloadInfo); } else { @@ -244,8 +244,8 @@ void ContentManagerDelegate::handleLastColumnClicked(const QModelIndex& index, Q int x = r.left(); int w = r.width(); - if (node->isDownloading()) { - if (node->getDownloadInfo().paused) { + if (const auto downloadState = node->getDownloadState()) { + if (downloadState->getDownloadInfo().paused) { if (clickX < (x + w/2)) { KiwixApp::instance()->getContentManager()->cancelBook(id, index); } else { diff --git a/src/contentmanagermodel.cpp b/src/contentmanagermodel.cpp index add8f71..11daf42 100644 --- a/src/contentmanagermodel.cpp +++ b/src/contentmanagermodel.cpp @@ -5,6 +5,7 @@ #include #include #include "kiwixapp.h" +#include ContentManagerModel::ContentManagerModel(QObject *parent) : QAbstractItemModel(parent) @@ -101,7 +102,7 @@ QVariant ContentManagerModel::headerData(int section, Qt::Orientation orientatio } } -void ContentManagerModel::setBooksData(const QList>& data) +void ContentManagerModel::setBooksData(const BookInfoList& data) { m_data = data; rootNode = std::shared_ptr(new RowNode({tr("Icon"), tr("Name"), tr("Date"), tr("Size"), tr("Content Type"), tr("Download")}, "", std::weak_ptr())); @@ -109,25 +110,52 @@ void ContentManagerModel::setBooksData(const QList>& dat emit dataChanged(QModelIndex(), QModelIndex()); } -QString convertToUnits(QString size) +std::shared_ptr ContentManagerModel::createNode(BookInfo bookItem, QMap iconMap) const { - QStringList units = {"bytes", "KB", "MB", "GB", "TB", "PB", "EB"}; - int unitIndex = 0; - auto bytes = size.toDouble(); - while (bytes >= 1024 && unitIndex < units.size()) { - bytes /= 1024; - unitIndex++; + auto faviconUrl = "https://" + bookItem["faviconUrl"].toString(); + QString id = bookItem["id"].toString(); + QByteArray bookIcon; + try { + auto book = KiwixApp::instance()->getLibrary()->getBookById(id); + std::string favicon; + auto item = book.getIllustration(48); + favicon = item->getData(); + bookIcon = QByteArray::fromRawData(reinterpret_cast(favicon.data()), favicon.size()); + bookIcon.detach(); // deep copy + } catch (...) { + if (iconMap.contains(faviconUrl)) { + bookIcon = iconMap[faviconUrl]; + } } + std::weak_ptr weakRoot = rootNode; + auto rowNodePtr = std::shared_ptr(new + RowNode({bookIcon, bookItem["title"], + bookItem["date"], + QString::fromStdString(kiwix::beautifyFileSize(bookItem["size"].toULongLong())), + bookItem["tags"] + }, id, weakRoot)); + std::weak_ptr weakRowNodePtr = rowNodePtr; + const auto descNodePtr = std::make_shared(DescriptionNode(bookItem["description"].toString(), weakRowNodePtr)); - const auto preciseBytes = QString::number(bytes, 'g', 3); - return preciseBytes + " " + units[unitIndex]; + rowNodePtr->appendChild(descNodePtr); + return rowNodePtr; } void ContentManagerModel::setupNodes() { beginResetModel(); + bookIdToRowMap.clear(); for (auto bookItem : m_data) { - rootNode->appendChild(RowNode::createNode(bookItem, iconMap, rootNode)); + const auto rowNode = createNode(bookItem, iconMap); + + // Restore download state during model updates (filtering, etc) + const auto downloadIter = m_downloads.constFind(rowNode->getBookId()); + if ( downloadIter != m_downloads.constEnd() ) { + rowNode->setDownloadState(downloadIter.value()); + } + + bookIdToRowMap[bookItem["id"].toString()] = rootNode->childCount(); + rootNode->appendChild(rowNode); } endResetModel(); } @@ -222,58 +250,68 @@ std::shared_ptr getSharedPointer(RowNode* ptr) void ContentManagerModel::startDownload(QModelIndex index) { auto node = getSharedPointer(static_cast(index.internalPointer())); - node->setIsDownloading(true); - auto id = node->getBookId(); - QTimer *timer = new QTimer(this); + const auto bookId = node->getBookId(); + const auto newDownload = std::make_shared(); + m_downloads[bookId] = newDownload; + node->setDownloadState(newDownload); + QTimer *timer = newDownload->getDownloadUpdateTimer(); connect(timer, &QTimer::timeout, this, [=]() { - auto downloadInfos = KiwixApp::instance()->getContentManager()->updateDownloadInfos(id, {"status", "completedLength", "totalLength", "downloadSpeed"}); - double percent = (double) downloadInfos["completedLength"].toInt() / downloadInfos["totalLength"].toInt(); - percent *= 100; - percent = QString::number(percent, 'g', 3).toDouble(); - auto completedLength = convertToUnits(downloadInfos["completedLength"].toString()); - auto downloadSpeed = convertToUnits(downloadInfos["downloadSpeed"].toString()) + "/s"; - node->setDownloadInfo({percent, completedLength, downloadSpeed}); - if (!downloadInfos["status"].isValid()) { - node->setIsDownloading(false); - timer->stop(); - timer->deleteLater(); - } - emit dataChanged(index, index); + updateDownload(bookId); }); - timer->start(1000); - timers[id] = timer; +} + +void ContentManagerModel::updateDownload(QString bookId) +{ + const auto download = m_downloads.value(bookId); + + if ( ! download ) + return; + + const bool downloadStillValid = download->update(bookId); + + // The download->update() call above may result in + // ContentManagerModel::setBooksData() being called (through a chain + // of signals), which in turn will rebuild bookIdToRowMap. Hence + // bookIdToRowMap access must happen after it. + + const auto it = bookIdToRowMap.constFind(bookId); + + if ( ! downloadStillValid ) { + m_downloads.remove(bookId); + if ( it != bookIdToRowMap.constEnd() ) { + const size_t row = it.value(); + RowNode& rowNode = static_cast(*rootNode->child(row)); + rowNode.setDownloadState(nullptr); + } + } + + if ( it != bookIdToRowMap.constEnd() ) { + const size_t row = it.value(); + const QModelIndex rootNodeIndex = this->index(0, 0); + const QModelIndex newIndex = this->index(row, 5, rootNodeIndex); + emit dataChanged(newIndex, newIndex); + } } void ContentManagerModel::pauseDownload(QModelIndex index) { auto node = static_cast(index.internalPointer()); - auto id = node->getBookId(); - auto prevDownloadInfo = node->getDownloadInfo(); - prevDownloadInfo.paused = true; - node->setDownloadInfo(prevDownloadInfo); - timers[id]->stop(); + node->getDownloadState()->pause(); emit dataChanged(index, index); } void ContentManagerModel::resumeDownload(QModelIndex index) { auto node = static_cast(index.internalPointer()); - auto id = node->getBookId(); - auto prevDownloadInfo = node->getDownloadInfo(); - prevDownloadInfo.paused = false; - node->setDownloadInfo(prevDownloadInfo); - timers[id]->start(1000); + node->getDownloadState()->resume(); emit dataChanged(index, index); } void ContentManagerModel::cancelDownload(QModelIndex index) { auto node = static_cast(index.internalPointer()); - auto id = node->getBookId(); - node->setIsDownloading(false); - node->setDownloadInfo({0, "", "", false}); - timers[id]->stop(); - timers[id]->deleteLater(); + node->setDownloadState(nullptr); + m_downloads.remove(node->getBookId()); emit dataChanged(index, index); } diff --git a/src/contentmanagermodel.h b/src/contentmanagermodel.h index 22de079..f5da2d1 100644 --- a/src/contentmanagermodel.h +++ b/src/contentmanagermodel.h @@ -6,6 +6,7 @@ #include #include #include "thumbnaildownloader.h" +#include "rownode.h" #include class RowNode; @@ -16,7 +17,11 @@ class ContentManagerModel : public QAbstractItemModel { Q_OBJECT -public: +public: // types + typedef QMap BookInfo; + typedef QList BookInfoList; + +public: // functions explicit ContentManagerModel(QObject *parent = nullptr); ~ContentManagerModel(); @@ -29,12 +34,14 @@ public: QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; - void setBooksData(const QList>& data); + void setBooksData(const BookInfoList& data); void setupNodes(); bool hasChildren(const QModelIndex &parent) const override; void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; void refreshIcons(); + std::shared_ptr createNode(BookInfo bookItem, QMap iconMap) const; + public slots: void updateImage(QModelIndex index, QString url, QByteArray imageData); void startDownload(QModelIndex index); @@ -42,17 +49,21 @@ public slots: void resumeDownload(QModelIndex index); void cancelDownload(QModelIndex index); -protected: +protected: // functions bool canFetchMore(const QModelIndex &parent) const override; void fetchMore(const QModelIndex &parent) override; -private: - QList> m_data; +private: // functions + void updateDownload(QString bookId); + +private: // data + BookInfoList m_data; std::shared_ptr rootNode; int zimCount = 0; ThumbnailDownloader td; + QMap bookIdToRowMap; QMap iconMap; - QMap timers; + QMap> m_downloads; }; #endif // CONTENTMANAGERMODEL_H diff --git a/src/opdsrequestmanager.cpp b/src/opdsrequestmanager.cpp index 8a24302..a3346fe 100644 --- a/src/opdsrequestmanager.cpp +++ b/src/opdsrequestmanager.cpp @@ -5,8 +5,22 @@ OpdsRequestManager::OpdsRequestManager() { } -#define CATALOG_HOST "library.kiwix.org" -#define CATALOG_PORT 443 +QString OpdsRequestManager::getCatalogHost() +{ + const char* const envVarVal = getenv("KIWIX_CATALOG_HOST"); + return envVarVal + ? envVarVal + : "library.kiwix.org"; +} + +int OpdsRequestManager::getCatalogPort() +{ + const char* const envVarVal = getenv("KIWIX_CATALOG_PORT"); + return envVarVal + ? atoi(envVarVal) + : 443; +} + void OpdsRequestManager::doUpdate(const QString& currentLanguage, const QString& categoryFilter) { QUrlQuery query; @@ -36,9 +50,10 @@ void OpdsRequestManager::doUpdate(const QString& currentLanguage, const QString& QNetworkReply* OpdsRequestManager::opdsResponseFromPath(const QString &path, const QUrlQuery &query) { QUrl url; - url.setScheme("https"); - url.setHost(CATALOG_HOST); - url.setPort(CATALOG_PORT); + const int port = getCatalogPort(); + url.setScheme(port == 443 ? "https" : "http"); + url.setHost(getCatalogHost()); + url.setPort(port); url.setPath(path); url.setQuery(query); qInfo() << "Downloading" << url.toString(QUrl::FullyEncoded); diff --git a/src/opdsrequestmanager.h b/src/opdsrequestmanager.h index 5a3d3c2..b14f9a2 100644 --- a/src/opdsrequestmanager.h +++ b/src/opdsrequestmanager.h @@ -32,6 +32,10 @@ public slots: void receiveContent(QNetworkReply*); void receiveLanguages(QNetworkReply*); void receiveCategories(QNetworkReply*); + +public: + static QString getCatalogHost(); + static int getCatalogPort(); }; #endif // OPDSREQUESTMANAGER_H diff --git a/src/rownode.cpp b/src/rownode.cpp index b7ba2d3..2559b99 100644 --- a/src/rownode.cpp +++ b/src/rownode.cpp @@ -2,12 +2,81 @@ #include #include "kiwixapp.h" #include "descriptionnode.h" -#include "kiwix/tools.h" + +//////////////////////////////////////////////////////////////////////////////// +// DowloadState +//////////////////////////////////////////////////////////////////////////////// + +DownloadState::DownloadState() + : m_downloadInfo({0, "", "", false}) +{ + m_downloadUpdateTimer.reset(new QTimer); + m_downloadUpdateTimer->start(1000); +} + +namespace +{ + +QString convertToUnits(QString size) +{ + QStringList units = {"bytes", "KB", "MB", "GB", "TB", "PB", "EB"}; + int unitIndex = 0; + auto bytes = size.toDouble(); + while (bytes >= 1024 && unitIndex < units.size()) { + bytes /= 1024; + unitIndex++; + } + + const auto preciseBytes = QString::number(bytes, 'g', 3); + return preciseBytes + " " + units[unitIndex]; +} + +} // unnamed namespace + +bool DownloadState::update(QString id) +{ + auto downloadInfos = KiwixApp::instance()->getContentManager()->updateDownloadInfos(id, {"status", "completedLength", "totalLength", "downloadSpeed"}); + if (!downloadInfos["status"].isValid()) { + m_downloadUpdateTimer->stop(); + + // Deleting the timer object immediately instead of via + // QObject::deleteLater() seems to be safe since it is not a recipient + // of any events that may be in the process of being delivered to it + // from another thread. + m_downloadUpdateTimer.reset(); + m_downloadInfo = {0, "", "", false}; + return false; + } + + double percent = downloadInfos["completedLength"].toDouble() / downloadInfos["totalLength"].toDouble(); + percent *= 100; + percent = QString::number(percent, 'g', 3).toDouble(); + auto completedLength = convertToUnits(downloadInfos["completedLength"].toString()); + auto downloadSpeed = convertToUnits(downloadInfos["downloadSpeed"].toString()) + "/s"; + m_downloadInfo = {percent, completedLength, downloadSpeed, false}; + return true; +} + +void DownloadState::pause() +{ + m_downloadInfo.paused = true; + m_downloadUpdateTimer->stop(); +} + +void DownloadState::resume() +{ + m_downloadInfo.paused = false; + m_downloadUpdateTimer->start(1000); +} + + +//////////////////////////////////////////////////////////////////////////////// +// RowNode +//////////////////////////////////////////////////////////////////////////////// RowNode::RowNode(QList itemData, QString bookId, std::weak_ptr parent) : m_itemData(itemData), m_parentItem(parent), m_bookId(bookId) { - m_downloadInfo = {0, "", "", false}; } RowNode::~RowNode() @@ -65,36 +134,6 @@ int RowNode::row() const return 0; } -std::shared_ptr RowNode::createNode(QMap bookItem, QMap iconMap, std::shared_ptr rootNode) -{ - auto faviconUrl = "https://" + bookItem["faviconUrl"].toString(); - QString id = bookItem["id"].toString(); - QByteArray bookIcon; - try { - auto book = KiwixApp::instance()->getLibrary()->getBookById(id); - std::string favicon; - auto item = book.getIllustration(48); - favicon = item->getData(); - bookIcon = QByteArray::fromRawData(reinterpret_cast(favicon.data()), favicon.size()); - bookIcon.detach(); // deep copy - } catch (...) { - if (iconMap.contains(faviconUrl)) { - bookIcon = iconMap[faviconUrl]; - } - } - std::weak_ptr weakRoot = rootNode; - auto rowNodePtr = std::shared_ptr(new - RowNode({bookIcon, bookItem["title"], - bookItem["date"], - QString::fromStdString(kiwix::beautifyFileSize(bookItem["size"].toULongLong())), - bookItem["tags"] - }, id, weakRoot)); - std::weak_ptr weakRowNodePtr = rowNodePtr; - const auto descNodePtr = std::make_shared(DescriptionNode(bookItem["description"].toString(), weakRowNodePtr)); - rowNodePtr->appendChild(descNodePtr); - return rowNodePtr; -} - bool RowNode::isChild(Node *candidate) { if (!candidate) @@ -105,3 +144,8 @@ bool RowNode::isChild(Node *candidate) } return false; } + +void RowNode::setDownloadState(std::shared_ptr ds) +{ + m_downloadState = ds; +} diff --git a/src/rownode.h b/src/rownode.h index d82db4f..85cbe61 100644 --- a/src/rownode.h +++ b/src/rownode.h @@ -3,7 +3,6 @@ #include "node.h" #include -#include "contentmanagermodel.h" #include #include "kiwix/book.h" @@ -15,6 +14,25 @@ struct DownloadInfo bool paused; }; +class DownloadState +{ +public: + DownloadState(); + + bool isDownloading() const { return m_downloadUpdateTimer.get() != nullptr; } + DownloadInfo getDownloadInfo() const { return m_downloadInfo; } + QTimer* getDownloadUpdateTimer() const { return m_downloadUpdateTimer.get(); } + void pause(); + void resume(); + bool update(QString id); + +protected: + // This is non-NULL only for a pending (even if paused) download + std::unique_ptr m_downloadUpdateTimer; + + DownloadInfo m_downloadInfo; +}; + class RowNode : public Node { public: @@ -29,20 +47,18 @@ public: int row() const override; QString getBookId() const override { return m_bookId; } void setIconData(QByteArray iconData) { m_itemData[0] = iconData; } - bool isDownloading() const { return m_isDownloading; } - void setDownloadInfo(DownloadInfo downloadInfo) { m_downloadInfo = downloadInfo; } - DownloadInfo getDownloadInfo() const { return m_downloadInfo; } - void setIsDownloading(bool val) { m_isDownloading = val; } - static std::shared_ptr createNode(QMap bookItem, QMap iconMap, std::shared_ptr rootNode); bool isChild(Node* candidate); + + void setDownloadState(std::shared_ptr ds); + std::shared_ptr getDownloadState() { return m_downloadState; } + private: QList m_itemData; QList> m_childItems; std::weak_ptr m_parentItem; QString m_bookId; - bool m_isDownloading = false; - DownloadInfo m_downloadInfo; + std::shared_ptr m_downloadState; };