diff --git a/src/app/qml/PeruseMain.qml b/src/app/qml/PeruseMain.qml index 0f30408..5757d3b 100644 --- a/src/app/qml/PeruseMain.qml +++ b/src/app/qml/PeruseMain.qml @@ -1,299 +1,315 @@ /* * 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 . * */ import QtQuick 2.1 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.4 as QtControls import QtQuick.Window 2.2 import org.kde.kirigami 2.1 as Kirigami import org.kde.peruse 0.1 as Peruse import org.kde.contentlist 0.1 /** * @brief main application window. * * This splits the window in two sections: * - A section where you can select comics. * - A "global drawer" which can be used to switch between categories * and access settings and the open book dialog. * * The global drawer controls which is the main component on the left. * It initializes on WelcomePage. The category filters are each handled * by a BookShelf. The store page by Store and the settings by Settings. * * This also controls the bookViewer, which is a Book object where the * main reading of comics is done. * * There is also the PeruseContextDrawer, which is only accesible on the book * page and requires flicking in from the right. */ Kirigami.ApplicationWindow { id: mainWindow; title: "Comic Book Reader"; property int animationDuration: 200; property bool isLoading: true; pageStack.initialPage: welcomePage; visible: true; // If the controls are not visible, being able to drag the pagestack feels really weird, // so we just turn that ability off :) pageStack.interactive: controlsVisible; /// Which type of device we're running on. 0 is desktop, 1 is phone property int deviceType: PLASMA_PLATFORM.substring(0, 5) === "phone" ? 1 : 0; property int deviceTypeDesktop: 0; property int deviceTypePhone: 1; function showBook(filename, currentPage) { if(mainWindow.pageStack.lastItem.objectName === "bookViewer") { mainWindow.pageStack.pop(); } mainWindow.pageStack.layers.push(bookViewer, { focus: true, file: filename, currentPage: currentPage }) peruseConfig.bookOpened(filename); } Peruse.BookListModel { id: contentList; contentModel: ContentList { autoSearch: false onSearchCompleted: { mainWindow.isLoading = false; mainWindow.globalDrawer.actions = globalDrawerActions; } ContentQuery { type: ContentQuery.Comics locations: peruseConfig.bookLocations } } onCacheLoadedChanged: { if(!cacheLoaded) { return; } contentList.contentModel.setKnownFiles(contentList.knownBookFiles()); contentList.contentModel.startSearch() } } Peruse.Config { id: peruseConfig; } function homeDir() { return peruseConfig.homeDir(); } header: Kirigami.ApplicationHeader {} contextDrawer: PeruseContextDrawer { id: contextDrawer; } globalDrawer: Kirigami.GlobalDrawer { title: i18nc("application title for the sidebar", "Peruse"); titleIcon: "peruse"; drawerOpen: PLASMA_PLATFORM.substring(0, 5) === "phone" ? false : true; modal: PLASMA_PLATFORM.substring(0, 5) === "phone" ? true : false; actions: [] } property list globalDrawerActions: [ Kirigami.Action { text: i18nc("Switch to the welcome page", "Welcome"); iconName: "start-over"; checked: mainWindow.currentCategory === "welcomePage"; onTriggered: { changeCategory(welcomePage); pageStack.currentItem.updateRecent(); } }, Kirigami.Action { }, Kirigami.Action { text: i18nc("Switch to the listing page showing the most recently discovered books", "Recently Added Books"); iconName: "appointment-new"; checked: mainWindow.currentCategory === "bookshelfAdded"; onTriggered: changeCategory(bookshelfAdded); }, Kirigami.Action { text: i18nc("Switch to the listing page showing items grouped by title", "Group by Title"); iconName: "view-media-title"; checked: mainWindow.currentCategory === "bookshelfTitle"; onTriggered: changeCategory(bookshelfTitle); }, Kirigami.Action { text: i18nc("Switch to the listing page showing items grouped by author", "Group by Author"); iconName: "actor"; checked: mainWindow.currentCategory === "bookshelfAuthor"; onTriggered: changeCategory(bookshelfAuthor); }, Kirigami.Action { text: i18nc("Switch to the listing page showing items grouped by series", "Group by Series"); iconName: "edit-group"; checked: currentCategory === "bookshelfSeries"; onTriggered: changeCategory(bookshelfSeries); }, Kirigami.Action { text: i18nc("Switch to the listing page showing items grouped by publisher", "Group by Publisher"); iconName: "view-media-publisher"; checked: mainWindow.currentCategory === "bookshelfPublisher"; onTriggered: changeCategory(bookshelfPublisher); }, + Kirigami.Action { + text: i18nc("Switch to the listing page showing items grouped by keywords, characters or genres", "Group by Keywords"); + iconName: "tag"; + checked: mainWindow.currentCategory === "bookshelfKeywords"; + onTriggered: changeCategory(bookshelfKeywords); + }, Kirigami.Action { text: i18nc("Switch to the listing page showing items grouped by their filesystem folder", "Filter by Folder"); iconName: "tag-folder"; checked: mainWindow.currentCategory === "bookshelfFolder"; onTriggered: changeCategory(bookshelfFolder); }, Kirigami.Action { }, Kirigami.Action { text: i18nc("Open a book from somewhere on disk (uses the open dialog, or a drilldown on touch devices)", "Open Other..."); iconName: "document-open"; onTriggered: openOther(); }, Kirigami.Action { text: i18nc("Switch to the book store page", "Get Hot New Books"); iconName: "get-hot-new-stuff"; checked: mainWindow.currentCategory === "storePage"; onTriggered: changeCategory(storePage); }, Kirigami.Action { }, Kirigami.Action { text: i18nc("Open the settings page", "Settings"); iconName: "configure" checked: mainWindow.currentCategory === "settingsPage"; onTriggered: changeCategory(settingsPage); } ] Component { id: welcomePage; WelcomePage { onBookSelected: mainWindow.showBook(filename, currentPage); } } Component { id: bookViewer; Book { id: viewerRoot; onCurrentPageChanged: { contentList.setBookData(viewerRoot.file, "currentPage", viewerRoot.currentPage); } onTotalPagesChanged: { contentList.setBookData(viewerRoot.file, "totalPages", viewerRoot.totalPages); } } } Component { id: bookshelfTitle; Bookshelf { model: contentList.titleCategoryModel; headerText: i18nc("Title of the page with books grouped by the title start letters", "Group by Title"); onBookSelected: mainWindow.showBook(filename, currentPage); categoryName: "bookshelfTitle"; } } Component { id: bookshelfAdded; Bookshelf { model: contentList.newlyAddedCategoryModel; headerText: i18nc("Title of the page with all books ordered by which was added most recently", "Recently Added Books"); sectionRole: "created"; sectionCriteria: ViewSection.FullString; onBookSelected: mainWindow.showBook(filename, currentPage); categoryName: "bookshelfAdded"; } } Component { id: bookshelfSeries; Bookshelf { model: contentList.seriesCategoryModel; headerText: i18nc("Title of the page with books grouped by what series they are in", "Group by Series"); onBookSelected: mainWindow.showBook(filename, currentPage); categoryName: "bookshelfSeries"; } } Component { id: bookshelfAuthor; Bookshelf { model: contentList.authorCategoryModel; headerText: i18nc("Title of the page with books grouped by author", "Group by Author"); onBookSelected: mainWindow.showBook(filename, currentPage); categoryName: "bookshelfAuthor"; } } Component { id: bookshelfPublisher; Bookshelf { model: contentList.publisherCategoryModel; headerText: i18nc("Title of the page with books grouped by who published them", "Group by Publisher"); onBookSelected: mainWindow.showBook(filename, currentPage); categoryName: "bookshelfPublisher"; } } + Component { + id: bookshelfKeywords; + Bookshelf { + model: contentList.keywordCategoryModel; + headerText: i18nc("Title of the page with books grouped by keywords, character or genres", "Group by Keywords, Characters and Genres"); + onBookSelected: mainWindow.showBook(filename, currentPage); + categoryName: "bookshelfKeywords"; + } + } + Component { id: bookshelfFolder; Bookshelf { model: contentList.folderCategoryModel; headerText: i18nc("Title of the page with books grouped by what folder they are in", "Filter by Folder"); onBookSelected: mainWindow.showBook(filename, currentPage); categoryName: "bookshelfFolder"; } } Component { id: bookshelf; Bookshelf { onBookSelected: mainWindow.showBook(filename, currentPage); } } Component { id: storePage; Store { } } Component { id: settingsPage; Settings { } } property string currentCategory: "welcomePage"; function changeCategory(categoryItem) { // Clear all the way to the welcome page if we change the category... mainWindow.pageStack.clear(); mainWindow.pageStack.push(categoryItem); currentCategory = mainWindow.pageStack.currentItem.categoryName; if (PLASMA_PLATFORM.substring(0, 5) === "phone") { globalDrawer.close(); } } } diff --git a/src/qtquick/BookDatabase.cpp b/src/qtquick/BookDatabase.cpp index 010c4b6..d71ef28 100644 --- a/src/qtquick/BookDatabase.cpp +++ b/src/qtquick/BookDatabase.cpp @@ -1,165 +1,171 @@ /* * 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, seriesVolumes varchar, seriesNumbers varchar)"))) { + 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, genres varchar, keywords varchar, characters 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, seriesNumbers, seriesVolumes FROM books"); + QSqlQuery allEntries("SELECT filename, filetitle, title, series, author, publisher, created, lastOpenedTime, totalPages, currentPage, thumbnail, description, comment, tags, rating, seriesNumbers, seriesVolumes, genres, keywords, characters 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->series = allEntries.value(3).toString().split(",", QString::SkipEmptyParts); + entry->author = allEntries.value(4).toString().split(",", QString::SkipEmptyParts); 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->description = allEntries.value(11).toString().split("\n", QString::SkipEmptyParts); entry->comment = allEntries.value(12).toString(); - entry->tags = allEntries.value(13).toString().split(","); + entry->tags = allEntries.value(13).toString().split(",", QString::SkipEmptyParts); entry->rating = allEntries.value(14).toInt(); - entry->seriesNumbers = allEntries.value(15).toString().split(","); - entry->seriesVolumes = allEntries.value(16).toString().split(","); + entry->seriesNumbers = allEntries.value(15).toString().split(",", QString::SkipEmptyParts); + entry->seriesVolumes = allEntries.value(16).toString().split(",", QString::SkipEmptyParts); + entry->genres = allEntries.value(17).toString().split(",", QString::SkipEmptyParts); + entry->keywords = allEntries.value(18).toString().split(",", QString::SkipEmptyParts); + entry->characters = allEntries.value(19).toString().split(",", QString::SkipEmptyParts); 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, seriesNumbers, seriesVolumes) " - "VALUES (:filename, :filetitle, :title, :series, :author, :publisher, :created, :lastOpenedTime, :totalPages, :currentPage, :thumbnail, :description, :comment, :tags, :rating, :seriesNumbers, :seriesVolumes)"); + newEntry.prepare("INSERT INTO books (filename, filetitle, title, series, author, publisher, created, lastOpenedTime, totalPages, currentPage, thumbnail, description, comment, tags, rating, seriesNumbers, seriesVolumes, genres, keywords, characters) " + "VALUES (:filename, :filetitle, :title, :series, :author, :publisher, :created, :lastOpenedTime, :totalPages, :currentPage, :thumbnail, :description, :comment, :tags, :rating, :seriesNumbers, :seriesVolumes, :genres, :keywords, :characters)"); 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(":description", entry->description.join("\n")); 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.bindValue(":genres", entry->genres.join(",")); + newEntry.bindValue(":keywords", entry->keywords.join(",")); + newEntry.bindValue(":characters", entry->characters.join(",")); newEntry.exec(); d->closeDb(); } void BookDatabase::removeEntry(BookEntry* entry) { if(!d->prepareDb()) { return; } qDebug() << "Removing book from the database" << entry->filename; QSqlQuery removeEntry; removeEntry.prepare("DELETE FROM books WHERE filename='"+entry->filename+"';"); removeEntry.exec(); d->closeDb(); } diff --git a/src/qtquick/BookListModel.cpp b/src/qtquick/BookListModel.cpp index 54bb15d..145d553 100644 --- a/src/qtquick/BookListModel.cpp +++ b/src/qtquick/BookListModel.cpp @@ -1,420 +1,447 @@ /* * 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) + , keywordCategoryModel(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* keywordCategoryModel; 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(!keywordCategoryModel) + { + keywordCategoryModel = new CategoryEntriesModel(q); + connect(q, SIGNAL(entryDataUpdated(BookEntry*)), keywordCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); + connect(q, SIGNAL(entryRemoved(BookEntry*)), keywordCategoryModel, SIGNAL(entryRemoved(BookEntry*))); + emit q->keywordCategoryModelChanged(); + } 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, 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); } + for (int i=0; igenres.size(); i++) { + keywordCategoryModel->addCategoryEntry(QString("Genre/").append(entry->genres.at(i)), entry, GenreRole); + } + for (int i=0; icharacters.size(); i++) { + keywordCategoryModel->addCategoryEntry(QString("Characters/").append(entry->characters.at(i)), entry, GenreRole); + } + for (int i=0; ikeywords.size(); i++) { + keywordCategoryModel->addCategoryEntry(QString("Keywords/").append(entry->keywords.at(i)), entry, GenreRole); + } + } void loadCache(BookListModel* q) { QList entries = db->loadEntries(); if(entries.count() > 0) { initializeSubModels(q); } int i = 0; foreach(BookEntry* entry, entries) { /* * This might turn out a little slow, but we should avoid having entries * that do not exist. If we end up with slowdown issues when loading the * cache this would be a good place to start investigating. */ if (QFileInfo::exists(entry->filename)) { addEntry(q, entry); if(++i % 100 == 0) { emit q->countChanged(); qApp->processEvents(); } } else { db->removeEntry(entry); } } 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()) { 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()) { 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(""); + entry->genres = acbfDocument->metaData()->bookInfo()->genres(); + entry->characters = acbfDocument->metaData()->bookInfo()->characters(); + entry->keywords = acbfDocument->metaData()->bookInfo()->keywords(""); } 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::keywordCategoryModel() const +{ + return d->keywordCategoryModel; +} + 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); d->db->removeEntry(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/BookListModel.h b/src/qtquick/BookListModel.h index d40a5e2..143f4a1 100644 --- a/src/qtquick/BookListModel.h +++ b/src/qtquick/BookListModel.h @@ -1,236 +1,248 @@ /* * 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 BOOKLISTMODEL_H #define BOOKLISTMODEL_H #include "CategoryEntriesModel.h" #include /** * \brief Main catalogue model class. * * BookListModel extends CategoryEntriesModel, and is the main model that * handles book entries and the different categories that books can be in. * * It also extends QQmlParseStatus to ensure that the loading the cache of * books is postponed until the application UI has been painted at least once. * * BookListModel keeps track of which books there are, how they can be sorted * and how far the reader is in reading a specific book. * * It caches its entries in the BookDataBase. * * ContentModel is the model used to enable searching the collection, it is * typically a ContentList. */ class BookListModel : public CategoryEntriesModel, public QQmlParserStatus { Q_OBJECT /** * \brief count holds how many entries there are in the catalogue. */ Q_PROPERTY(int count READ count NOTIFY countChanged) /** * \brief The content model is an abstract list model that holds data to search through. */ Q_PROPERTY(QObject* contentModel READ contentModel WRITE setContentModel NOTIFY contentModelChanged) /** * \brief The "newly added" category entries model manages the newly added entries. */ Q_PROPERTY(QObject* newlyAddedCategoryModel READ newlyAddedCategoryModel NOTIFY newlyAddedCategoryModelChanged) /** * \brief The "title" category entries model manages the sorting of entries by title. */ Q_PROPERTY(QObject* titleCategoryModel READ titleCategoryModel NOTIFY titleCategoryModelChanged) /** * \brief The "author" category entries model manages the sorting of entries by author. */ Q_PROPERTY(QObject* authorCategoryModel READ authorCategoryModel NOTIFY authorCategoryModelChanged) /** * \brief The "series" category entries model managed the sorting of entry by series. */ Q_PROPERTY(QObject* seriesCategoryModel READ seriesCategoryModel NOTIFY seriesCategoryModelChanged) /** * \brief The "publisher" category entries model managed the sorting of entry by publisher. */ Q_PROPERTY(QObject* publisherCategoryModel READ publisherCategoryModel NOTIFY publisherCategoryModelChanged) + /** + * \brief The "keyword" category entries model managed the sorting of entry by keyword. + */ + Q_PROPERTY(QObject* keywordCategoryModel READ keywordCategoryModel NOTIFY keywordCategoryModelChanged) /** * \brief The "folder" category entries model managed the sorting of entry by file system folder. */ Q_PROPERTY(QObject* folderCategoryModel READ folderCategoryModel NOTIFY folderCategoryModelChanged) /** * \brief cacheLoaded holds whether the database cache has been loaded.. */ Q_PROPERTY(bool cacheLoaded READ cacheLoaded NOTIFY cacheLoadedChanged) Q_ENUMS(Grouping) Q_INTERFACES(QQmlParserStatus) public: explicit BookListModel(QObject* parent = nullptr); ~BookListModel() override; /** * Inherited from QmlParserStatus, not implemented. */ void classBegin() override {}; /** * \brief triggers the loading of the cache. * Inherited from QmlParserStatus */ void componentComplete() override; /** * \brief Enum holding the different categories implemented. */ enum Grouping { GroupByNone = 0, GroupByRecentlyAdded, GroupByRecentlyRead, GroupByTitle, GroupByAuthor, GroupByPublisher }; /** * @return the contentModel. Used for searching. */ QObject* contentModel() const; /** * \brief set the ContentModel. * @param newModel The new content model. */ void setContentModel(QObject* newModel); /** * \brief Fires when the content model has changed. */ Q_SIGNAL void contentModelChanged(); /** * @returns how many entries there are in the catelogue. */ int count() const; /** * \brief Fires when the count has changed. */ Q_SIGNAL void countChanged(); /** * @return The categoryEntriesModel that manages the sorting of entries by title. */ QObject* titleCategoryModel() const; /** * \brief Fires when the titleCategoryModel has changed or finished initializing. */ Q_SIGNAL void titleCategoryModelChanged(); /** * @return The categoryEntriesModel that manages the recently added entries. */ QObject* newlyAddedCategoryModel() const; /** * \brief Fires when the newlyAddedCategoryModel has changed or finished initializing. */ Q_SIGNAL void newlyAddedCategoryModelChanged(); /** * @return The categoryEntriesModel that manages the sorting of entries by author. */ QObject* authorCategoryModel() const; /** * \brief Fires when the authorCategoryModel has changed or finished initializing. */ Q_SIGNAL void authorCategoryModelChanged(); /** * @return The categoryEntriesModel that manages the sorting of entries by series. */ QObject* seriesCategoryModel() const; /** * \brief Fires when the seriesCategoryModel has changed or finished initializing. */ Q_SIGNAL void seriesCategoryModelChanged(); /** * Returns the leaf model representing the series the entry with the passed URL is a part of * Base assumption: A book is only part of one series. This is not always true, but not sure how * to sensibly represent that. * * @param fileName the File Name of the entry to get the series of. */ Q_INVOKABLE QObject* seriesModelForEntry(QString fileName); /** * @return The categoryEntriesModel that manages the sorting of entries by publisher. */ QObject* publisherCategoryModel() const; /** * \brief Fires when the publisherCategoryModel has changed or finished initializing. */ Q_SIGNAL void publisherCategoryModelChanged(); + /** + * @return The categoryEntriesModel that manages the sorting of entries by keywords, names and genres. + */ + QObject* keywordCategoryModel() const; + /** + * \brief Fires when the keywordCategoryModel has changed or finished initializing. + */ + Q_SIGNAL void keywordCategoryModelChanged(); /** * @return The categoryEntriesModel that manages the sorting of entries by folder. */ QObject* folderCategoryModel() const; /** * \brief Fires when the folderCategoryModel has changed or finished initializing. */ Q_SIGNAL void folderCategoryModelChanged(); /** * @returns whether the cache is loaded from the database. */ bool cacheLoaded() const; /** * \brief Fires when the cache is done loading. */ Q_SIGNAL void cacheLoadedChanged(); /** * \brief Update the data of a book at runtime * * This is used in to update totalPages and currentPage. * * @param fileName The filename to update the page for. * @param property The property to update, can be "currentPage" or * "totalPages". * @param value The value to set it to. */ Q_INVOKABLE void setBookData(QString fileName, QString property, QString value); /** * Delete a book from the model, and optionally delete the entry from file storage. * @param fileName The filename of the book to remove. * @param deleteFile Whether to also delete the file from the disk. */ Q_INVOKABLE void removeBook(QString fileName, bool deleteFile = false); /** * \brief A list of the files currently known by the applications * @returns a QStringList with paths to known books. */ Q_INVOKABLE QStringList knownBookFiles() const; private: class Private; Private* d; Q_SLOT void contentModelItemsInserted(QModelIndex index,int first, int last); }; #endif//BOOKLISTMODEL_H diff --git a/src/qtquick/CategoryEntriesModel.cpp b/src/qtquick/CategoryEntriesModel.cpp index 7bdb35e..32bddb4 100644 --- a/src/qtquick/CategoryEntriesModel.cpp +++ b/src/qtquick/CategoryEntriesModel.cpp @@ -1,460 +1,473 @@ /* * 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("genres", entry->genres); + obj->setProperty("keywords", entry->keywords); + obj->setProperty("characters", entry->characters); 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[GenreRole] = "genres"; + roles[KeywordRole] = "keywords"; 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 GenreRole: + result.setValue(entry->genres); + break; + case KeywordRole: + result.setValue(entry->keywords); + break; + case CharacterRole: + result.setValue(entry->characters); + 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;} 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, 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, 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 1e53c16..542403a 100644 --- a/src/qtquick/CategoryEntriesModel.h +++ b/src/qtquick/CategoryEntriesModel.h @@ -1,211 +1,217 @@ /* * 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 genres; + QStringList keywords; + QStringList characters; 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, /// For getting a string with the full path to the book. FiletitleRole, /// For getting a string with the basename of the book. TitleRole, /// For getting a string with the proper title of the book. SeriesRole, /// For getting a stringlist of series this book is part of. SeriesNumbersRole, /// For getting a stringlist of numbers, which represent the sequence number the book has within each series. SeriesVolumesRole, /// For getting a stringlist of numbers, which represent the volume number the book has within a series. This is optional. AuthorRole, /// For getting a stringlist of all the authors. PublisherRole, /// For getting a string with the publisher name. CreatedRole, /// For getting the creation date of the book as a QDateTime. LastOpenedTimeRole, /// For getting the last time the book was opened as a QDateTime. TotalPagesRole, /// For getting the total amount of pages in this book. CurrentPageRole, /// For getting the current page as an int. CategoryEntriesModelRole, /// For getting the model of this category. CategoryEntryCountRole, /// For getting the an int with the number of books within this category. ThumbnailRole, /// For getting a thumbnail url for this book. DescriptionRole, /// For getting a stringlist with a book description. CommentRole, /// For getting a string with user assigned comment. TagsRole, /// For getting a stringlist with user assigned tags. - RatingRole /// For getting an int with the rating of the comic. This is gotten from KFileMeta and thus goes from 1-10 with 0 being no rating. + RatingRole, /// For getting an int with the rating of the comic. This is gotten from KFileMeta and thus goes from 1-10 with 0 being no rating. + GenreRole, /// For getting a stringlist with genres assigned to this book. + KeywordRole, /// For getting a stringlist with keywords assigned to this book. Where tags are user assigned, keywords come from the book itself. + CharacterRole /// For getting a stringlist with names of characters in this book. }; /** * @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, 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