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.
This commit is contained in:
Luis Ángel San Martín 2023-08-13 11:40:59 +02:00
parent 2e9ec030ad
commit 3a0e8db189
2 changed files with 263 additions and 92 deletions

View File

@ -10,24 +10,29 @@
#include "qnaturalsorting.h" #include "qnaturalsorting.h"
#include "comic_db.h" #include "comic_db.h"
#include "db_helper.h" #include "db_helper.h"
#include "query_parser.h"
#include "reading_list_model.h" #include "reading_list_model.h"
// ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read // ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read
#include "QsLog.h" #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) ComicModel::ComicModel(QObject *parent)
: QAbstractItemModel(parent), showRecent(false), recentDays(1) : QAbstractItemModel(parent), showRecent(false), recentDays(1)
{ {
} }
ComicModel::ComicModel(QSqlQuery &sqlquery, QObject *parent)
: QAbstractItemModel(parent), showRecent(false), recentDays(1)
{
setupModelData(sqlquery);
}
ComicModel::~ComicModel() ComicModel::~ComicModel()
{ {
qDeleteAll(_data); qDeleteAll(_data);
@ -456,7 +461,30 @@ QStringList ComicModel::getPaths(const QString &_source)
return paths; 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<ComicItem *> ComicModel::createFolderModelData(unsigned long long folderId, const QString &databasePath) const
{
QList<ComicItem *> 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) 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(); _data.clear();
_databasePath = databasePath; _databasePath = databasePath;
takeData(createFolderModelData(folderId, databasePath));
endResetModel();
}
QList<ComicItem *> ComicModel::createLabelModelData(unsigned long long parentLabel, const QString &databasePath) const
{
QList<ComicItem *> modelData;
QString connectionName = ""; QString connectionName = "";
{ {
QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath);
QSqlQuery selectQuery(db); QSqlQuery selectQuery(db);
selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " "
"FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) "
"WHERE c.parentId = :parentId"); "INNER JOIN comic_label cl ON (c.id == cl.comic_id) "
selectQuery.bindValue(":parentId", folderId); "WHERE cl.label_id = :parentLabelId "
"ORDER BY cl.ordering");
selectQuery.bindValue(":parentLabelId", parentLabel);
selectQuery.exec(); selectQuery.exec();
setupModelData(selectQuery); modelData = createModelDataForList(selectQuery);
connectionName = db.connectionName(); connectionName = db.connectionName();
} }
QSqlDatabase::removeDatabase(connectionName); QSqlDatabase::removeDatabase(connectionName);
endResetModel();
return modelData;
} }
void ComicModel::setupLabelModelData(unsigned long long parentLabel, const QString &databasePath) 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(); _data.clear();
_databasePath = databasePath; _databasePath = databasePath;
QString connectionName = "";
{ takeData(createLabelModelData(parentLabel, databasePath));
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);
endResetModel(); endResetModel();
} }
void ComicModel::setupReadingListModelData(unsigned long long parentReadingList, const QString &databasePath) QList<ComicItem *> ComicModel::createReadingListData(unsigned long long parentReadingList, const QString &databasePath, bool &enableResorting) const
{ {
mode = ReadingList; QList<ComicItem *> modelData;
sourceId = parentReadingList;
beginResetModel();
qDeleteAll(_data);
_data.clear();
_databasePath = databasePath;
QString connectionName = ""; QString connectionName = "";
{ {
QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath);
@ -552,20 +575,54 @@ void ComicModel::setupReadingListModelData(unsigned long long parentReadingList,
selectQuery.bindValue(":parentReadingList", id); selectQuery.bindValue(":parentReadingList", id);
selectQuery.exec(); selectQuery.exec();
// TODO, extra information is needed (resorting) modelData << createModelDataForList(selectQuery);
QList<ComicItem *> tempData = _data;
_data.clear();
setupModelDataForList(selectQuery);
_data = tempData << _data;
} }
connectionName = db.connectionName(); connectionName = db.connectionName();
} }
QSqlDatabase::removeDatabase(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(); endResetModel();
} }
QList<ComicItem *> ComicModel::createFavoritesModelData(const QString &databasePath) const
{
QList<ComicItem *> 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) void ComicModel::setupFavoritesModelData(const QString &databasePath)
{ {
enableResorting = true; enableResorting = true;
@ -577,22 +634,32 @@ void ComicModel::setupFavoritesModelData(const QString &databasePath)
_data.clear(); _data.clear();
_databasePath = databasePath; _databasePath = databasePath;
takeData(createFavoritesModelData(databasePath));
endResetModel();
}
QList<ComicItem *> ComicModel::createReadingModelData(const QString &databasePath) const
{
QList<ComicItem *> modelData;
QString connectionName = ""; QString connectionName = "";
{ {
QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath);
QSqlQuery selectQuery(db); QSqlQuery selectQuery(db);
selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " "
"FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " "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 ci.hasBeenOpened = 1 AND ci.read = 0 "
"WHERE cdrl.default_reading_list_id = :parentDefaultListId " "ORDER BY ci.lastTimeOpened DESC");
"ORDER BY cdrl.ordering");
selectQuery.bindValue(":parentDefaultListId", 1);
selectQuery.exec(); selectQuery.exec();
setupModelDataForList(selectQuery);
modelData = createModelDataForList(selectQuery);
connectionName = db.connectionName(); connectionName = db.connectionName();
} }
QSqlDatabase::removeDatabase(connectionName); QSqlDatabase::removeDatabase(connectionName);
endResetModel();
return modelData;
} }
void ComicModel::setupReadingModelData(const QString &databasePath) void ComicModel::setupReadingModelData(const QString &databasePath)
@ -606,21 +673,33 @@ void ComicModel::setupReadingModelData(const QString &databasePath)
_data.clear(); _data.clear();
_databasePath = databasePath; _databasePath = databasePath;
takeData(createReadingModelData(databasePath));
endResetModel();
}
QList<ComicItem *> ComicModel::createRecentModelData(const QString &databasePath) const
{
QList<ComicItem *> modelData;
QString connectionName = ""; QString connectionName = "";
{ {
QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath);
QSqlQuery selectQuery(db); QSqlQuery selectQuery(db);
selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " "
"FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) "
"WHERE ci.hasBeenOpened = 1 AND ci.read = 0 " "WHERE ci.added > :limit "
"ORDER BY ci.lastTimeOpened DESC"); "ORDER BY ci.added DESC");
selectQuery.bindValue(":limit", QDateTime::currentDateTime().addDays(-recentDays).toSecsSinceEpoch());
selectQuery.exec(); selectQuery.exec();
setupModelDataForList(selectQuery); modelData = createModelDataForList(selectQuery);
connectionName = db.connectionName(); connectionName = db.connectionName();
} }
QSqlDatabase::removeDatabase(connectionName); QSqlDatabase::removeDatabase(connectionName);
endResetModel();
return modelData;
} }
void ComicModel::setupRecentModelData(const QString &databasePath) void ComicModel::setupRecentModelData(const QString &databasePath)
@ -635,26 +714,17 @@ void ComicModel::setupRecentModelData(const QString &databasePath)
_databasePath = databasePath; _databasePath = databasePath;
QString connectionName = ""; takeData(createRecentModelData(databasePath));
{
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();
setupModelDataForList(selectQuery);
connectionName = db.connectionName();
}
QSqlDatabase::removeDatabase(connectionName);
endResetModel(); endResetModel();
} }
void ComicModel::setModelData(QList<ComicItem *> *data, const QString &databasePath) void ComicModel::setModelData(QList<ComicItem *> *data, const QString &databasePath)
{ {
enableResorting = false;
mode = SearchResult;
sourceId = -1;
_databasePath = databasePath; _databasePath = databasePath;
beginResetModel(); beginResetModel();
@ -679,8 +749,10 @@ QString ComicModel::getComicPath(QModelIndex mi)
return ""; return "";
} }
void ComicModel::setupModelData(QSqlQuery &sqlquery) QList<ComicItem *> ComicModel::createModelData(QSqlQuery &sqlquery) const
{ {
QList<ComicItem *> modelData;
int numColumns = sqlquery.record().count(); int numColumns = sqlquery.record().count();
while (sqlquery.next()) { while (sqlquery.next()) {
@ -689,25 +761,19 @@ void ComicModel::setupModelData(QSqlQuery &sqlquery)
for (int i = 0; i < numColumns; i++) for (int i = 0; i < numColumns; i++)
data << sqlquery.value(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) { std::sort(modelData.begin(), modelData.end(), defaultFolderContentSortFunction);
if (c1->data(ComicModel::Number).isNull() && c2->data(ComicModel::Number).isNull()) {
return naturalSortLessThanCI(c1->data(ComicModel::FileName).toString(), c2->data(ComicModel::FileName).toString()); return modelData;
} 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();
}
}
});
} }
// comics are sorted by "ordering", the sorting is done in the sql query // the sorting is done in the sql query
void ComicModel::setupModelDataForList(QSqlQuery &sqlquery) QList<ComicItem *> ComicModel::createModelDataForList(QSqlQuery &sqlquery) const
{ {
QList<ComicItem *> modelData;
int numColumns = sqlquery.record().count(); int numColumns = sqlquery.record().count();
while (sqlquery.next()) { while (sqlquery.next()) {
@ -715,7 +781,92 @@ void ComicModel::setupModelDataForList(QSqlQuery &sqlquery)
for (int i = 0; i < numColumns; i++) for (int i = 0; i < numColumns; i++)
data << sqlquery.value(i); data << sqlquery.value(i);
_data.append(new ComicItem(data)); modelData.append(new ComicItem(data));
}
return modelData;
}
void ComicModel::takeData(const QList<ComicItem *> &data)
{
qDeleteAll(_data);
_data = data;
}
void ComicModel::takeUpdatedData(const QList<ComicItem *> &updatedData, std::function<bool(ComicItem *, ComicItem *)> 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); DBHelper::removeFromDB(&c, dbTransaction);
beginRemoveRows(QModelIndex(), row, row); beginRemoveRows(QModelIndex(), row, row);
removeRow(row); removeRow(row);
delete _data.at(row); delete _data.at(row);
_data.removeAt(row); _data.removeAt(row);
@ -941,26 +1093,34 @@ void ComicModel::reloadContinueReading()
setupReadingModelData(_databasePath); setupReadingModelData(_databasePath);
} }
// The `comparator` passed to `takeUpdatedData` is used to determine if a row has been removed or added
void ComicModel::reload() void ComicModel::reload()
{ {
switch (mode) { switch (mode) {
case Folder: case Folder:
setupFolderModelData(sourceId, _databasePath); takeUpdatedData(createFolderModelData(sourceId, _databasePath), defaultFolderContentSortFunction);
break; break;
case Favorites: case Favorites:
setupFavoritesModelData(_databasePath); setupFavoritesModelData(_databasePath); // TODO we need a comparator
break; break;
case Reading: 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; break;
case Recent: 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; break;
case Label: case Label:
setupLabelModelData(sourceId, _databasePath); setupLabelModelData(sourceId, _databasePath); // TODO we need a comparator
break; break;
case ReadingList: 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; break;
} }
} }

View File

@ -38,6 +38,7 @@ public:
PublicationDate = 13, PublicationDate = 13,
Added = 14, Added = 14,
Type = 15, Type = 15,
LastTimeOpened = 16,
}; };
enum Roles { enum Roles {
@ -69,12 +70,12 @@ public:
Reading, Reading,
Recent, Recent,
Label, Label,
ReadingList ReadingList,
SearchResult
}; };
public: public:
explicit ComicModel(QObject *parent = nullptr); explicit ComicModel(QObject *parent = nullptr);
explicit ComicModel(QSqlQuery &sqlquery, QObject *parent = nullptr);
~ComicModel() override; ~ComicModel() override;
QVariant data(const QModelIndex &index, int role) const override; QVariant data(const QModelIndex &index, int role) const override;
@ -162,8 +163,18 @@ public slots:
protected: protected:
private: private:
void setupModelData(QSqlQuery &sqlquery); QList<ComicItem *> createModelData(QSqlQuery &sqlquery) const;
void setupModelDataForList(QSqlQuery &sqlquery); QList<ComicItem *> createModelDataForList(QSqlQuery &sqlquery) const;
QList<ComicItem *> createFolderModelData(unsigned long long parentLabel, const QString &databasePath) const;
QList<ComicItem *> createLabelModelData(unsigned long long parentLabel, const QString &databasePath) const;
QList<ComicItem *> createReadingListData(unsigned long long parentReadingList, const QString &databasePath, bool &enableResorting) const;
QList<ComicItem *> createFavoritesModelData(const QString &databasePath) const;
QList<ComicItem *> createReadingModelData(const QString &databasePath) const;
QList<ComicItem *> createRecentModelData(const QString &databasePath) const;
void takeData(const QList<ComicItem *> &data);
void takeUpdatedData(const QList<ComicItem *> &updatedData, std::function<bool(ComicItem *, ComicItem *)> comparator);
ComicDB _getComic(const QModelIndex &mi); ComicDB _getComic(const QModelIndex &mi);
QList<ComicItem *> _data; QList<ComicItem *> _data;