From 3a0e8db1894f3d3dc9edbc8a06777a359887ec37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20=C3=81ngel=20San=20Mart=C3=ADn?= Date: Sun, 13 Aug 2023 11:40:59 +0200 Subject: [PATCH] Implement support for updating ComicModel without resetting the whole model TODO: Favorites, Label, ReadingList still fallback to full reload because we need a way of comparing old vs new results. SearchResult does nothing because we don't have access to the search query. --- YACReaderLibrary/db/comic_model.cpp | 336 ++++++++++++++++++++-------- YACReaderLibrary/db/comic_model.h | 19 +- 2 files changed, 263 insertions(+), 92 deletions(-) diff --git a/YACReaderLibrary/db/comic_model.cpp b/YACReaderLibrary/db/comic_model.cpp index 84c48800..7dff6a6b 100644 --- a/YACReaderLibrary/db/comic_model.cpp +++ b/YACReaderLibrary/db/comic_model.cpp @@ -10,24 +10,29 @@ #include "qnaturalsorting.h" #include "comic_db.h" #include "db_helper.h" -#include "query_parser.h" #include "reading_list_model.h" // ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read #include "QsLog.h" +auto defaultFolderContentSortFunction = [](const ComicItem *c1, const ComicItem *c2) { + if (c1->data(ComicModel::Number).isNull() && c2->data(ComicModel::Number).isNull()) { + return naturalSortLessThanCI(c1->data(ComicModel::FileName).toString(), c2->data(ComicModel::FileName).toString()); + } else { + if (c1->data(ComicModel::Number).isNull() == false && c2->data(ComicModel::Number).isNull() == false) { + return naturalSortLessThanCI(c1->data(ComicModel::Number).toString(), c2->data(ComicModel::Number).toString()); + } else { + return c2->data(ComicModel::Number).isNull(); + } + } +}; + ComicModel::ComicModel(QObject *parent) : QAbstractItemModel(parent), showRecent(false), recentDays(1) { } -ComicModel::ComicModel(QSqlQuery &sqlquery, QObject *parent) - : QAbstractItemModel(parent), showRecent(false), recentDays(1) -{ - setupModelData(sqlquery); -} - ComicModel::~ComicModel() { qDeleteAll(_data); @@ -456,7 +461,30 @@ QStringList ComicModel::getPaths(const QString &_source) return paths; } -#define COMIC_MODEL_QUERY_FIELDS "ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened,ci.date,ci.added,ci.type" +#define COMIC_MODEL_QUERY_FIELDS "ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened,ci.date,ci.added,ci.type,ci.lastTimeOpened" + +QList ComicModel::createFolderModelData(unsigned long long folderId, const QString &databasePath) const +{ + QList modelData; + + QString connectionName = ""; + { + QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); + QSqlQuery selectQuery(db); + selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " + "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " + "WHERE c.parentId = :parentId"); + selectQuery.bindValue(":parentId", folderId); + selectQuery.exec(); + + modelData = createModelData(selectQuery); + + connectionName = db.connectionName(); + } + QSqlDatabase::removeDatabase(connectionName); + + return modelData; +} void ComicModel::setupFolderModelData(unsigned long long int folderId, const QString &databasePath) { @@ -469,20 +497,33 @@ void ComicModel::setupFolderModelData(unsigned long long int folderId, const QSt _data.clear(); _databasePath = databasePath; + + takeData(createFolderModelData(folderId, databasePath)); + + endResetModel(); +} + +QList ComicModel::createLabelModelData(unsigned long long parentLabel, const QString &databasePath) const +{ + QList modelData; + QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); QSqlQuery selectQuery(db); selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " - "WHERE c.parentId = :parentId"); - selectQuery.bindValue(":parentId", folderId); + "INNER JOIN comic_label cl ON (c.id == cl.comic_id) " + "WHERE cl.label_id = :parentLabelId " + "ORDER BY cl.ordering"); + selectQuery.bindValue(":parentLabelId", parentLabel); selectQuery.exec(); - setupModelData(selectQuery); + modelData = createModelDataForList(selectQuery); connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); - endResetModel(); + + return modelData; } void ComicModel::setupLabelModelData(unsigned long long parentLabel, const QString &databasePath) @@ -496,34 +537,16 @@ void ComicModel::setupLabelModelData(unsigned long long parentLabel, const QStri _data.clear(); _databasePath = databasePath; - QString connectionName = ""; - { - QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); - QSqlQuery selectQuery(db); - selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " - "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " - "INNER JOIN comic_label cl ON (c.id == cl.comic_id) " - "WHERE cl.label_id = :parentLabelId " - "ORDER BY cl.ordering"); - selectQuery.bindValue(":parentLabelId", parentLabel); - selectQuery.exec(); - setupModelDataForList(selectQuery); - connectionName = db.connectionName(); - } - QSqlDatabase::removeDatabase(connectionName); + + takeData(createLabelModelData(parentLabel, databasePath)); + endResetModel(); } -void ComicModel::setupReadingListModelData(unsigned long long parentReadingList, const QString &databasePath) +QList ComicModel::createReadingListData(unsigned long long parentReadingList, const QString &databasePath, bool &enableResorting) const { - mode = ReadingList; - sourceId = parentReadingList; + QList modelData; - beginResetModel(); - qDeleteAll(_data); - _data.clear(); - - _databasePath = databasePath; QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); @@ -552,20 +575,54 @@ void ComicModel::setupReadingListModelData(unsigned long long parentReadingList, selectQuery.bindValue(":parentReadingList", id); selectQuery.exec(); - // TODO, extra information is needed (resorting) - QList tempData = _data; - _data.clear(); - - setupModelDataForList(selectQuery); - - _data = tempData << _data; + modelData << createModelDataForList(selectQuery); } connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); + + return modelData; +} + +void ComicModel::setupReadingListModelData(unsigned long long parentReadingList, const QString &databasePath) +{ + mode = ReadingList; + sourceId = parentReadingList; + + beginResetModel(); + qDeleteAll(_data); + _data.clear(); + + _databasePath = databasePath; + + takeData(createReadingListData(parentReadingList, databasePath, enableResorting)); + endResetModel(); } +QList ComicModel::createFavoritesModelData(const QString &databasePath) const +{ + QList modelData; + + QString connectionName = ""; + { + QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); + QSqlQuery selectQuery(db); + selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " + "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " + "INNER JOIN comic_default_reading_list cdrl ON (c.id == cdrl.comic_id) " + "WHERE cdrl.default_reading_list_id = :parentDefaultListId " + "ORDER BY cdrl.ordering"); + selectQuery.bindValue(":parentDefaultListId", 1); + selectQuery.exec(); + modelData = createModelDataForList(selectQuery); + connectionName = db.connectionName(); + } + QSqlDatabase::removeDatabase(connectionName); + + return modelData; +} + void ComicModel::setupFavoritesModelData(const QString &databasePath) { enableResorting = true; @@ -577,22 +634,32 @@ void ComicModel::setupFavoritesModelData(const QString &databasePath) _data.clear(); _databasePath = databasePath; + + takeData(createFavoritesModelData(databasePath)); + + endResetModel(); +} + +QList ComicModel::createReadingModelData(const QString &databasePath) const +{ + QList modelData; + QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); QSqlQuery selectQuery(db); selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " - "INNER JOIN comic_default_reading_list cdrl ON (c.id == cdrl.comic_id) " - "WHERE cdrl.default_reading_list_id = :parentDefaultListId " - "ORDER BY cdrl.ordering"); - selectQuery.bindValue(":parentDefaultListId", 1); + "WHERE ci.hasBeenOpened = 1 AND ci.read = 0 " + "ORDER BY ci.lastTimeOpened DESC"); selectQuery.exec(); - setupModelDataForList(selectQuery); + + modelData = createModelDataForList(selectQuery); connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); - endResetModel(); + + return modelData; } void ComicModel::setupReadingModelData(const QString &databasePath) @@ -606,21 +673,33 @@ void ComicModel::setupReadingModelData(const QString &databasePath) _data.clear(); _databasePath = databasePath; + + takeData(createReadingModelData(databasePath)); + + endResetModel(); +} + +QList ComicModel::createRecentModelData(const QString &databasePath) const +{ + QList modelData; + QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); QSqlQuery selectQuery(db); selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " - "WHERE ci.hasBeenOpened = 1 AND ci.read = 0 " - "ORDER BY ci.lastTimeOpened DESC"); + "WHERE ci.added > :limit " + "ORDER BY ci.added DESC"); + selectQuery.bindValue(":limit", QDateTime::currentDateTime().addDays(-recentDays).toSecsSinceEpoch()); selectQuery.exec(); - setupModelDataForList(selectQuery); + modelData = createModelDataForList(selectQuery); connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); - endResetModel(); + + return modelData; } void ComicModel::setupRecentModelData(const QString &databasePath) @@ -635,26 +714,17 @@ void ComicModel::setupRecentModelData(const QString &databasePath) _databasePath = databasePath; - QString connectionName = ""; - { - QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); - QSqlQuery selectQuery(db); - selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " - "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " - "WHERE ci.added > :limit " - "ORDER BY ci.added DESC"); - selectQuery.bindValue(":limit", QDateTime::currentDateTime().addDays(-recentDays).toSecsSinceEpoch()); - selectQuery.exec(); + takeData(createRecentModelData(databasePath)); - setupModelDataForList(selectQuery); - connectionName = db.connectionName(); - } - QSqlDatabase::removeDatabase(connectionName); endResetModel(); } void ComicModel::setModelData(QList *data, const QString &databasePath) { + enableResorting = false; + mode = SearchResult; + sourceId = -1; + _databasePath = databasePath; beginResetModel(); @@ -679,8 +749,10 @@ QString ComicModel::getComicPath(QModelIndex mi) return ""; } -void ComicModel::setupModelData(QSqlQuery &sqlquery) +QList ComicModel::createModelData(QSqlQuery &sqlquery) const { + QList modelData; + int numColumns = sqlquery.record().count(); while (sqlquery.next()) { @@ -689,25 +761,19 @@ void ComicModel::setupModelData(QSqlQuery &sqlquery) for (int i = 0; i < numColumns; i++) data << sqlquery.value(i); - _data.append(new ComicItem(data)); + modelData.append(new ComicItem(data)); } - std::sort(_data.begin(), _data.end(), [](const ComicItem *c1, const ComicItem *c2) { - if (c1->data(ComicModel::Number).isNull() && c2->data(ComicModel::Number).isNull()) { - return naturalSortLessThanCI(c1->data(ComicModel::FileName).toString(), c2->data(ComicModel::FileName).toString()); - } else { - if (c1->data(ComicModel::Number).isNull() == false && c2->data(ComicModel::Number).isNull() == false) { - return naturalSortLessThanCI(c1->data(ComicModel::Number).toString(), c2->data(ComicModel::Number).toString()); - } else { - return c2->data(ComicModel::Number).isNull(); - } - } - }); + std::sort(modelData.begin(), modelData.end(), defaultFolderContentSortFunction); + + return modelData; } -// comics are sorted by "ordering", the sorting is done in the sql query -void ComicModel::setupModelDataForList(QSqlQuery &sqlquery) +// the sorting is done in the sql query +QList ComicModel::createModelDataForList(QSqlQuery &sqlquery) const { + QList modelData; + int numColumns = sqlquery.record().count(); while (sqlquery.next()) { @@ -715,7 +781,92 @@ void ComicModel::setupModelDataForList(QSqlQuery &sqlquery) for (int i = 0; i < numColumns; i++) data << sqlquery.value(i); - _data.append(new ComicItem(data)); + modelData.append(new ComicItem(data)); + } + + return modelData; +} + +void ComicModel::takeData(const QList &data) +{ + qDeleteAll(_data); + _data = data; +} + +void ComicModel::takeUpdatedData(const QList &updatedData, std::function comparator) +{ + int lenght = _data.size(); + int lenghtUpdated = updatedData.size(); + + int i; // index of the internal data + int j; // index of the updated children + for (i = 0, j = 0; i < lenght && j < lenghtUpdated;) { + auto comic = _data.at(i); + auto updatedComic = updatedData.at(j); + + auto sameComic = comic->data(ComicModel::Id) == updatedComic->data(ComicModel::Id); + if (sameComic) { + if (comic->getData() != updatedComic->getData()) { + auto modelIndexToUpdate = index(i, 0, QModelIndex()); + + comic->setData(updatedComic->getData()); + + emit dataChanged(modelIndexToUpdate, modelIndexToUpdate); + } + + i++; + j++; + continue; + } + + auto lessThan = comparator(comic, updatedComic); + + // comic added + if (!lessThan) { + beginInsertRows(QModelIndex(), i, i); + + _data.insert(i, updatedComic); + + endInsertRows(); + + i++; + j++; + lenght++; + continue; + } + + // comic removed + if (lessThan) { + beginRemoveRows(QModelIndex(), i, i); + + _data.removeAt(i); + + endRemoveRows(); + + lenght--; + continue; + } + } + + // add remaining comics + for (; j < lenghtUpdated; j++) { + beginInsertRows(QModelIndex(), i, i); + + _data.append(updatedData.at(j)); + + endInsertRows(); + + i++; + } + + // remove remaining comics { + for (; i < lenght; i++) { + beginRemoveRows(QModelIndex(), i, i); + + delete _data.at(i); + _data.removeAt(i); + + endRemoveRows(); } } @@ -929,6 +1080,7 @@ void ComicModel::removeInTransaction(int row) DBHelper::removeFromDB(&c, dbTransaction); beginRemoveRows(QModelIndex(), row, row); + removeRow(row); delete _data.at(row); _data.removeAt(row); @@ -941,26 +1093,34 @@ void ComicModel::reloadContinueReading() setupReadingModelData(_databasePath); } +// The `comparator` passed to `takeUpdatedData` is used to determine if a row has been removed or added void ComicModel::reload() { switch (mode) { case Folder: - setupFolderModelData(sourceId, _databasePath); + takeUpdatedData(createFolderModelData(sourceId, _databasePath), defaultFolderContentSortFunction); break; case Favorites: - setupFavoritesModelData(_databasePath); + setupFavoritesModelData(_databasePath); // TODO we need a comparator break; case Reading: - setupReadingModelData(_databasePath); + takeUpdatedData(createReadingModelData(_databasePath), [](const ComicItem *c1, const ComicItem *c2) { + return c1->data(ComicModel::LastTimeOpened).toDateTime() > c2->data(ComicModel::LastTimeOpened).toDateTime(); + }); break; case Recent: - setupRecentModelData(_databasePath); + takeUpdatedData(createRecentModelData(_databasePath), [](const ComicItem *c1, const ComicItem *c2) { + return c1->data(ComicModel::Added).toDateTime() > c2->data(ComicModel::Added).toDateTime(); + }); break; case Label: - setupLabelModelData(sourceId, _databasePath); + setupLabelModelData(sourceId, _databasePath); // TODO we need a comparator break; case ReadingList: - setupReadingListModelData(sourceId, _databasePath); + setupReadingListModelData(sourceId, _databasePath); // TODO we need a comparator + break; + case SearchResult: + // TODO: reload search results, we don't have a way to recreate the query in this class break; } } diff --git a/YACReaderLibrary/db/comic_model.h b/YACReaderLibrary/db/comic_model.h index 4b155e7e..f9b8b5ca 100644 --- a/YACReaderLibrary/db/comic_model.h +++ b/YACReaderLibrary/db/comic_model.h @@ -38,6 +38,7 @@ public: PublicationDate = 13, Added = 14, Type = 15, + LastTimeOpened = 16, }; enum Roles { @@ -69,12 +70,12 @@ public: Reading, Recent, Label, - ReadingList + ReadingList, + SearchResult }; public: explicit ComicModel(QObject *parent = nullptr); - explicit ComicModel(QSqlQuery &sqlquery, QObject *parent = nullptr); ~ComicModel() override; QVariant data(const QModelIndex &index, int role) const override; @@ -162,8 +163,18 @@ public slots: protected: private: - void setupModelData(QSqlQuery &sqlquery); - void setupModelDataForList(QSqlQuery &sqlquery); + QList createModelData(QSqlQuery &sqlquery) const; + QList createModelDataForList(QSqlQuery &sqlquery) const; + + QList createFolderModelData(unsigned long long parentLabel, const QString &databasePath) const; + QList createLabelModelData(unsigned long long parentLabel, const QString &databasePath) const; + QList createReadingListData(unsigned long long parentReadingList, const QString &databasePath, bool &enableResorting) const; + QList createFavoritesModelData(const QString &databasePath) const; + QList createReadingModelData(const QString &databasePath) const; + QList createRecentModelData(const QString &databasePath) const; + + void takeData(const QList &data); + void takeUpdatedData(const QList &updatedData, std::function comparator); ComicDB _getComic(const QModelIndex &mi); QList _data;