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;
};