diff --git a/src/app/qml/PeruseMain.qml b/src/app/qml/PeruseMain.qml index 317e224..1a33643 100644 --- a/src/app/qml/PeruseMain.qml +++ b/src/app/qml/PeruseMain.qml @@ -1,298 +1,287 @@ /* * 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 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; } - - var bookLocations = peruseConfig.bookLocations; - for(var i = 0; i < bookLocations.length; ++i) { - contentList.contentModel.addLocation(bookLocations[i]); - } - contentList.contentModel.setSearchString("cbz OR cbr OR cb7 OR cbt OR cba OR chm OR djvu OR epub OR pdf"); - contentList.contentModel.addMimetype("application/x-cbz"); - contentList.contentModel.addMimetype("application/x-cbr"); - contentList.contentModel.addMimetype("application/x-cb7"); - contentList.contentModel.addMimetype("application/x-cbt"); - contentList.contentModel.addMimetype("application/x-cba"); - contentList.contentModel.addMimetype("application/vnd.comicbook+zip"); - contentList.contentModel.addMimetype("application/vnd.comicbook+rar"); - contentList.contentModel.addMimetype("application/vnd.ms-htmlhelp"); - contentList.contentModel.addMimetype("image/vnd.djvu"); - contentList.contentModel.addMimetype("image/x-djvu"); - contentList.contentModel.addMimetype("application/epub+zip"); - contentList.contentModel.addMimetype("application/pdf"); contentList.contentModel.setKnownFiles(contentList.knownBookFiles()); - contentList.contentModel.startSearch(); + 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: "Welcome"; iconName: "start-over"; checked: mainWindow.currentCategory === "welcomePage"; checkable: true; 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"; checkable: true; 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"; checkable: true; 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"; checkable: true; 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"; checkable: true; 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 their filesystem folder", "Filter by Folder"); iconName: "tag-folder"; checked: mainWindow.currentCategory === "bookshelfFolder"; checkable: true; 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"; onTriggered: changeCategory(storePage); }, Kirigami.Action { }, Kirigami.Action { text: i18nc("Open the settings page", "Settings"); iconName: "configure" checked: mainWindow.currentCategory === "settingsPage"; checkable: true; 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; 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: 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/contentlist/BalooContentLister.cpp b/src/contentlist/BalooContentLister.cpp index e1c7df5..dd1d618 100644 --- a/src/contentlist/BalooContentLister.cpp +++ b/src/contentlist/BalooContentLister.cpp @@ -1,182 +1,197 @@ /* * 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 "BalooContentLister.h" #include #include #include #include #include #include #include #include #include #include +#include + +#include "ContentQuery.h" class BalooContentLister::Private { public: - Private() {} + Private(BalooContentLister* qq) : q(qq) {} + + BalooContentLister* q = nullptr; + + Baloo::QueryRunnable* createQuery(ContentQuery* contentQuery, const QString& location = QString{}); + QStringList knownFiles; QStringList locations; QString searchString; QList queries; QList queryLocations; + + QMimeDatabase mimeDatabase; }; BalooContentLister::BalooContentLister(QObject* parent) : ContentListerBase(parent) - , d(new Private) + , d(new Private(this)) { } BalooContentLister::~BalooContentLister() { + QThreadPool::globalInstance()->waitForDone(); delete d; } bool BalooContentLister::balooEnabled() const { Baloo::IndexerConfig config; bool result = config.fileIndexingEnabled(); if(result) { // It would be terribly nice with a bit of baloo engine exporting, so // we can ask the database about whether or not it is accessible... // But, this is a catch-all check anyway, so we get a complete "everything's broken" // result if anything is broken... guess it will do :) QProcess statuscheck; statuscheck.start("balooctl", QStringList() << "status"); statuscheck.waitForFinished(); QString output = statuscheck.readAll(); - qDebug() << "Baloo status check says:" << output; if(statuscheck.exitStatus() == QProcess::CrashExit || statuscheck.exitCode() != 0) { result = false; } } return result; } -void BalooContentLister::addLocation(QString path) -{ - d->locations.append(path); -} - -void BalooContentLister::addMimetype(QString mimetype) -{ - Q_UNUSED(mimetype) - // yes, unsatisfactory... we're using this to find comic books for now, and their mimetypes are terrible -} - -void BalooContentLister::setSearchString(const QString& searchString) -{ - d->searchString = searchString; -} - -void BalooContentLister::setKnownFiles(QStringList knownFiles) +void BalooContentLister::startSearch(const QList& queries) { - d->knownFiles = knownFiles; -} - -void BalooContentLister::startSearch() -{ - Q_FOREACH(const QString& location, d->locations) + for(const auto& query : queries) { - Baloo::Query query; - query.setSearchString(d->searchString); - query.setIncludeFolder(location); - - Baloo::QueryRunnable *runnable = new Baloo::QueryRunnable(query); - connect(runnable, SIGNAL(queryResult(Baloo::QueryRunnable*, QString)), - this, SLOT(queryResult(Baloo::QueryRunnable*, QString)), Qt::QueuedConnection); - connect(runnable, SIGNAL(finished(Baloo::QueryRunnable*)), - this, SLOT(queryCompleted(Baloo::QueryRunnable*))); - - d->queries.append(runnable); - d->queryLocations.append(location); + for(const auto& location : query->locations()) + { + d->queries.append(d->createQuery(query, location)); + } + + if(query->locations().isEmpty()) + d->queries.append(d->createQuery(query)); } - // This ensures that, should we decide to search more stuff later, we can do so granularly - d->locations.clear(); if(!d->queries.empty()) { QThreadPool::globalInstance()->start(d->queries.first()); } } void BalooContentLister::queryCompleted(Baloo::QueryRunnable* query) { d->queries.removeAll(query); - d->queryLocations.takeFirst(); if(d->queries.empty()) { emit searchCompleted(); } else { QThreadPool::globalInstance()->start(d->queries.first()); } } -void BalooContentLister::queryResult(Baloo::QueryRunnable* query, QString file) +void BalooContentLister::queryResult(const ContentQuery* query, const QString& location, const QString& file) { - Q_UNUSED(query) - if(d->knownFiles.contains(file)) { return; } // wow, this isn't nice... why is baloo not limiting searches like it's supposed to? - if(!file.startsWith(d->queryLocations.first())) { + if(!file.startsWith(location)) { return; } - QVariantHash metadata; + // Like the one above, this is also not nice: apparently Baloo can return results to + // files that no longer exist on the file system. So we have to check manually whether + // the results provided are actually sensible results... + if(!QFile::exists(file)) { + return; + } + + // It would be nice if Baloo could do mime type filtering on its own... + auto mimeType = d->mimeDatabase.mimeTypeForFile(file).name(); + if(!query->mimeTypes().isEmpty() && !query->mimeTypes().contains(mimeType)) + return; + + auto metadata = metaDataForFile(file); Baloo::File balooFile(file); balooFile.load(); KFileMetaData::PropertyMap properties = balooFile.properties(); KFileMetaData::PropertyMap::const_iterator it = properties.constBegin(); for (; it != properties.constEnd(); it++) { KFileMetaData::PropertyInfo propInfo(it.key()); metadata[propInfo.name()] = it.value(); -// qDebug() << KFileMetaData::PropertyInfo(it.key()).name() << " --> " -// << it.value().toString() << " (" << it.value().typeName() << ")\n"; } - QFileInfo info(file); - metadata["lastModified"] = info.lastModified(); - metadata["created"] = info.created(); - metadata["lastRead"] = info.lastRead(); - - KFileMetaData::UserMetaData data(file); - int currentPage = data.attribute("peruse.currentPage").toInt(); - metadata["currentPage"] = QVariant::fromValue(currentPage); - int totalPages = data.attribute("peruse.totalPages").toInt(); - metadata["totalPages"] = QVariant::fromValue(totalPages); emit fileFound(file, metadata); } + +Baloo::QueryRunnable* BalooContentLister::Private::createQuery(ContentQuery* contentQuery, const QString& location) +{ + auto balooQuery = Baloo::Query{}; + if(!location.isEmpty()) + balooQuery.setIncludeFolder(location); + + switch(contentQuery->type()) + { + case ContentQuery::Audio: + balooQuery.setType("Audio"); + break; + case ContentQuery::Documents: + balooQuery.setType("Document"); + break; + case ContentQuery::Images: + balooQuery.setType("Image"); + break; + case ContentQuery::Video: + balooQuery.setType("Video"); + break; + default: + break; + } + + if(!contentQuery->searchString().isEmpty()) + balooQuery.setSearchString(contentQuery->searchString()); + + auto runnable = new Baloo::QueryRunnable{balooQuery}; + connect(runnable, &Baloo::QueryRunnable::queryResult, [this, contentQuery, location](QRunnable*, const QString& file) { + q->queryResult(contentQuery, location, file); + }); + connect(runnable, &Baloo::QueryRunnable::finished, q, &BalooContentLister::queryCompleted); + + return runnable; +} diff --git a/src/contentlist/BalooContentLister.h b/src/contentlist/BalooContentLister.h index 214a27e..8210636 100644 --- a/src/contentlist/BalooContentLister.h +++ b/src/contentlist/BalooContentLister.h @@ -1,53 +1,50 @@ /* * 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 BALOOCONTENTLISTER_H #define BALOOCONTENTLISTER_H #include "ContentListerBase.h" #include #include class BalooContentLister : public ContentListerBase { Q_OBJECT public: explicit BalooContentLister(QObject* parent = nullptr); ~BalooContentLister() override; bool balooEnabled() const; - void addLocation(QString path) override; - void addMimetype(QString mimetype) override; - void setSearchString(const QString& searchString) override; - void setKnownFiles(QStringList knownFiles) override; - void startSearch() override; + void startSearch(const QList& queries) override; + private: class Private; Private* d; Q_SLOT void queryCompleted(Baloo::QueryRunnable* query); - Q_SLOT void queryResult(Baloo::QueryRunnable* query, QString file); + void queryResult(const ContentQuery* query, const QString& location, const QString& file); }; #endif//BALOOCONTENTLISTER_H diff --git a/src/contentlist/CMakeLists.txt b/src/contentlist/CMakeLists.txt index cc42555..316d3aa 100644 --- a/src/contentlist/CMakeLists.txt +++ b/src/contentlist/CMakeLists.txt @@ -1,30 +1,32 @@ set(qmlplugin_SRCS qmlplugin.cpp ContentList.cpp ContentListerBase.cpp FilesystemContentLister.cpp + ContentQuery.cpp ) if(KF5Baloo_FOUND) set(qmlplugin_baloo_SRCS BalooContentLister.cpp ) endif() add_library (contentlistqmlplugin SHARED ${qmlplugin_SRCS} ${qmlplugin_baloo_SRCS}) target_link_libraries (contentlistqmlplugin + Qt5::Gui Qt5::Qml KF5::FileMetaData ) if(KF5Baloo_FOUND) target_compile_definitions(contentlistqmlplugin PRIVATE -DBALOO_FOUND="${KF5Baloo_FOUND}") target_link_libraries(contentlistqmlplugin KF5::Baloo ) endif() install (TARGETS contentlistqmlplugin DESTINATION ${QML_INSTALL_DIR}/org/kde/contentlist) install (FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kde/contentlist) diff --git a/src/contentlist/ContentList.cpp b/src/contentlist/ContentList.cpp index ba2c80c..59266ba 100644 --- a/src/contentlist/ContentList.cpp +++ b/src/contentlist/ContentList.cpp @@ -1,155 +1,283 @@ /* * 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 "ContentList.h" #include "FilesystemContentLister.h" #ifdef BALOO_FOUND #include "BalooContentLister.h" #endif #include #include #include +#include struct ContentEntry { QString filename; - QVariantHash metadata; + QUrl filePath; + QVariantMap metadata; }; class ContentList::Private { public: + typedef QQmlListProperty QueryListProperty; + Private() : actualContentList(nullptr) {} QList entries; ContentListerBase* actualContentList; + + QList queries; + QueryListProperty listProperty; + + QSet knownFiles; + + bool autoSearch = false; + bool cacheResults = false; + bool completed = false; + + static void appendToList(QueryListProperty* property, ContentQuery* value); + static ContentQuery* listValueAt(QueryListProperty* property, int index); + static void clearList(QueryListProperty* property); + static int countList(QueryListProperty* property); + + static QStringList cachedFiles; }; +QStringList ContentList::Private::cachedFiles; + ContentList::ContentList(QObject* parent) : QAbstractListModel(parent) , d(new Private) { #ifdef BALOO_FOUND BalooContentLister* baloo = new BalooContentLister(this); if(baloo->balooEnabled()) { d->actualContentList = baloo; - qDebug() << "Baloo support enabled"; } else { baloo->deleteLater(); d->actualContentList = new FilesystemContentLister(this); - qDebug() << "Baloo is disabled for the system, use the filesystem scraper"; } #else d->actualContentList = new FilesystemContentLister(this); #endif - connect(d->actualContentList, SIGNAL(fileFound(QString,QVariantHash)), this, SLOT(fileFound(QString,QVariantHash))); - connect(d->actualContentList, SIGNAL(searchCompleted()), this, SIGNAL(searchCompleted())); + connect(d->actualContentList, &ContentListerBase::fileFound, this, &ContentList::fileFound); + connect(d->actualContentList, &ContentListerBase::searchCompleted, this, &ContentList::searchCompleted); + + d->listProperty = QQmlListProperty{this, &d->queries, + &ContentList::Private::appendToList, + &ContentList::Private::countList, + &ContentList::Private::listValueAt, + &ContentList::Private::clearList + }; } ContentList::~ContentList() { delete d; } -QString ContentList::getMimetype(QString filePath) +QQmlListProperty ContentList::queries() { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForFile(filePath); - return mime.name(); + return d->listProperty; } -void ContentList::addLocation(QString path) +bool ContentList::autoSearch() const { - d->actualContentList->addLocation(path); + return d->autoSearch; } -void ContentList::addMimetype(QString mimetype) +bool ContentList::cacheResults() const { - d->actualContentList->addMimetype(mimetype); + return d->cacheResults; } -void ContentList::setSearchString(const QString& searchString) +QString ContentList::getMimetype(QString filePath) { - d->actualContentList->setSearchString(searchString); + QMimeDatabase db; + QMimeType mime = db.mimeTypeForFile(filePath); + return mime.name(); } void ContentList::startSearch() { - QTimer::singleShot(1, d->actualContentList, SLOT(startSearch())); + QTimer::singleShot(1, [this]() { + d->actualContentList->startSearch(d->queries); + }); } -void ContentList::setKnownFiles(QStringList knownFiles) +void ContentList::fileFound(const QString& filePath, const QVariantMap& metaData) { - d->actualContentList->setKnownFiles(knownFiles); -} + if(d->knownFiles.contains(filePath)) + return; + + auto fileUrl = QUrl::fromLocalFile(filePath); -void ContentList::fileFound(const QString& filePath, const QVariantHash& metadata) -{ ContentEntry* entry = new ContentEntry(); - entry->filename = filePath; - entry->metadata = metadata; + entry->filename = fileUrl.fileName(); + entry->filePath = fileUrl; + entry->metadata = metaData; int newRow = d->entries.count(); beginInsertRows(QModelIndex(), newRow, newRow); d->entries.append(entry); endInsertRows(); + + if(d->cacheResults) + Private::cachedFiles.append(filePath); +} + +void ContentList::setAutoSearch(bool autoSearch) +{ + if(autoSearch == d->autoSearch) + return; + + d->autoSearch = autoSearch; + emit autoSearchChanged(); +} + +void ContentList::setCacheResults(bool cacheResults) +{ + if(cacheResults == d->cacheResults) + return; + + d->cacheResults = cacheResults; + + if(d->cacheResults && d->completed && !Private::cachedFiles.isEmpty()) + { + setKnownFiles(Private::cachedFiles); + } + + emit cacheResultsChanged(); +} + +void ContentList::setKnownFiles(const QStringList& results) +{ + beginResetModel(); + d->entries.clear(); + d->knownFiles.clear(); + for(const auto& result : results) + { + auto entry = new ContentEntry{}; + auto url = QUrl::fromLocalFile(result); + + entry->filename = url.fileName(); + entry->filePath = url; + entry->metadata = ContentListerBase::metaDataForFile(result); + + d->entries.append(entry); + d->knownFiles.insert(result); + } + endResetModel(); } QHash ContentList::roleNames() const { QHash roles; roles[FilenameRole] = "filename"; + roles[FilePathRole] = "filePath"; roles[MetadataRole] = "metadata"; return roles; } QVariant ContentList::data(const QModelIndex& index, int role) const { QVariant result; if(index.isValid() && index.row() > -1 && index.row() < d->entries.count()) { const ContentEntry* entry = d->entries[index.row()]; switch(role) { case FilenameRole: result.setValue(entry->filename); break; + case FilePathRole: + result.setValue(entry->filePath); + break; case MetadataRole: result.setValue(entry->metadata); break; default: - result.setValue(QString("Unknown role")); break; } } return result; } int ContentList::rowCount(const QModelIndex& parent) const { if(parent.isValid()) return 0; return d->entries.count(); } + +void ContentList::classBegin() +{ +} + +void ContentList::componentComplete() +{ + d->completed = true; + + if(d->cacheResults && !Private::cachedFiles.isEmpty()) + setKnownFiles(Private::cachedFiles); + + if(d->autoSearch) + d->actualContentList->startSearch(d->queries); +} + +bool ContentList::isComplete() const +{ + return d->completed; +} + +void ContentList::Private::appendToList(Private::QueryListProperty* property, ContentQuery* value) +{ + auto list = static_cast*>(property->data); + auto model = static_cast(property->object); + list->append(value); + if(model->autoSearch() && model->isComplete()) + model->startSearch(); +} + +ContentQuery* ContentList::Private::listValueAt(Private::QueryListProperty* property, int index) +{ + return static_cast*>(property->data)->at(index); +} + +int ContentList::Private::countList(Private::QueryListProperty* property) +{ + return static_cast*>(property->data)->size(); +} + +void ContentList::Private::clearList(Private::QueryListProperty* property) +{ + auto list = static_cast*>(property->data); + auto model = static_cast(property->object); + model->beginResetModel(); + list->clear(); + model->endResetModel(); +} diff --git a/src/contentlist/ContentList.h b/src/contentlist/ContentList.h index 0dcccca..d401b0a 100644 --- a/src/contentlist/ContentList.h +++ b/src/contentlist/ContentList.h @@ -1,58 +1,80 @@ /* * 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 CONTENTLISTBASE_H #define CONTENTLISTBASE_H #include +#include +#include -class ContentList : public QAbstractListModel +#include "ContentQuery.h" + +class ContentList : public QAbstractListModel, public QQmlParserStatus { Q_OBJECT + Q_CLASSINFO("DefaultProperty", "queries") + Q_PROPERTY(QQmlListProperty queries READ queries) + Q_PROPERTY(bool autoSearch READ autoSearch WRITE setAutoSearch NOTIFY autoSearchChanged) + Q_PROPERTY(bool cacheResults READ cacheResults WRITE setCacheResults NOTIFY cacheResultsChanged) public: explicit ContentList(QObject* parent = nullptr); ~ContentList() override; - Q_INVOKABLE static QString getMimetype(QString filePath); - - Q_SLOT void addLocation(QString path); - Q_SLOT void addMimetype(QString mimetype); - Q_SLOT void setSearchString(const QString& searchString); - Q_SLOT void setKnownFiles(QStringList knownFiles); - Q_SLOT void startSearch(); - enum Roles { FilenameRole = Qt::UserRole + 1, + FilePathRole, MetadataRole }; + QQmlListProperty queries(); + + bool autoSearch() const; + bool cacheResults() const; + QHash roleNames() const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; - Q_SLOT void fileFound(const QString& filePath, const QVariantHash& metaData); + void classBegin() override; + void componentComplete() override; + + Q_SLOT void setAutoSearch(bool autoSearch); + Q_SLOT void setCacheResults(bool cacheResults); + + Q_SLOT void setKnownFiles(const QStringList& results); + Q_SLOT void startSearch(); + + Q_SIGNAL void autoSearchChanged(); + Q_SIGNAL void cacheResultsChanged(); Q_SIGNAL void searchCompleted(); + + Q_INVOKABLE static QString getMimetype(QString filePath); + private: + bool isComplete() const; + Q_SLOT void fileFound(const QString& filePath, const QVariantMap& metaData); + class Private; Private* d; }; #endif//CONTENTLISTBASE_H diff --git a/src/contentlist/ContentListerBase.cpp b/src/contentlist/ContentListerBase.cpp index 18b7d28..b137937 100644 --- a/src/contentlist/ContentListerBase.cpp +++ b/src/contentlist/ContentListerBase.cpp @@ -1,56 +1,69 @@ /* * 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 "ContentListerBase.h" +#include +#include +#include + +#include + ContentListerBase::ContentListerBase(QObject* parent) : QObject(parent) { } ContentListerBase::~ContentListerBase() { } -void ContentListerBase::addLocation(QString path) -{ - Q_UNUSED(path) -} -void ContentListerBase::addMimetype(QString mimetype) +void ContentListerBase::startSearch(const QList& queries) { - Q_UNUSED(mimetype) + Q_UNUSED(queries); } -void ContentListerBase::setSearchString(const QString& searchString) +QVariantMap ContentListerBase::metaDataForFile(const QString& file) { - Q_UNUSED(searchString) -} + QVariantMap metadata; -void ContentListerBase::setKnownFiles(QStringList knownFiles) -{ - Q_UNUSED(knownFiles); -} + //TODO: This should include the same information for both the Baloo and + //File searchers. Unfortunately, currently KFileMetaData does not seem able + //to provide this. So this needs changes at a lower level. -void ContentListerBase::startSearch() -{ -} + QFileInfo info(file); + metadata["lastModified"] = info.lastModified(); + metadata["created"] = info.created(); + metadata["lastRead"] = info.lastRead(); + KFileMetaData::UserMetaData data(file); + if (data.hasAttribute("peruse.currentPage")) { + int currentPage = data.attribute("peruse.currentPage").toInt(); + metadata["currentPage"] = QVariant::fromValue(currentPage); + } + if (data.hasAttribute("peruse.totalPages")) { + int totalPages = data.attribute("peruse.totalPages").toInt(); + metadata["totalPages"] = QVariant::fromValue(totalPages); + } + + return metadata; +} diff --git a/src/contentlist/ContentListerBase.h b/src/contentlist/ContentListerBase.h index 41fbc73..e795f7f 100644 --- a/src/contentlist/ContentListerBase.h +++ b/src/contentlist/ContentListerBase.h @@ -1,44 +1,44 @@ /* * 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 CONTENTLISTERBASE_H #define CONTENTLISTERBASE_H #include +class ContentQuery; + class ContentListerBase : public QObject { Q_OBJECT public: explicit ContentListerBase(QObject* parent = nullptr); ~ContentListerBase() override; - virtual void addLocation(QString path); - virtual void addMimetype(QString mimetype); - virtual void setSearchString(const QString& searchString); - virtual void setKnownFiles(QStringList knownFiles); - Q_SLOT virtual void startSearch(); + Q_SLOT virtual void startSearch(const QList& queries); - Q_SIGNAL void fileFound(const QString& filePath, const QVariantHash& metadata); + Q_SIGNAL void fileFound(const QString& filePath, const QVariantMap& metadata); Q_SIGNAL void searchCompleted(); + + static QVariantMap metaDataForFile(const QString& file); }; #endif//CONTENTLISTERBASE_H diff --git a/src/contentlist/ContentQuery.cpp b/src/contentlist/ContentQuery.cpp new file mode 100644 index 0000000..853c7e9 --- /dev/null +++ b/src/contentlist/ContentQuery.cpp @@ -0,0 +1,175 @@ +/* + * Copyright 2018 Arjen Hiemstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 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 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ContentQuery.h" + +#include +#include + +class ContentQuery::Private +{ +public: + QStringList mimeTypesForType(Type type); + + Type type = Any; + QString searchString; + QStringList locations; + QStringList mimeTypes; +}; + +ContentQuery::ContentQuery(QObject* parent) + : QObject(parent), d(new Private) +{ +} + +ContentQuery::~ContentQuery() +{ +} + +ContentQuery::Type ContentQuery::type() const +{ + return d->type; +} + +QString ContentQuery::searchString() const +{ + return d->searchString; +} + +QStringList ContentQuery::locations() const +{ + return d->locations; +} + +QStringList ContentQuery::mimeTypes() const +{ + if(!d->mimeTypes.isEmpty()) + return d->mimeTypes; + + return d->mimeTypesForType(d->type); +} + +void ContentQuery::setType(ContentQuery::Type type) +{ + if(type == d->type) + return; + + d->type = type; + emit typeChanged(); +} + +void ContentQuery::setSearchString(const QString& searchString) +{ + if(searchString == d->searchString) + return; + + d->searchString = searchString; + emit searchStringChanged(); +} + +void ContentQuery::setLocations(const QStringList& locations) +{ + if(locations == d->locations) + return; + + d->locations = locations; + emit locationsChanged(); +} + +void ContentQuery::setMimeTypes(const QStringList& mimeTypes) +{ + if(mimeTypes == d->mimeTypes) + return; + + d->mimeTypes = mimeTypes; + emit mimeTypesChanged(); +} + +QStringList ContentQuery::Private::mimeTypesForType(ContentQuery::Type type) +{ + QStringList result; + QMimeDatabase mimeDatabase; + + //TODO: Find a better solution for this. + switch(type) { + case ContentQuery::Video: + result << QStringLiteral("video/x-matroska"); + result << QStringLiteral("video/mp4"); + result << QStringLiteral("video/mpeg"); + result << QStringLiteral("video/ogg"); + result << QStringLiteral("video/quicktime"); + result << QStringLiteral("video/webm"); + result << QStringLiteral("video/x-ms-wmv"); + result << QStringLiteral("video/x-msvideo"); + result << QStringLiteral("video/x-ogm+ogg"); + result << QStringLiteral("video/x-theora+ogg"); + break; + case ContentQuery::Audio: + result << QStringLiteral("audio/aac"); + result << QStringLiteral("audio/flac"); + result << QStringLiteral("audio/mp2"); + result << QStringLiteral("audio/mp4"); + result << QStringLiteral("audio/mpeg"); + result << QStringLiteral("audio/ogg"); + result << QStringLiteral("audio/webm"); + result << QStringLiteral("audio/x-opus+ogg"); + result << QStringLiteral("audio/x-ms-wma"); + result << QStringLiteral("audio/x-vorbis+ogg"); + result << QStringLiteral("audio/x-wav"); + break; + case ContentQuery::Documents: + result << QStringLiteral("application/vnd.oasis.opendocument.text"); + result << QStringLiteral("application/vnd.oasis.opendocument.spreadsheet"); + result << QStringLiteral("application/vnd.oasis.opendocument.presentation"); + result << QStringLiteral("application/vnd.ms-word"); + result << QStringLiteral("application/vnd.ms-excel"); + result << QStringLiteral("application/vnd.ms-powerpoint"); + result << QStringLiteral("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.xml"); + result << QStringLiteral("application/vnd.openxmlformats-officedocument.wordprocessingml.document.xml"); + result << QStringLiteral("application/vnd.openxmlformats-officedocument.presentationml.presentation.xml"); + result << QStringLiteral("text/plain"); + result << QStringLiteral("application/pdf"); + break; + case ContentQuery::Images: + for(const auto& item : QImageReader::supportedMimeTypes()) + { + result << QString::fromUtf8(item); + } + break; + case ContentQuery::Comics: + result << QStringLiteral("application/x-cbz"); + result << QStringLiteral("application/x-cbr"); + result << QStringLiteral("application/x-cb7"); + result << QStringLiteral("application/x-cbt"); + result << QStringLiteral("application/x-cba"); + result << QStringLiteral("application/vnd.comicbook+zip"); + result << QStringLiteral("application/vnd.comicbook+rar"); + result << QStringLiteral("application/vnd.ms-htmlhelp"); + result << QStringLiteral("image/vnd.djvu"); + result << QStringLiteral("image/x-djvu"); + result << QStringLiteral("application/epub+zip"); + result << QStringLiteral("application/pdf"); + break; + default: + break; + } + + return result; +} diff --git a/src/contentlist/ContentQuery.h b/src/contentlist/ContentQuery.h new file mode 100644 index 0000000..5a452cf --- /dev/null +++ b/src/contentlist/ContentQuery.h @@ -0,0 +1,148 @@ +/* + * Copyright 2018 Arjen Hiemstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 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 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CONTENTQUERY_H +#define CONTENTQUERY_H + +#include + +#include + +/** + * Encapsulates searching parameters for files on the file system. + * + * + */ +class ContentQuery : public QObject +{ + Q_OBJECT + /** + * The type of files to search for. + */ + Q_PROPERTY(Type type READ type WRITE setType NOTIFY typeChanged) + /** + * A string that should be included in the file's file name. + */ + Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY searchStringChanged) + /** + * A list of directories. Only these directories and their subdirectories will be searched. + */ + Q_PROPERTY(QStringList locations READ locations WRITE setLocations NOTIFY locationsChanged) + /** + * A list of mime type names to search for. + * + * Note that if this property has not explicitly been set, the list of mime types + * is based on the type property. + */ + Q_PROPERTY(QStringList mimeTypes READ mimeTypes WRITE setMimeTypes NOTIFY mimeTypesChanged) + +public: + /** + * The type of files to search for. + */ + enum Type { + Any, ///< Do not limit results by any type. + Video, ///< Only search for videos. + Audio, ///< Only search for audio files. + Documents, ///< Only search for documents. + Images, ///< Only search for images. + Comics, ///< Only search for comic books. + }; + Q_ENUM(Type) + + /** + * Constructor + * + * @param parent The QObject parent. + */ + explicit ContentQuery(QObject* parent = nullptr); + + /** + * Destructor + */ + ~ContentQuery(); + + /** + * Get the type property. + */ + Type type() const; + /** + * Get the searchString property. + */ + QString searchString() const; + /** + * Get the locations property. + */ + QStringList locations() const; + /** + * Get the mimeTypes property. + */ + QStringList mimeTypes() const; + +public Q_SLOTS: + /** + * Set the type property. + * + * \param type The new type. + */ + void setType(Type type); + /** + * Set the searchString property. + * + * \param searchString The new search string. + */ + void setSearchString(const QString& searchString); + /** + * Set the location property. + * + * \param location The new location. + */ + void setLocations(const QStringList& location); + /** + * Set the mimeTypes property. + * + * \param mimeTypes The new list of mime types. + */ + void setMimeTypes(const QStringList& mimeTypes); + +Q_SIGNALS: + /** + * Emitted whenever the type property changes. + */ + void typeChanged(); + /** + * Emitted whenever the searchString property changes. + */ + void searchStringChanged(); + /** + * Emitted whenever the location property changes. + */ + void locationsChanged(); + /** + * Emitted whenever the mimeTypes property changes. + */ + void mimeTypesChanged(); + +private: + class Private; + const std::unique_ptr d; +}; + +#endif // CONTENTQUERY_H diff --git a/src/contentlist/FilesystemContentLister.cpp b/src/contentlist/FilesystemContentLister.cpp index f6a3f4e..acbb538 100644 --- a/src/contentlist/FilesystemContentLister.cpp +++ b/src/contentlist/FilesystemContentLister.cpp @@ -1,137 +1,134 @@ /* * 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 "FilesystemContentLister.h" #include #include #include #include #include #include #include #include +#include +#include + +#include "ContentQuery.h" + +class FileSystemSearcher : public QObject, public QRunnable +{ + Q_OBJECT +public: + FileSystemSearcher(ContentQuery* query) : QObject() { m_query = query; } + + void run() override + { + QMimeDatabase mimeDb; + + auto locations = m_query->locations(); + if(locations.isEmpty()) + locations.append(QDir::homePath()); + + for(const auto& location : locations) + { + QDirIterator it(location, QDirIterator::Subdirectories); + while (it.hasNext()) + { + auto filePath = it.next(); + + if(it.fileInfo().isDir()) + continue; + + QString mimeType = mimeDb.mimeTypeForFile(filePath).name(); + if(!m_query->mimeTypes().isEmpty() && !m_query->mimeTypes().contains(mimeType)) + continue; + + auto metadata = ContentListerBase::metaDataForFile(filePath); + + emit fileFound(filePath, metadata); + } + } + + emit finished(this); + } + +Q_SIGNALS: + void fileFound(const QString& path, const QVariantMap& metaData); + void finished(FileSystemSearcher* searcher); + +private: + ContentQuery* m_query; +}; class FilesystemContentLister::Private { public: - Private() {} - QString searchString; - QStringList knownFiles; - QStringList locations; - QStringList mimetypes; + Private() { } + + QList runnables; }; FilesystemContentLister::FilesystemContentLister(QObject* parent) : ContentListerBase(parent) , d(new Private) { + } FilesystemContentLister::~FilesystemContentLister() { + QThreadPool::globalInstance()->waitForDone(); delete d; } -void FilesystemContentLister::addLocation(QString path) -{ - d->locations.append(path); -} - -void FilesystemContentLister::addMimetype(QString mimetype) +void FilesystemContentLister::startSearch(const QList& queries) { - d->mimetypes.append(mimetype); -} + for(const auto& query : queries) + { + auto runnable = new FileSystemSearcher{query}; + connect(runnable, &FileSystemSearcher::fileFound, this, &FilesystemContentLister::fileFound); + connect(runnable, &FileSystemSearcher::finished, this, &FilesystemContentLister::queryFinished); -void FilesystemContentLister::setSearchString(const QString& searchString) -{ - d->searchString = searchString; -} + d->runnables.append(runnable); + } -void FilesystemContentLister::setKnownFiles(QStringList knownFiles) -{ - d->knownFiles = knownFiles; + if(!d->runnables.isEmpty()) + QThreadPool::globalInstance()->start(d->runnables.first()); } -void FilesystemContentLister::startSearch() +void FilesystemContentLister::queryFinished(QRunnable* runnable) { - QMimeDatabase mimeDb; - bool useThis(false); + d->runnables.removeAll(static_cast(runnable)); - qDebug() << "Searching in" << d->locations; - Q_FOREACH(const QString& folder, d->locations) + if(!d->runnables.isEmpty()) { - QDirIterator it(folder, QDirIterator::Subdirectories); - while (it.hasNext()) - { - QString filePath = it.next(); - if(d->knownFiles.contains(filePath)) { - continue; - } - - QFileInfo info(filePath); - - if(info.isDir()) - { - qApp->processEvents(); - continue; - } - useThis = false; - QString mimetype = mimeDb.mimeTypeForFile(filePath, QMimeDatabase::MatchExtension).name(); -// qDebug() << useThis << mimetype << filePath; - Q_FOREACH(const QString& type, d->mimetypes) - { - if(type == mimetype) { - useThis = true; - break; - } - } - - if(useThis) - { - QVariantHash metadata; - metadata["created"] = info.created(); - - KFileMetaData::UserMetaData data(filePath); - if (data.hasAttribute("peruse.currentPage")) { - int currentPage = data.attribute("peruse.currentPage").toInt(); - metadata["currentPage"] = QVariant::fromValue(currentPage); - } - if (data.hasAttribute("peruse.totalPages")) { - int totalPages = data.attribute("peruse.totalPages").toInt(); - metadata["totalPages"] = QVariant::fromValue(totalPages); - } - - emit fileFound(filePath, metadata); - } - qApp->processEvents(); - } + QThreadPool::globalInstance()->start(d->runnables.first()); + } + else + { + emit searchCompleted(); } - // This ensures that, should we decide to search more stuff later, we can do so granularly - d->locations.clear(); - - // Not entirely happy about this, but it makes things not break... - // Previously, the welcome page in Peruse would end up unpopulated because a signal - // was unreceived from the main window upon search completion (and consequently - // application readiness) - QTimer::singleShot(0, this, SIGNAL(searchCompleted())); } + +// This needs to be included since we define a QObject subclass here in the C++ file. +#include "FilesystemContentLister.moc" diff --git a/src/contentlist/FilesystemContentLister.h b/src/contentlist/FilesystemContentLister.h index 8dd4365..a2abd97 100644 --- a/src/contentlist/FilesystemContentLister.h +++ b/src/contentlist/FilesystemContentLister.h @@ -1,44 +1,45 @@ /* * 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 FILESYSTEMCONTENTLISTER_H #define FILESYSTEMCONTENTLISTER_H +#include + #include "ContentListerBase.h" class FilesystemContentLister : public ContentListerBase { Q_OBJECT public: explicit FilesystemContentLister(QObject* parent = nullptr); ~FilesystemContentLister() override; - void addLocation(QString path) override; - void addMimetype(QString mimetype) override; - void setSearchString(const QString& searchString) override; - void setKnownFiles(QStringList knownFiles) override; - void startSearch() override; + void startSearch(const QList& queries) override; + private: + void queryFinished(QRunnable* runnable); + class Private; Private* d; }; #endif//FILESYSTEMCONTENTLISTER_H diff --git a/src/contentlist/qmlplugin.cpp b/src/contentlist/qmlplugin.cpp index 07166be..25c385a 100644 --- a/src/contentlist/qmlplugin.cpp +++ b/src/contentlist/qmlplugin.cpp @@ -1,35 +1,37 @@ /* * 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 "qmlplugin.h" -#include "ContentList.h" - #include +#include "ContentList.h" +#include "ContentQuery.h" + void QmlPlugins::initializeEngine(QQmlEngine *, const char *) { } void QmlPlugins::registerTypes(const char *uri) { qmlRegisterType(uri, 0, 1, "ContentList"); + qmlRegisterType(uri, 0, 1, "ContentQuery"); } diff --git a/src/qtquick/BookListModel.cpp b/src/qtquick/BookListModel.cpp index 8256aac..68fe570 100644 --- a/src/qtquick/BookListModel.cpp +++ b/src/qtquick/BookListModel.cpp @@ -1,345 +1,346 @@ /* * 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 #include #include #include #include #include #include class BookListModel::Private { public: Private() : contentModel(nullptr) , titleCategoryModel(nullptr) , newlyAddedCategoryModel(nullptr) , authorCategoryModel(nullptr) , seriesCategoryModel(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* 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(!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); authorCategoryModel->addCategoryEntry(entry->author, entry); seriesCategoryModel->addCategoryEntry(entry->series, entry); newlyAddedCategoryModel->append(entry, CreatedRole); QUrl url(entry->filename.left(entry->filename.lastIndexOf("/"))); folderCategoryModel->addCategoryEntry(url.path().mid(1), entry); 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 filename = d->contentModel->data(d->contentModel->index(first, 0, index), Qt::UserRole + 1); + QVariant filePath = d->contentModel->data(d->contentModel->index(first, 0, index), role); BookEntry* entry = new BookEntry(); - entry->filename = filename.toString(); + entry->filename = filePath.toUrl().toLocalFile(); QStringList splitName = entry->filename.split("/"); if (!splitName.isEmpty()) entry->filetitle = splitName.takeLast(); if(!splitName.isEmpty()) entry->series = splitName.takeLast(); // hahahaheuristics (dumb assumptions about filesystems, go!) // 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); } 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().toString().trimmed(); } 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(); } } // 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 = sequence->title(); break; } } // TODO extend the model to support multiple authors per book, ditto series/sequences entry->author = 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::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(); } 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; }