File indexing completed on 2024-04-28 15:35:51

0001 // SPDX-FileCopyrightText: 2015 Dan Leinir Turthra Jensen <admin@leinir.dk>
0002 // SPDX-License-Identifier: LGPL-2.1-only or LGPL-3.0-only or LicenseRef-KDE-Accepted-LGPL
0003 
0004 #include "categoryentriesmodel.h"
0005 #include "propertycontainer.h"
0006 #include <KFileMetaData/UserMetaData>
0007 #include <QDir>
0008 #include <QFileInfo>
0009 
0010 class CategoryEntriesModel::Private
0011 {
0012 public:
0013     Private(CategoryEntriesModel *qq)
0014         : q(qq){};
0015     ~Private() = default;
0016     CategoryEntriesModel *q;
0017     QString name;
0018     Roles role;
0019     QList<BookEntry> entries;
0020     QList<CategoryEntriesModel *> categoryModels;
0021 };
0022 
0023 CategoryEntriesModel::CategoryEntriesModel(QObject *parent)
0024     : QAbstractListModel(parent)
0025     , d(std::make_unique<Private>(this))
0026 {
0027     connect(this, &CategoryEntriesModel::entryDataUpdated, this, &CategoryEntriesModel::entryDataChanged);
0028     connect(this, &CategoryEntriesModel::entryRemoved, this, &CategoryEntriesModel::entryRemove);
0029 }
0030 
0031 CategoryEntriesModel::~CategoryEntriesModel() = default;
0032 
0033 QHash<int, QByteArray> CategoryEntriesModel::roleNames() const
0034 {
0035     return {
0036         {FilenameRole, "filename"},
0037         {FiletitleRole, "filetitle"},
0038         {TitleRole, "title"},
0039         {GenreRole, "genres"},
0040         {KeywordRole, "keywords"},
0041         {SeriesRole, "series"},
0042         {SeriesNumbersRole, "seriesNumber"},
0043         {SeriesVolumesRole, "seriesVolume"},
0044         {AuthorRole, "author"},
0045         {PublisherRole, "publisher"},
0046         {CreatedRole, "created"},
0047         {LastOpenedTimeRole, "lastOpenedTime"},
0048         {CurrentProgressRole, "currentProgress"},
0049         {CurrentLocationRole, "currentLocation"},
0050         {CategoryEntriesModelRole, "categoryEntriesModel"},
0051         {CategoryEntryCountRole, "categoryEntriesCount"},
0052         {ThumbnailRole, "thumbnail"},
0053         {DescriptionRole, "description"},
0054         {CommentRole, "comment"},
0055         {TagsRole, "tags"},
0056         {RatingRole, "rating"},
0057         {LocationsRole, "locations"},
0058     };
0059 }
0060 
0061 QVariant CategoryEntriesModel::data(const QModelIndex &index, int role) const
0062 {
0063     if (!index.isValid() || index.row() <= -1) {
0064         return {};
0065     }
0066 
0067     if (index.row() < d->categoryModels.count()) {
0068         CategoryEntriesModel *model = d->categoryModels[index.row()];
0069         switch (role) {
0070         case Qt::DisplayRole:
0071         case TitleRole:
0072             return model->name();
0073         case CategoryEntryCountRole:
0074             return model->bookCount();
0075         case CategoryEntriesModelRole:
0076             return QVariant::fromValue<CategoryEntriesModel *>(model);
0077         case ThumbnailRole:
0078             switch (model->role()) {
0079             case SeriesRole:
0080                 return QStringLiteral("edit-group");
0081             case PublisherRole:
0082                 return QStringLiteral("view-media-publisher");
0083             case GenreRole:
0084                 return name() == QStringLiteral("Characters") ? QStringLiteral("actor") : QStringLiteral("tag-symbolic");
0085             default:
0086                 return QStringLiteral("actor");
0087             }
0088         default:
0089             return QString();
0090         }
0091     } else {
0092         const BookEntry &entry = d->entries[index.row() - d->categoryModels.count()];
0093         switch (role) {
0094         case Qt::DisplayRole:
0095         case FilenameRole:
0096             return entry.filename;
0097         case FiletitleRole:
0098             return entry.filetitle;
0099         case TitleRole:
0100             return entry.title;
0101         case GenreRole:
0102             return entry.genres;
0103         case KeywordRole:
0104             return entry.keywords;
0105         case CharacterRole:
0106             return entry.characters;
0107         case SeriesRole:
0108             return entry.series;
0109         case SeriesNumbersRole:
0110             return entry.seriesNumbers;
0111         case SeriesVolumesRole:
0112             return entry.seriesVolumes;
0113         case AuthorRole:
0114             return entry.author;
0115         case PublisherRole:
0116             return entry.publisher;
0117         case CreatedRole:
0118             return entry.created;
0119         case LastOpenedTimeRole:
0120             return entry.lastOpenedTime;
0121         case CurrentProgressRole:
0122             return entry.currentProgress;
0123         case CurrentLocationRole:
0124             return entry.currentLocation;
0125         case CategoryEntriesModelRole:
0126             // Nothing, if we're not equipped with one such...
0127             return QString{};
0128         case CategoryEntryCountRole:
0129             return QVariant::fromValue<int>(0);
0130         case ThumbnailRole:
0131             return entry.thumbnail;
0132         case DescriptionRole:
0133             return entry.description;
0134         case CommentRole:
0135             return entry.comment;
0136         case TagsRole:
0137             return entry.tags;
0138         case RatingRole:
0139             return entry.rating;
0140         case LocationsRole:
0141             return entry.locations;
0142         default:
0143             return QString();
0144         }
0145     }
0146 }
0147 
0148 int CategoryEntriesModel::rowCount(const QModelIndex &parent) const
0149 {
0150     if (parent.isValid()) {
0151         return 0;
0152     }
0153     return d->categoryModels.count() + d->entries.count();
0154 }
0155 
0156 int CategoryEntriesModel::count() const
0157 {
0158     return rowCount();
0159 }
0160 
0161 void CategoryEntriesModel::append(const BookEntry &entry, Roles compareRole)
0162 {
0163     int insertionIndex = 0;
0164     if (compareRole == UnknownRole) {
0165         // If we don't know what order to sort by, literally just append the entry
0166         insertionIndex = d->entries.count();
0167     } else {
0168         int seriesOne = -1;
0169         int seriesTwo = -1;
0170         if (compareRole == SeriesRole) {
0171             seriesOne = entry.series.indexOf(name());
0172             if (entry.series.contains(name(), Qt::CaseInsensitive) && seriesOne == -1) {
0173                 for (int s = 0; s < entry.series.size(); s++) {
0174                     if (QString::compare(name(), entry.series.at(s), Qt::CaseInsensitive)) {
0175                         seriesOne = s;
0176                     }
0177                 }
0178             }
0179         }
0180         for (; insertionIndex < d->entries.count(); ++insertionIndex) {
0181             if (compareRole == SeriesRole) {
0182                 seriesTwo = d->entries.at(insertionIndex).series.indexOf(name());
0183                 if (d->entries.at(insertionIndex).series.contains(name(), Qt::CaseInsensitive) && seriesTwo == -1) {
0184                     for (int s = 0; s < d->entries.at(insertionIndex).series.size(); s++) {
0185                         if (QString::compare(name(), d->entries.at(insertionIndex).series.at(s), Qt::CaseInsensitive)) {
0186                             seriesTwo = s;
0187                         }
0188                     }
0189                 }
0190             }
0191             if (compareRole == CreatedRole) {
0192                 if (entry.created <= d->entries.at(insertionIndex).created) {
0193                     continue;
0194                 }
0195                 break;
0196             } else if ((seriesOne > -1 && seriesTwo > -1) && entry.seriesNumbers.count() > -1 && entry.seriesNumbers.count() > seriesOne
0197                        && d->entries.at(insertionIndex).seriesNumbers.count() > -1 && d->entries.at(insertionIndex).seriesNumbers.count() > seriesTwo
0198                        && entry.seriesNumbers.at(seriesOne).toInt() > 0 && d->entries.at(insertionIndex).seriesNumbers.at(seriesTwo).toInt() > 0) {
0199                 if (entry.seriesVolumes.count() > -1 && entry.seriesVolumes.count() > seriesOne && d->entries.at(insertionIndex).seriesVolumes.count() > -1
0200                     && d->entries.at(insertionIndex).seriesVolumes.count() > seriesTwo
0201                     && entry.seriesVolumes.at(seriesOne).toInt() >= d->entries.at(insertionIndex).seriesVolumes.at(seriesTwo).toInt()
0202                     && entry.seriesNumbers.at(seriesOne).toInt() > d->entries.at(insertionIndex).seriesNumbers.at(seriesTwo).toInt()) {
0203                     continue;
0204                 }
0205                 break;
0206             } else {
0207                 if (QString::localeAwareCompare(d->entries.at(insertionIndex).title, entry.title) > 0) {
0208                     break;
0209                 }
0210             }
0211         }
0212     }
0213     beginInsertRows({}, insertionIndex, insertionIndex);
0214     d->entries.insert(insertionIndex, entry);
0215     Q_EMIT countChanged();
0216     endInsertRows();
0217 }
0218 
0219 void CategoryEntriesModel::clear()
0220 {
0221     beginResetModel();
0222     d->entries.clear();
0223     endResetModel();
0224 }
0225 
0226 const QString &CategoryEntriesModel::name() const
0227 {
0228     return d->name;
0229 }
0230 
0231 void CategoryEntriesModel::setName(const QString &newName)
0232 {
0233     d->name = newName;
0234 }
0235 
0236 CategoryEntriesModel *CategoryEntriesModel::leafModelForEntry(const BookEntry &entry)
0237 {
0238     CategoryEntriesModel *model = nullptr;
0239     if (d->categoryModels.count() == 0) {
0240         if (d->entries.contains(entry)) {
0241             model = this;
0242         }
0243     } else {
0244         for (CategoryEntriesModel *testModel : std::as_const(d->categoryModels)) {
0245             model = testModel->leafModelForEntry(entry);
0246             if (model) {
0247                 break;
0248             }
0249         }
0250     }
0251     return model;
0252 }
0253 
0254 void CategoryEntriesModel::addCategoryEntry(const QString &categoryName, const BookEntry &entry, Roles compareRole)
0255 {
0256     if (categoryName.length() > 0) {
0257         static const QString splitString = QStringLiteral("/");
0258         int splitPos = categoryName.indexOf(splitString);
0259         QString desiredCategory{categoryName};
0260         if (splitPos > -1) {
0261             desiredCategory = categoryName.left(splitPos);
0262         }
0263         CategoryEntriesModel *categoryModel = nullptr;
0264         for (CategoryEntriesModel *existingModel : qAsConst(d->categoryModels)) {
0265             if (QString::compare(existingModel->name(), desiredCategory, Qt::CaseInsensitive) == 0) {
0266                 categoryModel = existingModel;
0267                 break;
0268             }
0269         }
0270         if (!categoryModel) {
0271             categoryModel = new CategoryEntriesModel(this);
0272             categoryModel->setRole(compareRole);
0273             connect(this, &CategoryEntriesModel::entryDataUpdated, categoryModel, &CategoryEntriesModel::entryDataUpdated);
0274             connect(this, &CategoryEntriesModel::entryRemoved, categoryModel, &CategoryEntriesModel::entryRemoved);
0275             categoryModel->setName(desiredCategory);
0276 
0277             int insertionIndex = 0;
0278             for (; insertionIndex < d->categoryModels.count(); ++insertionIndex) {
0279                 if (QString::localeAwareCompare(d->categoryModels.at(insertionIndex)->name(), categoryModel->name()) > 0) {
0280                     break;
0281                 }
0282             }
0283             beginInsertRows(QModelIndex(), insertionIndex, insertionIndex);
0284             d->categoryModels.insert(insertionIndex, categoryModel);
0285             endInsertRows();
0286         }
0287         if (splitPos > -1) {
0288             categoryModel->addCategoryEntry(categoryName.mid(splitPos + 1), entry, compareRole);
0289         } else if (categoryModel->indexOfFile(entry.filename) == -1) {
0290             categoryModel->append(entry, compareRole);
0291         }
0292     }
0293 }
0294 
0295 std::optional<BookEntry> CategoryEntriesModel::getBookEntry(int index)
0296 {
0297     if (index > -1 && index < d->entries.count()) {
0298         return d->entries.at(index);
0299     }
0300     return std::nullopt;
0301 }
0302 
0303 int CategoryEntriesModel::indexOfFile(const QString &filename)
0304 {
0305     int index = -1, i = 0;
0306     if (QFile::exists(filename)) {
0307         for (const BookEntry &entry : std::as_const(d->entries)) {
0308             if (entry.filename == filename) {
0309                 index = i;
0310                 break;
0311             }
0312             ++i;
0313         }
0314     }
0315     return index;
0316 }
0317 
0318 bool CategoryEntriesModel::indexIsBook(int index)
0319 {
0320     if (index < d->categoryModels.count() || index >= rowCount()) {
0321         return false;
0322     }
0323     return true;
0324 }
0325 
0326 int CategoryEntriesModel::bookCount() const
0327 {
0328     return d->entries.count();
0329 }
0330 
0331 std::optional<BookEntry> CategoryEntriesModel::bookFromFile(const QString &filename)
0332 {
0333     const auto entry = getBookEntry(indexOfFile(filename));
0334     if (!entry) {
0335         return std::nullopt;
0336     }
0337     auto book = entry.value();
0338     if (book.filename.isEmpty()) {
0339         if (QFileInfo::exists(filename)) {
0340             QFileInfo info(filename);
0341             book.title = info.completeBaseName();
0342             book.created = info.birthTime();
0343 
0344             KFileMetaData::UserMetaData data(filename);
0345             if (data.hasAttribute(QStringLiteral("arianna.currentLocation"))) {
0346                 book.currentLocation = data.attribute(QStringLiteral("arianna.currentLocation"));
0347             }
0348             book.rating = data.rating();
0349             if (!data.tags().isEmpty()) {
0350                 book.tags = data.tags();
0351             }
0352             if (!data.userComment().isEmpty()) {
0353                 book.comment = data.userComment();
0354             }
0355             book.filename = filename;
0356         }
0357     }
0358     return book;
0359 }
0360 
0361 void CategoryEntriesModel::entryDataChanged(const BookEntry &entry)
0362 {
0363     int entryIndex = d->entries.indexOf(entry) + d->categoryModels.count();
0364     QModelIndex changed = index(entryIndex);
0365     Q_EMIT dataChanged(changed, changed);
0366 }
0367 
0368 void CategoryEntriesModel::entryRemove(const BookEntry &entry)
0369 {
0370     int listIndex = d->entries.indexOf(entry);
0371     if (listIndex > -1) {
0372         int entryIndex = listIndex + d->categoryModels.count();
0373         beginRemoveRows(QModelIndex(), entryIndex, entryIndex);
0374         d->entries.removeAll(entry);
0375         endRemoveRows();
0376     }
0377 }
0378 
0379 CategoryEntriesModel::Roles CategoryEntriesModel::role() const
0380 {
0381     return d->role;
0382 }
0383 
0384 void CategoryEntriesModel::setRole(Roles role)
0385 {
0386     d->role = role;
0387 }
0388 
0389 bool operator==(const BookEntry &b1, const BookEntry &b2) noexcept
0390 {
0391     return b1.filename == b2.filename;
0392 }
0393 
0394 #include "moc_categoryentriesmodel.cpp"