diff --git a/src/app/qml/PeruseMain.qml b/src/app/qml/PeruseMain.qml --- a/src/app/qml/PeruseMain.qml +++ b/src/app/qml/PeruseMain.qml @@ -54,35 +54,24 @@ 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() } } diff --git a/src/contentlist/BalooContentLister.h b/src/contentlist/BalooContentLister.h --- a/src/contentlist/BalooContentLister.h +++ b/src/contentlist/BalooContentLister.h @@ -37,17 +37,14 @@ 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/BalooContentLister.cpp b/src/contentlist/BalooContentLister.cpp --- a/src/contentlist/BalooContentLister.cpp +++ b/src/contentlist/BalooContentLister.cpp @@ -32,26 +32,37 @@ #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; } @@ -70,7 +81,6 @@ 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; @@ -80,46 +90,18 @@ 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()) { @@ -130,7 +112,6 @@ void BalooContentLister::queryCompleted(Baloo::QueryRunnable* query) { d->queries.removeAll(query); - d->queryLocations.takeFirst(); if(d->queries.empty()) { emit searchCompleted(); @@ -141,20 +122,30 @@ } } -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(); @@ -164,19 +155,43 @@ { 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/CMakeLists.txt b/src/contentlist/CMakeLists.txt --- a/src/contentlist/CMakeLists.txt +++ b/src/contentlist/CMakeLists.txt @@ -3,6 +3,7 @@ ContentList.cpp ContentListerBase.cpp FilesystemContentLister.cpp + ContentQuery.cpp ) @@ -15,6 +16,7 @@ add_library (contentlistqmlplugin SHARED ${qmlplugin_SRCS} ${qmlplugin_baloo_SRCS}) target_link_libraries (contentlistqmlplugin + Qt5::Gui Qt5::Qml KF5::FileMetaData ) diff --git a/src/contentlist/ContentList.h b/src/contentlist/ContentList.h --- a/src/contentlist/ContentList.h +++ b/src/contentlist/ContentList.h @@ -23,34 +23,56 @@ #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; }; diff --git a/src/contentlist/ContentList.cpp b/src/contentlist/ContentList.cpp --- a/src/contentlist/ContentList.cpp +++ b/src/contentlist/ContentList.cpp @@ -29,21 +29,43 @@ #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) @@ -53,74 +75,130 @@ 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; } @@ -136,11 +214,13 @@ 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; } } @@ -153,3 +233,51 @@ 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/ContentListerBase.h b/src/contentlist/ContentListerBase.h --- a/src/contentlist/ContentListerBase.h +++ b/src/contentlist/ContentListerBase.h @@ -24,21 +24,21 @@ #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/ContentListerBase.cpp b/src/contentlist/ContentListerBase.cpp --- a/src/contentlist/ContentListerBase.cpp +++ b/src/contentlist/ContentListerBase.cpp @@ -21,6 +21,12 @@ #include "ContentListerBase.h" +#include +#include +#include + +#include + ContentListerBase::ContentListerBase(QObject* parent) : QObject(parent) { @@ -30,27 +36,34 @@ { } -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/ContentQuery.h b/src/contentlist/ContentQuery.h new file mode 100644 --- /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/ContentQuery.cpp b/src/contentlist/ContentQuery.cpp new file mode 100644 --- /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/FilesystemContentLister.h b/src/contentlist/FilesystemContentLister.h --- a/src/contentlist/FilesystemContentLister.h +++ b/src/contentlist/FilesystemContentLister.h @@ -22,6 +22,8 @@ #ifndef FILESYSTEMCONTENTLISTER_H #define FILESYSTEMCONTENTLISTER_H +#include + #include "ContentListerBase.h" class FilesystemContentLister : public ContentListerBase @@ -31,12 +33,11 @@ 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; }; diff --git a/src/contentlist/FilesystemContentLister.cpp b/src/contentlist/FilesystemContentLister.cpp --- a/src/contentlist/FilesystemContentLister.cpp +++ b/src/contentlist/FilesystemContentLister.cpp @@ -30,108 +30,105 @@ #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/qmlplugin.cpp b/src/contentlist/qmlplugin.cpp --- a/src/contentlist/qmlplugin.cpp +++ b/src/contentlist/qmlplugin.cpp @@ -21,15 +21,17 @@ #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 --- a/src/qtquick/BookListModel.cpp +++ b/src/qtquick/BookListModel.cpp @@ -182,11 +182,12 @@ 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();