diff --git a/src/qtquick/BookDatabase.cpp b/src/qtquick/BookDatabase.cpp index 5778f59..24d42b6 100644 --- a/src/qtquick/BookDatabase.cpp +++ b/src/qtquick/BookDatabase.cpp @@ -1,147 +1,151 @@ /* * Copyright (C) 2017 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #include "BookDatabase.h" #include "CategoryEntriesModel.h" #include #include #include #include #include #include class BookDatabase::Private { public: Private() { db = QSqlDatabase::addDatabase("QSQLITE"); QDir location{QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)}; if(!location.exists()) location.mkpath("."); dbfile = location.absoluteFilePath("library.sqlite"); db.setDatabaseName(dbfile); } QSqlDatabase db; QString dbfile; bool prepareDb() { if (!db.open()) { qDebug() << "Failed to open the book database file" << dbfile << db.lastError(); return false; } QStringList tables = db.tables(); if (tables.contains("books", Qt::CaseInsensitive)) return true; QSqlQuery q; - if (!q.exec(QLatin1String("create table books(filename varchar primary key, filetitle varchar, title varchar, series varchar, author varchar, publisher varchar, created datetime, lastOpenedTime datetime, totalPages integer, currentPage integer, thumbnail varchar, description varchar, comment varchar, tags varchar, rating integer)"))) { + if (!q.exec(QLatin1String("create table books(filename varchar primary key, filetitle varchar, title varchar, series varchar, author varchar, publisher varchar, created datetime, lastOpenedTime datetime, totalPages integer, currentPage integer, thumbnail varchar, description varchar, comment varchar, tags varchar, rating integer, seriesVolumes varchar, seriesNumbers varchar)"))) { qDebug() << "Database could not create the table books"; return false; } return true; } void closeDb() { db.close(); } }; BookDatabase::BookDatabase(QObject* parent) : QObject(parent) , d(new Private) { } BookDatabase::~BookDatabase() { delete d; } QList BookDatabase::loadEntries() { if(!d->prepareDb()) { return QList(); } QList entries; - QSqlQuery allEntries("SELECT filename, filetitle, title, series, author, publisher, created, lastOpenedTime, totalPages, currentPage, thumbnail, description, comment, tags, rating FROM books"); + QSqlQuery allEntries("SELECT filename, filetitle, title, series, author, publisher, created, lastOpenedTime, totalPages, currentPage, thumbnail, description, comment, tags, rating, seriesNumbers, seriesVolumes FROM books"); while(allEntries.next()) { BookEntry* entry = new BookEntry(); entry->filename = allEntries.value(0).toString(); entry->filetitle = allEntries.value(1).toString(); entry->title = allEntries.value(2).toString(); entry->series = allEntries.value(3).toString().split(","); entry->author = allEntries.value(4).toString().split(","); entry->publisher = allEntries.value(5).toString(); entry->created = allEntries.value(6).toDateTime(); entry->lastOpenedTime = allEntries.value(7).toDateTime(); entry->totalPages = allEntries.value(8).toInt(); entry->currentPage = allEntries.value(9).toInt(); entry->thumbnail = allEntries.value(10).toString(); entry->description = allEntries.value(11).toString().split(","); entry->comment = allEntries.value(12).toString(); entry->tags = allEntries.value(13).toString().split(","); entry->rating = allEntries.value(14).toInt(); + entry->seriesNumbers = allEntries.value(15).toString().split(","); + entry->seriesVolumes = allEntries.value(16).toString().split(","); entries.append(entry); } d->closeDb(); return entries; } void BookDatabase::addEntry(BookEntry* entry) { if(!d->prepareDb()) { return; } qDebug() << "Adding newly discovered book to the database" << entry->filename; QSqlQuery newEntry; - newEntry.prepare("INSERT INTO books (filename, filetitle, title, series, author, publisher, created, lastOpenedTime, totalPages, currentPage, thumbnail, description, comment, tags, rating) " - "VALUES (:filename, :filetitle, :title, :series, :author, :publisher, :created, :lastOpenedTime, :totalPages, :currentPage, :thumbnail, :description, :comment, :tags, :rating)"); + newEntry.prepare("INSERT INTO books (filename, filetitle, title, series, author, publisher, created, lastOpenedTime, totalPages, currentPage, thumbnail, description, comment, tags, rating, seriesNumbers, seriesVolumes) " + "VALUES (:filename, :filetitle, :title, :series, :author, :publisher, :created, :lastOpenedTime, :totalPages, :currentPage, :thumbnail, :description, :comment, :tags, :rating, :seriesNumbers, :seriesVolumes)"); newEntry.bindValue(":filename", entry->filename); newEntry.bindValue(":filetitle", entry->filetitle); newEntry.bindValue(":title", entry->title); newEntry.bindValue(":series", entry->series.join(",")); newEntry.bindValue(":author", entry->author.join(",")); newEntry.bindValue(":publisher", entry->publisher); newEntry.bindValue(":publisher", entry->publisher); newEntry.bindValue(":created", entry->created); newEntry.bindValue(":lastOpenedTime", entry->lastOpenedTime); newEntry.bindValue(":totalPages", entry->totalPages); newEntry.bindValue(":currentPage", entry->currentPage); newEntry.bindValue(":thumbnail", entry->thumbnail); newEntry.bindValue(":description", entry->description.join(",")); newEntry.bindValue(":comment", entry->comment); newEntry.bindValue(":tags", entry->tags.join(",")); newEntry.bindValue(":rating", entry->rating); + newEntry.bindValue(":seriesNumbers", entry->seriesNumbers.join(",")); + newEntry.bindValue(":seriesVolumes", entry->seriesVolumes.join(",")); newEntry.exec(); d->closeDb(); } diff --git a/src/qtquick/BookListModel.cpp b/src/qtquick/BookListModel.cpp index 1412e28..9fa5a09 100644 --- a/src/qtquick/BookListModel.cpp +++ b/src/qtquick/BookListModel.cpp @@ -1,398 +1,410 @@ /* * Copyright (C) 2015 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #include "BookListModel.h" #include "BookDatabase.h" #include "CategoryEntriesModel.h" #include "ArchiveBookModel.h" #include "AcbfAuthor.h" #include "AcbfSequence.h" #include "AcbfBookinfo.h" #include #include #include #include #include #include #include #include class BookListModel::Private { public: Private() : contentModel(nullptr) , titleCategoryModel(nullptr) , newlyAddedCategoryModel(nullptr) , authorCategoryModel(nullptr) , seriesCategoryModel(nullptr) , publisherCategoryModel(nullptr) , folderCategoryModel(nullptr) , cacheLoaded(false) { db = new BookDatabase(); }; ~Private() { qDeleteAll(entries); db->deleteLater(); } QList entries; QAbstractListModel* contentModel; CategoryEntriesModel* titleCategoryModel; CategoryEntriesModel* newlyAddedCategoryModel; CategoryEntriesModel* authorCategoryModel; CategoryEntriesModel* seriesCategoryModel; CategoryEntriesModel* publisherCategoryModel; CategoryEntriesModel* folderCategoryModel; BookDatabase* db; bool cacheLoaded; void initializeSubModels(BookListModel* q) { if(!titleCategoryModel) { titleCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), titleCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), titleCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->titleCategoryModelChanged(); } if(!newlyAddedCategoryModel) { newlyAddedCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), newlyAddedCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), newlyAddedCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->newlyAddedCategoryModelChanged(); } if(!authorCategoryModel) { authorCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), authorCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), authorCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->authorCategoryModelChanged(); } if(!seriesCategoryModel) { seriesCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), seriesCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), seriesCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->seriesCategoryModelChanged(); } if(!publisherCategoryModel) { publisherCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), publisherCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), publisherCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->publisherCategoryModelChanged(); } if(!folderCategoryModel) { folderCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), folderCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), folderCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->folderCategoryModel(); } } void addEntry(BookListModel* q, BookEntry* entry) { entries.append(entry); q->append(entry); titleCategoryModel->addCategoryEntry(entry->title.left(1).toUpper(), entry); for (int i=0; iauthor.size(); i++) { authorCategoryModel->addCategoryEntry(entry->author.at(i), entry); } for (int i=0; iseries.size(); i++) { - seriesCategoryModel->addCategoryEntry(entry->series.at(i), entry); + seriesCategoryModel->addCategoryEntry(entry->series.at(i), entry, SeriesRole); } if (newlyAddedCategoryModel->indexOfFile(entry->filename) == -1) { newlyAddedCategoryModel->append(entry, CreatedRole); } publisherCategoryModel->addCategoryEntry(entry->publisher, entry); QUrl url(entry->filename.left(entry->filename.lastIndexOf("/"))); folderCategoryModel->addCategoryEntry(url.path().mid(1), entry); if (folderCategoryModel->indexOfFile(entry->filename) == -1) { folderCategoryModel->append(entry); } } void loadCache(BookListModel* q) { QList entries = db->loadEntries(); if(entries.count() > 0) { initializeSubModels(q); } int i = 0; foreach(BookEntry* entry, entries) { addEntry(q, entry); if(++i % 100 == 0) { emit q->countChanged(); qApp->processEvents(); } } cacheLoaded = true; emit q->cacheLoadedChanged(); } }; BookListModel::BookListModel(QObject* parent) : CategoryEntriesModel(parent) , d(new Private) { } BookListModel::~BookListModel() { delete d; } void BookListModel::componentComplete() { QTimer::singleShot(0, this, [this](){ d->loadCache(this); }); } bool BookListModel::cacheLoaded() const { return d->cacheLoaded; } void BookListModel::setContentModel(QObject* newModel) { if(d->contentModel) { d->contentModel->disconnect(this); } d->contentModel = qobject_cast(newModel); if(d->contentModel) { connect(d->contentModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(contentModelItemsInserted(QModelIndex,int, int))); } emit contentModelChanged(); } QObject * BookListModel::contentModel() const { return d->contentModel; } void BookListModel::contentModelItemsInserted(QModelIndex index, int first, int last) { d->initializeSubModels(this); int newRow = d->entries.count(); beginInsertRows(QModelIndex(), newRow, newRow + (last - first)); int role = d->contentModel->roleNames().key("filePath"); for(int i = first; i < last + 1; ++i) { QVariant filePath = d->contentModel->data(d->contentModel->index(first, 0, index), role); BookEntry* entry = new BookEntry(); entry->filename = filePath.toUrl().toLocalFile(); QStringList splitName = entry->filename.split("/"); if (!splitName.isEmpty()) entry->filetitle = splitName.takeLast(); - if(!splitName.isEmpty()) + if(!splitName.isEmpty()) { entry->series = QStringList(splitName.takeLast()); // hahahaheuristics (dumb assumptions about filesystems, go!) + entry->seriesNumbers = QStringList("0"); + entry->seriesVolumes = QStringList("0"); + } // just in case we end up without a title... using complete basename here, // as we would rather have "book one. part two" and the odd "book one - part two.tar" QFileInfo fileinfo(entry->filename); entry->title = fileinfo.completeBaseName(); if(entry->filename.toLower().endsWith("cbr")) { entry->thumbnail = QString("image://comiccover/").append(entry->filename); } #ifdef USE_PERUSE_PDFTHUMBNAILER else if(entry->filename.toLower().endsWith("pdf")) { entry->thumbnail = QString("image://pdfcover/").append(entry->filename); } #endif else { entry->thumbnail = QString("image://preview/").append(entry->filename); } KFileMetaData::UserMetaData data(entry->filename); entry->rating = data.rating(); entry->comment = data.userComment(); entry->tags = data.tags(); QVariantHash metadata = d->contentModel->data(d->contentModel->index(first, 0, index), Qt::UserRole + 2).toHash(); QVariantHash::const_iterator it = metadata.constBegin(); for (; it != metadata.constEnd(); it++) { if(it.key() == QLatin1String("author")) { entry->author = it.value().toStringList(); } else if(it.key() == QLatin1String("title")) { entry->title = it.value().toString().trimmed(); } else if(it.key() == QLatin1String("publisher")) { entry->publisher = it.value().toString().trimmed(); } else if(it.key() == QLatin1String("created")) { entry->created = it.value().toDateTime(); } else if(it.key() == QLatin1String("currentPage")) { entry->currentPage = it.value().toInt(); } else if(it.key() == QLatin1String("totalPages")) { entry->totalPages = it.value().toInt(); } else if(it.key() == QLatin1String("comments")) { entry->comment = it.value().toString();} else if(it.key() == QLatin1Literal("tags")) { entry->tags = it.value().toStringList();} else if(it.key() == QLatin1String("rating")) { entry->rating = it.value().toInt();} } // ACBF information is always preferred for CBRs, so let's just use that if it's there QMimeDatabase db; QString mimetype = db.mimeTypeForFile(entry->filename).name(); if(mimetype == "application/x-cbz" || mimetype == "application/x-cbr" || mimetype == "application/vnd.comicbook+zip" || mimetype == "application/vnd.comicbook+rar") { ArchiveBookModel* bookModel = new ArchiveBookModel(this); bookModel->setFilename(entry->filename); AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(bookModel->acbfData()); if(acbfDocument) { for(AdvancedComicBookFormat::Sequence* sequence : acbfDocument->metaData()->bookInfo()->sequence()) { - entry->series.append(sequence->title()); + if (!entry->series.contains(sequence->title())) { + entry->series.append(sequence->title()); + entry->seriesNumbers.append(QString::number(sequence->number())); + entry->seriesVolumes.append(QString::number(sequence->volume())); + } else { + int series = entry->series.indexOf(sequence->title()); + entry->seriesNumbers.replace(series, QString::number(sequence->number())); + entry->seriesVolumes.replace(series, QString::number(sequence->volume())); + } + } for(AdvancedComicBookFormat::Author* author : acbfDocument->metaData()->bookInfo()->author()) { entry->author.append(author->displayName()); } entry->description = acbfDocument->metaData()->bookInfo()->annotation(""); } if (entry->author.isEmpty()) { entry->author.append(bookModel->author()); } entry->title = bookModel->title(); entry->publisher = bookModel->publisher(); entry->totalPages = bookModel->pageCount(); bookModel->deleteLater(); } d->addEntry(this, entry); d->db->addEntry(entry); } endInsertRows(); emit countChanged(); qApp->processEvents(); } QObject * BookListModel::titleCategoryModel() const { return d->titleCategoryModel; } QObject * BookListModel::newlyAddedCategoryModel() const { return d->newlyAddedCategoryModel; } QObject * BookListModel::authorCategoryModel() const { return d->authorCategoryModel; } QObject * BookListModel::seriesCategoryModel() const { return d->seriesCategoryModel; } QObject * BookListModel::seriesModelForEntry(QString fileName) { Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == fileName) { return d->seriesCategoryModel->leafModelForEntry(entry); } } return nullptr; } QObject *BookListModel::publisherCategoryModel() const { return d->publisherCategoryModel; } QObject * BookListModel::folderCategoryModel() const { return d->folderCategoryModel; } int BookListModel::count() const { return d->entries.count(); } void BookListModel::setBookData(QString fileName, QString property, QString value) { Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == fileName) { if(property == "totalPages") { entry->totalPages = value.toInt(); } else if(property == "currentPage") { entry->currentPage = value.toInt(); } else if(property == "rating") { entry->rating = value.toInt(); } else if(property == "tags") { entry->tags = value.split(","); } else if(property == "comment") { entry->comment = value; } emit entryDataUpdated(entry); break; } } } void BookListModel::removeBook(QString fileName, bool deleteFile) { if(deleteFile) { KIO::DeleteJob* job = KIO::del(QUrl::fromLocalFile(fileName), KIO::HideProgressInfo); job->start(); } Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == fileName) { emit entryRemoved(entry); delete entry; break; } } } QStringList BookListModel::knownBookFiles() const { QStringList files; foreach(BookEntry* entry, d->entries) { files.append(entry->filename); } return files; } diff --git a/src/qtquick/CategoryEntriesModel.cpp b/src/qtquick/CategoryEntriesModel.cpp index 3047130..6ba4a3a 100644 --- a/src/qtquick/CategoryEntriesModel.cpp +++ b/src/qtquick/CategoryEntriesModel.cpp @@ -1,422 +1,461 @@ /* * Copyright (C) 2015 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #include "CategoryEntriesModel.h" #include "PropertyContainer.h" #include #include #include #include class CategoryEntriesModel::Private { public: Private(CategoryEntriesModel* qq) : q(qq) {}; ~Private() { // No deleting the entries - this is done by the master BookListModel already, so do that at your own risk } CategoryEntriesModel* q; QString name; QList entries; QList categoryModels; QObject* wrapBookEntry(const BookEntry* entry) { PropertyContainer* obj = new PropertyContainer("book", q); obj->setProperty("author", entry->author); obj->setProperty("currentPage", QString::number(entry->currentPage)); obj->setProperty("filename", entry->filename); obj->setProperty("filetitle", entry->filetitle); obj->setProperty("created", entry->created); obj->setProperty("lastOpenedTime", entry->lastOpenedTime); obj->setProperty("publisher", entry->publisher); obj->setProperty("series", entry->series); obj->setProperty("title", entry->title); obj->setProperty("totalPages", entry->totalPages); obj->setProperty("thumbnail", entry->thumbnail); obj->setProperty("description", entry->description); obj->setProperty("comment", entry->comment); obj->setProperty("tags", entry->tags); obj->setProperty("rating", QString::number(entry->rating)); return obj; } }; CategoryEntriesModel::CategoryEntriesModel(QObject* parent) : QAbstractListModel(parent) , d(new Private(this)) { connect(this, SIGNAL(entryDataUpdated(BookEntry*)), this, SLOT(entryDataChanged(BookEntry*))); connect(this, SIGNAL(entryRemoved(BookEntry*)), this, SLOT(entryRemove(BookEntry*))); } CategoryEntriesModel::~CategoryEntriesModel() { delete d; } QHash CategoryEntriesModel::roleNames() const { QHash roles; roles[FilenameRole] = "filename"; roles[FiletitleRole] = "filetitle"; roles[TitleRole] = "title"; roles[SeriesRole] = "series"; + roles[SeriesNumbersRole] = "seriesNumber"; + roles[SeriesVolumesRole] = "seriesVolume"; roles[AuthorRole] = "author"; roles[PublisherRole] = "publisher"; roles[CreatedRole] = "created"; roles[LastOpenedTimeRole] = "lastOpenedTime"; roles[TotalPagesRole] = "totalPages"; roles[CurrentPageRole] = "currentPage"; roles[CategoryEntriesModelRole] = "categoryEntriesModel"; roles[CategoryEntryCountRole] = "categoryEntriesCount"; roles[ThumbnailRole] = "thumbnail"; roles[DescriptionRole] = "description"; roles[CommentRole] = "comment"; roles[TagsRole] = "tags"; roles[RatingRole] = "rating"; return roles; } QVariant CategoryEntriesModel::data(const QModelIndex& index, int role) const { QVariant result; if(index.isValid() && index.row() > -1) { if(index.row() < d->categoryModels.count()) { CategoryEntriesModel* model = d->categoryModels[index.row()]; switch(role) { case Qt::DisplayRole: case TitleRole: result.setValue(model->name()); break; case CategoryEntryCountRole: result.setValue(model->bookCount()); break; case CategoryEntriesModelRole: result.setValue(model); break; default: result.setValue(QString("Unknown role")); break; } } else { const BookEntry* entry = d->entries[index.row() - d->categoryModels.count()]; switch(role) { case Qt::DisplayRole: case FilenameRole: result.setValue(entry->filename); break; case FiletitleRole: result.setValue(entry->filetitle); break; case TitleRole: result.setValue(entry->title); break; case SeriesRole: result.setValue(entry->series); break; + case SeriesNumbersRole: + result.setValue(entry->seriesNumbers); + break; + case SeriesVolumesRole: + result.setValue(entry->seriesVolumes); + break; case AuthorRole: result.setValue(entry->author); break; case PublisherRole: result.setValue(entry->publisher); break; case CreatedRole: result.setValue(entry->created); break; case LastOpenedTimeRole: result.setValue(entry->lastOpenedTime); break; case TotalPagesRole: result.setValue(entry->totalPages); break; case CurrentPageRole: result.setValue(entry->currentPage); break; case CategoryEntriesModelRole: // Nothing, if we're not equipped with one such... break; case CategoryEntryCountRole: result.setValue(0); break; case ThumbnailRole: result.setValue(entry->thumbnail); break; case DescriptionRole: result.setValue(entry->description); break; case CommentRole: result.setValue(entry->comment); break; case TagsRole: result.setValue(entry->tags); break; case RatingRole: result.setValue(entry->rating); break; default: result.setValue(QString("Unknown role")); break; } } } return result; } int CategoryEntriesModel::rowCount(const QModelIndex& parent) const { if(parent.isValid()) return 0; return d->categoryModels.count() + d->entries.count(); } void CategoryEntriesModel::append(BookEntry* entry, Roles compareRole) { int insertionIndex = 0; + int seriesOne = -1; int seriesTwo = -1; + if(compareRole == SeriesRole) { + seriesOne = entry->series.indexOf(name()); + if (entry->series.contains(name(), Qt::CaseInsensitive) && seriesOne == -1){ + for (int s=0; sseries.size();s++) { + if (name().toLower() == entry->series.at(s).toLower()) { + seriesOne = s; + } + } + } + } for(; insertionIndex < d->entries.count(); ++insertionIndex) { + if(compareRole == SeriesRole) { + seriesTwo = d->entries.at(insertionIndex)->series.indexOf(name()); + if ( d->entries.at(insertionIndex)->series.contains(name(), Qt::CaseInsensitive) && seriesTwo == -1){ + for (int s=0; s< d->entries.at(insertionIndex)->series.size();s++) { + if (name().toLower() == d->entries.at(insertionIndex)->series.at(s).toLower()) { + seriesTwo = s; + } + } + } + } if(compareRole == CreatedRole) { if(entry->created <= d->entries.at(insertionIndex)->created) { continue; } break; } + else if((seriesOne>-1 && seriesTwo>-1) + && entry->seriesNumbers.at(seriesOne).toInt() > 0 + && d->entries.at(insertionIndex)->seriesNumbers.at(seriesTwo).toInt() > 0) + { + if (entry->seriesVolumes.at(seriesOne).toInt() >= d->entries.at(insertionIndex)->seriesVolumes.at(seriesTwo).toInt() + && entry->seriesNumbers.at(seriesOne).toInt() > d->entries.at(insertionIndex)->seriesNumbers.at(seriesTwo).toInt()) + {continue;} + qDebug() << name() << entry->seriesNumbers.at(seriesOne).toInt() << d->entries.at(insertionIndex)->seriesNumbers.at(seriesTwo).toInt(); + break; + } else { if(QString::localeAwareCompare(d->entries.at(insertionIndex)->title, entry->title) > 0) { break; } } } beginInsertRows(QModelIndex(), insertionIndex, insertionIndex); d->entries.insert(insertionIndex, entry); endInsertRows(); } QString CategoryEntriesModel::name() const { return d->name; } void CategoryEntriesModel::setName(const QString& newName) { d->name = newName; } QObject * CategoryEntriesModel::leafModelForEntry(BookEntry* entry) { QObject* model(nullptr); if(d->categoryModels.count() == 0) { if(d->entries.contains(entry)) { model = this; } } else { Q_FOREACH(CategoryEntriesModel* testModel, d->categoryModels) { model = testModel->leafModelForEntry(entry); if(model) { break; } } } return model; } -void CategoryEntriesModel::addCategoryEntry(const QString& categoryName, BookEntry* entry) +void CategoryEntriesModel::addCategoryEntry(const QString& categoryName, BookEntry* entry, Roles compareRole) { if(categoryName.length() > 0) { QStringList splitName = categoryName.split("/"); // qDebug() << "Parsing" << categoryName; QString nextCategory = splitName.takeFirst(); CategoryEntriesModel* categoryModel = nullptr; Q_FOREACH(CategoryEntriesModel* existingModel, d->categoryModels) { if(existingModel->name().toLower() == nextCategory.toLower()) { categoryModel = existingModel; break; } } if(!categoryModel) { categoryModel = new CategoryEntriesModel(this); connect(this, SIGNAL(entryDataUpdated(BookEntry*)), categoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(this, SIGNAL(entryRemoved(BookEntry*)), categoryModel, SIGNAL(entryRemoved(BookEntry*))); categoryModel->setName(nextCategory); int insertionIndex = 0; for(; insertionIndex < d->categoryModels.count(); ++insertionIndex) { if(QString::localeAwareCompare(d->categoryModels.at(insertionIndex)->name(), categoryModel->name()) > 0) { break; } } beginInsertRows(QModelIndex(), insertionIndex, insertionIndex); d->categoryModels.insert(insertionIndex, categoryModel); endInsertRows(); } if (categoryModel->indexOfFile(entry->filename) == -1) { - categoryModel->append(entry); + categoryModel->append(entry, compareRole); } categoryModel->addCategoryEntry(splitName.join("/"), entry); } } QObject* CategoryEntriesModel::get(int index) { BookEntry* entry = new BookEntry(); bool deleteEntry = true; if(index > -1 && index < d->entries.count()) { entry = d->entries.at(index); deleteEntry = false; } QObject* obj = d->wrapBookEntry(entry); if(deleteEntry) { delete entry; } return obj; } int CategoryEntriesModel::indexOfFile(QString filename) { int index = -1, i = 0; if(QFile::exists(filename)) { Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == filename) { index = i; break; } ++i; } } return index; } bool CategoryEntriesModel::indexIsBook(int index) { if(index < d->categoryModels.count() || index >= rowCount()) { return false; } return true; } int CategoryEntriesModel::bookCount() const { return d->entries.count(); } QObject* CategoryEntriesModel::getEntry(int index) { PropertyContainer* obj = new PropertyContainer("book", this); if(index < 0 && index > rowCount() -1) { // don't be a silly person, you can't get a nothing... } else if(index > d->categoryModels.count() - 1) { // This is a book - get a book! obj = qobject_cast(get(index - d->categoryModels.count())); } else { CategoryEntriesModel* catEntry = d->categoryModels.at(index); obj->setProperty("title", catEntry->name()); obj->setProperty("categoryEntriesCount", catEntry->bookCount()); obj->setProperty("entriesModel", QVariant::fromValue(catEntry)); } return obj; } QObject* CategoryEntriesModel::bookFromFile(QString filename) { PropertyContainer* obj = qobject_cast(get(indexOfFile(filename))); if(obj->property("filename").toString().isEmpty()) { if(QFileInfo::exists(filename)) { QFileInfo info(filename); obj->setProperty("title", info.completeBaseName()); obj->setProperty("created", info.created()); KFileMetaData::UserMetaData data(filename); if (data.hasAttribute("peruse.currentPage")) { int currentPage = data.attribute("peruse.currentPage").toInt(); obj->setProperty("currentPage", QVariant::fromValue(currentPage)); } if (data.hasAttribute("peruse.totalPages")) { int totalPages = data.attribute("peruse.totalPages").toInt(); obj->setProperty("totalPages", QVariant::fromValue(totalPages)); } obj->setProperty("rating", QVariant::fromValue(data.rating())); if (!data.tags().isEmpty()) { obj->setProperty("tags", QVariant::fromValue(data.tags())); } if (!data.userComment().isEmpty()) { obj->setProperty("comment", QVariant::fromValue(data.userComment())); } obj->setProperty("filename", filename); QString thumbnail; if(filename.toLower().endsWith("cbr")) { thumbnail = QString("image://comiccover/").append(filename); } #ifdef USE_PERUSE_PDFTHUMBNAILER else if(filename.toLower().endsWith("pdf")) { thumbnail = QString("image://pdfcover/").append(filename); } #endif else { thumbnail = QString("image://preview/").append(filename); } obj->setProperty("thumbnail", thumbnail); } } return obj; } void CategoryEntriesModel::entryDataChanged(BookEntry* entry) { int entryIndex = d->entries.indexOf(entry) + d->categoryModels.count(); QModelIndex changed = index(entryIndex); dataChanged(changed, changed); } void CategoryEntriesModel::entryRemove(BookEntry* entry) { int listIndex = d->entries.indexOf(entry); if(listIndex > -1) { int entryIndex = listIndex + d->categoryModels.count(); beginRemoveRows(QModelIndex(), entryIndex, entryIndex); d->entries.removeAll(entry); endRemoveRows(); } } diff --git a/src/qtquick/CategoryEntriesModel.h b/src/qtquick/CategoryEntriesModel.h index 6d7697d..ebeb8e1 100644 --- a/src/qtquick/CategoryEntriesModel.h +++ b/src/qtquick/CategoryEntriesModel.h @@ -1,207 +1,211 @@ /* * Copyright (C) 2015 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #ifndef CATEGORYENTRIESMODEL_H #define CATEGORYENTRIESMODEL_H #include #include class CategoryEntriesModel; /** * \brief A struct for an Entry to the Book Database. */ struct BookEntry { BookEntry() : totalPages(0) , currentPage(0) {} QString filename; QString filetitle; QString title; QStringList series; + QStringList seriesNumbers; + QStringList seriesVolumes; QStringList author; QString publisher; QDateTime created; QDateTime lastOpenedTime; int totalPages; int currentPage; QString thumbnail; QStringList description; QString comment; QStringList tags; int rating; }; /** * \brief Model to handle the filter categories. * * This model in specific handles which categories there are * and which books are assigned to a category, if so, which. * * Used to handle sorting by author, title and so forth. * Is extended by BookListModel. * * categories and book entries are both in the same model * because there can be books that are not assigned categories. * Similarly, categories can contain categories, like in the case * of folder category. */ class CategoryEntriesModel : public QAbstractListModel { Q_OBJECT public: explicit CategoryEntriesModel(QObject* parent = nullptr); ~CategoryEntriesModel() override; /** * \brief Extra roles for the book entry access. */ enum Roles { FilenameRole = Qt::UserRole + 1, FiletitleRole, TitleRole, - SeriesRole, + SeriesRole, // Gets a stringlist of series this book is part of. + SeriesNumbersRole, // Gets a stringlist of numbers, which represent the sequence number the book has within each series. + SeriesVolumesRole, // Get a stringlist of numbers, which represent the volume number the book has within a series. This is optional. AuthorRole, PublisherRole, CreatedRole, LastOpenedTimeRole, TotalPagesRole, CurrentPageRole, CategoryEntriesModelRole, CategoryEntryCountRole, ThumbnailRole, DescriptionRole, CommentRole, TagsRole, RatingRole }; /** * @returns names for the extra roles defined. */ QHash roleNames() const override; /** * \brief Access the data inside the CategoryEntriesModel. * @param index The QModelIndex at which you wish to access the data. * @param role An enumerator of the type of data you want to access. * Is extended by the Roles enum. * * @return a QVariant with the book entry's data. */ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; /** * @param parent The QModel index of the parent. This only counts for * tree like page structures, and thus defaults to a freshly constructed * QModelIndex. A wellformed QModelIndex will cause this function to return 0 * @returns the number of total rows(bookentries and categories) there are. */ int rowCount(const QModelIndex& parent = QModelIndex()) const override; /** * \brief Add a book entry to the CategoryEntriesModel. * * @param entry The BookEntry to add. * @param compareRole The role that determines the data to sort the entry into. * Defaults to the Book title. */ void append(BookEntry* entry, Roles compareRole = TitleRole); /** * \brief Add a book entry to a category. * * This also adds it to the model's list of entries. */ - void addCategoryEntry(const QString& categoryName, BookEntry* entry); + void addCategoryEntry(const QString& categoryName, BookEntry* entry, Roles compareRole = TitleRole); /** * @param index an integer index pointing at the desired book. * @returns a QObject wrapper around a BookEntry struct for the given index. */ Q_INVOKABLE QObject* get(int index); /** * TODO: This is backwards... need to fox this to make get return the actual thing, not just a book, and create a getter for books... * @return an entry object. This can be either a category or a book. * @param index the index of the object. */ Q_INVOKABLE QObject* getEntry(int index); /** * @return an entry object for the given filename. Used to get the recently * read books. * @param filename the filename associated with an entry object. */ Q_INVOKABLE QObject* bookFromFile(QString filename); /** * @return an entry index for the given filename. * @param filename the filename associated with an entry object. */ Q_INVOKABLE int indexOfFile(QString filename); /** * @return whether the entry is a bookentry or a category entry. * @param index the index of the entry. */ Q_INVOKABLE bool indexIsBook(int index); /** * @return an integer with the total books in the model. */ int bookCount() const; /** * \brief Fires when a book entry is updated. * @param entry The updated entry * * Used in the BookListModel::setBookData() */ Q_SIGNAL void entryDataUpdated(BookEntry* entry); /** * \brief set a book entry as changed. * @param entry The changed entry. */ Q_SLOT void entryDataChanged(BookEntry* entry); /** * \brief Fires when a book entry is removed. * @param entry The removed entry */ Q_SIGNAL void entryRemoved(BookEntry* entry); /** * \brief Remove a book entry. * @param entry The entry to remove. */ Q_SLOT void entryRemove(BookEntry* entry); // This will iterate over all sub-models and find the model which contains the entry, or null if not found QObject* leafModelForEntry(BookEntry* entry); protected: /** * @return the name of the model. */ QString name() const; /** * \brief set the name of the model. * @param newName QString with the name. */ void setName(const QString& newName); private: class Private; Private* d; }; #endif//CATEGORYENTRIESMODEL_H