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"