diff --git a/src/attica/atticaprovider.cpp b/src/attica/atticaprovider.cpp index 42798d1c..cd334efc 100644 --- a/src/attica/atticaprovider.cpp +++ b/src/attica/atticaprovider.cpp @@ -1,534 +1,611 @@ /* Copyright (c) 2009-2010 Frederik Gladhorn 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) any later version. 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 "atticaprovider_p.h" +#include "commentsmodel.h" #include "question.h" #include "tagsfilterchecker.h" #include #include #include #include #include #include #include #include #include #include using namespace Attica; namespace KNSCore { AtticaProvider::AtticaProvider(const QStringList &categories) : mEntryJob(nullptr) , mInitialized(false) { // init categories map with invalid categories for (const QString &category : categories) { mCategoryMap.insert(category, Attica::Category()); } connect(&m_providerManager, &ProviderManager::providerAdded, this, &AtticaProvider::providerLoaded); connect(&m_providerManager, SIGNAL(authenticationCredentialsMissing(Provider)), SLOT(authenticationCredentialsMissing(Provider))); + connect(this, &Provider::loadComments, this, &AtticaProvider::loadComments); + connect(this, &Provider::loadPerson, this, &AtticaProvider::loadPerson); } AtticaProvider::AtticaProvider(const Attica::Provider &provider, const QStringList &categories) : mEntryJob(nullptr) , mInitialized(false) { // init categories map with invalid categories for (const QString &category : categories) { mCategoryMap.insert(category, Attica::Category()); } providerLoaded(provider); } QString AtticaProvider::id() const { return m_providerId; } void AtticaProvider::authenticationCredentialsMissing(const KNSCore::Provider &) { qCDebug(KNEWSTUFFCORE) << "Authentication missing!"; // FIXME Show autentication dialog } bool AtticaProvider::setProviderXML(const QDomElement &xmldata) { if (xmldata.tagName() != QLatin1String("provider")) { return false; } // FIXME this is quite ugly, repackaging the xml into a string QDomDocument doc(QStringLiteral("temp")); qCDebug(KNEWSTUFFCORE) << "setting provider xml" << doc.toString(); doc.appendChild(xmldata.cloneNode(true)); m_providerManager.addProviderFromXml(doc.toString()); if (!m_providerManager.providers().isEmpty()) { qCDebug(KNEWSTUFFCORE) << "base url of attica provider:" << m_providerManager.providers().constLast().baseUrl().toString(); } else { qCCritical(KNEWSTUFFCORE) << "Could not load provider."; return false; } return true; } void AtticaProvider::setCachedEntries(const KNSCore::EntryInternal::List &cachedEntries) { mCachedEntries = cachedEntries; } void AtticaProvider::providerLoaded(const Attica::Provider &provider) { mName = provider.name(); qCDebug(KNEWSTUFFCORE) << "Added provider: " << provider.name(); m_provider = provider; m_providerId = provider.baseUrl().toString(); Attica::ListJob *job = m_provider.requestCategories(); connect(job, &BaseJob::finished, this, &AtticaProvider::listOfCategoriesLoaded); job->start(); } void AtticaProvider::listOfCategoriesLoaded(Attica::BaseJob *listJob) { if (!jobSuccess(listJob)) { return; } qCDebug(KNEWSTUFFCORE) << "loading categories: " << mCategoryMap.keys(); Attica::ListJob *job = static_cast*>(listJob); const Category::List categoryList = job->itemList(); QList categoryMetadataList; for (const Category &category : categoryList) { if (mCategoryMap.contains(category.name())) { qCDebug(KNEWSTUFFCORE) << "Adding category: " << category.name() << category.displayName(); //If there is only the placeholder category, replace it if (mCategoryMap.contains(category.name()) && !mCategoryMap.value(category.name()).isValid()) { mCategoryMap.insert(category.name(), category); } else { mCategoryMap.insertMulti(category.name(), category); } CategoryMetadata categoryMetadata; categoryMetadata.id = category.id(); categoryMetadata.name = category.name(); categoryMetadata.displayName = category.displayName(); categoryMetadataList << categoryMetadata; } } std::sort(categoryMetadataList.begin(), categoryMetadataList.end(), [](const AtticaProvider::CategoryMetadata &i, const AtticaProvider::CategoryMetadata &j) -> bool { const QString a(i.displayName.isEmpty() ? i.name : i.displayName); const QString b(j.displayName.isEmpty() ? j.name : j.displayName); return (QCollator().compare(a, b) < 0); }); bool correct = false; for(auto it = mCategoryMap.cbegin(), itEnd = mCategoryMap.cend(); it!=itEnd; ++it) { if (!it.value().isValid()) { qCWarning(KNEWSTUFFCORE) << "Could not find category" << it.key(); } else { correct = true; } } if (correct) { mInitialized = true; emit providerInitialized(this); emit categoriesMetadataLoded(categoryMetadataList); } else { emit signalErrorCode(KNSCore::ConfigFileError, i18n("All categories are missing"), QVariant()); } } bool AtticaProvider::isInitialized() const { return mInitialized; } void AtticaProvider::loadEntries(const KNSCore::Provider::SearchRequest &request) { if (mEntryJob) { mEntryJob->abort(); mEntryJob = nullptr; } mCurrentRequest = request; switch (request.filter) { case None: break; case ExactEntryId: { ItemJob *job = m_provider.requestContent(request.searchTerm); connect(job, &BaseJob::finished, this, &AtticaProvider::detailsLoaded); job->start(); return; } case Installed: if (request.page == 0) { emit loadingFinished(request, installedEntries()); } else { emit loadingFinished(request, EntryInternal::List()); } return; case Updates: checkForUpdates(); return; } Attica::Provider::SortMode sorting = atticaSortMode(request.sortMode); Attica::Category::List categoriesToSearch; if (request.categories.isEmpty()) { // search in all categories categoriesToSearch = mCategoryMap.values(); } else { categoriesToSearch.reserve(request.categories.size()); for (const QString &categoryName : qAsConst(request.categories)) { categoriesToSearch.append(mCategoryMap.values(categoryName)); } } ListJob *job = m_provider.searchContents(categoriesToSearch, request.searchTerm, sorting, request.page, request.pageSize); connect(job, &BaseJob::finished, this, &AtticaProvider::categoryContentsLoaded); mEntryJob = job; job->start(); } void AtticaProvider::checkForUpdates() { for (const EntryInternal &e : qAsConst(mCachedEntries)) { ItemJob *job = m_provider.requestContent(e.uniqueId()); connect(job, &BaseJob::finished, this, &AtticaProvider::detailsLoaded); m_updateJobs.insert(job); job->start(); qCDebug(KNEWSTUFFCORE) << "Checking for update: " << e.name(); } } void AtticaProvider::loadEntryDetails(const KNSCore::EntryInternal &entry) { ItemJob *job = m_provider.requestContent(entry.uniqueId()); connect(job, &BaseJob::finished, this, &AtticaProvider::detailsLoaded); job->start(); } void AtticaProvider::detailsLoaded(BaseJob *job) { if (jobSuccess(job)) { ItemJob *contentJob = static_cast*>(job); Content content = contentJob->result(); EntryInternal entry = entryFromAtticaContent(content); emit entryDetailsLoaded(entry); qCDebug(KNEWSTUFFCORE) << "check update finished: " << entry.name(); } if (m_updateJobs.remove(job) && m_updateJobs.isEmpty()) { qCDebug(KNEWSTUFFCORE) << "check update finished."; QList updatable; for (const EntryInternal &entry : qAsConst(mCachedEntries)) { if (entry.status() == KNS3::Entry::Updateable) { updatable.append(entry); } } emit loadingFinished(mCurrentRequest, updatable); } } void AtticaProvider::categoryContentsLoaded(BaseJob *job) { if (!jobSuccess(job)) { return; } ListJob *listJob = static_cast*>(job); Content::List contents = listJob->itemList(); EntryInternal::List entries; TagsFilterChecker checker(tagFilter()); TagsFilterChecker downloadschecker(downloadTagFilter()); for (const Content &content : contents) { if (!content.isValid()) { qCDebug(KNEWSTUFFCORE) << "Filtered out an invalid entry. This suggests something is not right on the originating server. Please contact the administrators of" << name() << "and inform them there is an issue with content in the category or categories" << mCurrentRequest.categories; continue; } if (checker.filterAccepts(content.tags())) { bool filterAcceptsDownloads = true; if (content.downloads() > 0) { filterAcceptsDownloads = false; for (const Attica::DownloadDescription &dli : content.downloadUrlDescriptions()) { if (downloadschecker.filterAccepts(dli.tags())) { filterAcceptsDownloads = true; break; } } } if (filterAcceptsDownloads) { mCachedContent.insert(content.id(), content); entries.append(entryFromAtticaContent(content)); } else { qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << content.name() << "on download filter" << downloadTagFilter(); } } else { qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << content.name() << "on entry filter" << tagFilter(); } } qCDebug(KNEWSTUFFCORE) << "loaded: " << mCurrentRequest.hashForRequest() << " count: " << entries.size(); emit loadingFinished(mCurrentRequest, entries); mEntryJob = nullptr; } Attica::Provider::SortMode AtticaProvider::atticaSortMode(const SortMode &sortMode) { switch(sortMode) { case Newest: return Attica::Provider::Newest; case Alphabetical: return Attica::Provider::Alphabetical; case Downloads: return Attica::Provider::Downloads; default: return Attica::Provider::Rating; } } void AtticaProvider::loadPayloadLink(const KNSCore::EntryInternal &entry, int linkId) { Attica::Content content = mCachedContent.value(entry.uniqueId()); const DownloadDescription desc = content.downloadUrlDescription(linkId); if (desc.hasPrice()) { // Ask for balance, then show information... ItemJob *job = m_provider.requestAccountBalance(); connect(job, &BaseJob::finished, this, &AtticaProvider::accountBalanceLoaded); mDownloadLinkJobs[job] = qMakePair(entry, linkId); job->start(); qCDebug(KNEWSTUFFCORE) << "get account balance"; } else { ItemJob *job = m_provider.downloadLink(entry.uniqueId(), QString::number(linkId)); connect(job, &BaseJob::finished, this, &AtticaProvider::downloadItemLoaded); mDownloadLinkJobs[job] = qMakePair(entry, linkId); job->start(); qCDebug(KNEWSTUFFCORE) << " link for " << entry.uniqueId(); } } +void AtticaProvider::loadComments(const EntryInternal &entry, int commentsPerPage, int page) +{ + ListJob *job = m_provider.requestComments(Attica::Comment::ContentComment, entry.uniqueId(), QLatin1String("0"), page, commentsPerPage); + connect(job, &BaseJob::finished, this, &AtticaProvider::loadedComments); + job->start(); +} + +/// TODO KF6 QList is discouraged, and we'll probably want to switch this (and the rest of the KNS library) to QVector instead +QList> getCommentsList(const Attica::Comment::List &comments, std::shared_ptr parent) { + QList> knsComments; + for (const Attica::Comment &comment : comments) { + qCDebug(KNEWSTUFFCORE) << "Appending comment with id" << comment.id() << ", which has" << comment.childCount() << "children"; + auto knsComment = std::make_shared(); + knsComment->id = comment.id(); + knsComment->subject = comment.subject(); + knsComment->text = comment.text(); + knsComment->childCount = comment.childCount(); + knsComment->username = comment.user(); + knsComment->date = comment.date(); + knsComment->score = comment.score(); + knsComment->parent = parent; + knsComments << knsComment; + if (comment.childCount() > 0) { + qCDebug(KNEWSTUFFCORE) << "Getting more comments, as this one has children, and we currently have this number of comments:" << knsComments.count(); + knsComments << getCommentsList(comment.children(), knsComment); + qCDebug(KNEWSTUFFCORE) << "After getting the children, we now have the following number of comments:" << knsComments.count(); + } + } + return knsComments; +} + +void AtticaProvider::loadedComments(Attica::BaseJob *baseJob) +{ + if (!jobSuccess(baseJob)) { + return; + } + + ListJob *job = static_cast*>(baseJob); + Attica::Comment::List comments = job->itemList(); + + QList> receivedComments = getCommentsList(comments, nullptr); + emit commentsLoaded(receivedComments); +} + +void AtticaProvider::loadPerson(const QString &username) +{ + if (m_provider.hasPersonService()) { + ItemJob *job = m_provider.requestPerson(username); + job->setProperty("username", username); + connect(job, &BaseJob::finished, this, &AtticaProvider::loadedPerson); + job->start(); + } +} + +void AtticaProvider::loadedPerson(Attica::BaseJob *baseJob) +{ + if (!jobSuccess(baseJob)) { + return; + } + + ItemJob *job = static_cast*>(baseJob); + Attica::Person person = job->result(); + + auto author = std::make_shared(); + author->setId(job->property("username").toString()); // This is a touch hack-like, but it ensures we actually have the data in case it is not returned by the server + author->setName(QString::fromLatin1("%1 %2").arg(person.firstName()).arg(person.lastName()).trimmed()); + author->setHomepage(person.homepage()); + author->setProfilepage(person.extendedAttribute(QStringLiteral("profilepage"))); + author->setAvatarUrl(person.avatarUrl()); + author->setDescription(person.extendedAttribute(QStringLiteral("description"))); + emit personLoaded(author); +} + void AtticaProvider::accountBalanceLoaded(Attica::BaseJob *baseJob) { if (!jobSuccess(baseJob)) { return; } ItemJob *job = static_cast*>(baseJob); AccountBalance item = job->result(); QPair pair = mDownloadLinkJobs.take(job); EntryInternal entry(pair.first); Content content = mCachedContent.value(entry.uniqueId()); if (content.downloadUrlDescription(pair.second).priceAmount() < item.balance()) { qCDebug(KNEWSTUFFCORE) << "Your balance is greater than the price." << content.downloadUrlDescription(pair.second).priceAmount() << " balance: " << item.balance(); Question question; question.setQuestion(i18nc("the price of a download item, parameter 1 is the currency, 2 is the price", "This item costs %1 %2.\nDo you want to buy it?", item.currency(), content.downloadUrlDescription(pair.second).priceAmount() )); if(question.ask() == Question::YesResponse) { ItemJob *job = m_provider.downloadLink(entry.uniqueId(), QString::number(pair.second)); connect(job, &BaseJob::finished, this, &AtticaProvider::downloadItemLoaded); mDownloadLinkJobs[job] = qMakePair(entry, pair.second); job->start(); } else { return; } } else { qCDebug(KNEWSTUFFCORE) << "You don't have enough money on your account!" << content.downloadUrlDescription(0).priceAmount() << " balance: " << item.balance(); emit signalInformation(i18n("Your account balance is too low:\nYour balance: %1\nPrice: %2", item.balance(), content.downloadUrlDescription(0).priceAmount())); } } void AtticaProvider::downloadItemLoaded(BaseJob *baseJob) { if (!jobSuccess(baseJob)) { return; } ItemJob *job = static_cast*>(baseJob); DownloadItem item = job->result(); EntryInternal entry = mDownloadLinkJobs.take(job).first; entry.setPayload(QString(item.url().toString())); emit payloadLinkLoaded(entry); } EntryInternal::List AtticaProvider::installedEntries() const { EntryInternal::List entries; for (const EntryInternal &entry : qAsConst(mCachedEntries)) { if (entry.status() == KNS3::Entry::Installed || entry.status() == KNS3::Entry::Updateable) { entries.append(entry); } } return entries; } void AtticaProvider::vote(const EntryInternal &entry, uint rating) { PostJob *job = m_provider.voteForContent(entry.uniqueId(), rating); connect(job, &BaseJob::finished, this, &AtticaProvider::votingFinished); job->start(); } void AtticaProvider::votingFinished(Attica::BaseJob *job) { if (!jobSuccess(job)) { return; } emit signalInformation(i18nc("voting for an item (good/bad)", "Your vote was recorded.")); } void AtticaProvider::becomeFan(const EntryInternal &entry) { PostJob *job = m_provider.becomeFan(entry.uniqueId()); connect(job, &BaseJob::finished, this, &AtticaProvider::becomeFanFinished); job->start(); } void AtticaProvider::becomeFanFinished(Attica::BaseJob *job) { if (!jobSuccess(job)) { return; } emit signalInformation(i18n("You are now a fan.")); } bool AtticaProvider::jobSuccess(Attica::BaseJob *job) const { if (job->metadata().error() == Attica::Metadata::NoError) { return true; } qCDebug(KNEWSTUFFCORE) << "job error: " << job->metadata().error() << " status code: " << job->metadata().statusCode() << job->metadata().message(); if (job->metadata().error() == Attica::Metadata::NetworkError) { emit signalErrorCode(KNSCore::NetworkError, i18n("Network error %1: %2", job->metadata().statusCode(), job->metadata().statusString()), job->metadata().statusCode()); } if (job->metadata().error() == Attica::Metadata::OcsError) { if (job->metadata().statusCode() == 200) { emit signalErrorCode(KNSCore::OcsError, i18n("Too many requests to server. Please try again in a few minutes."), job->metadata().statusCode()); } else if (job->metadata().statusCode() == 405) { emit signalErrorCode(KNSCore::OcsError, i18n("The Open Collaboration Services instance %1 does not support the attempted function.").arg(name()), job->metadata().statusCode()); } else { emit signalErrorCode(KNSCore::OcsError, i18n("Unknown Open Collaboration Service API error. (%1)", job->metadata().statusCode()), job->metadata().statusCode()); } } return false; } EntryInternal AtticaProvider::entryFromAtticaContent(const Attica::Content &content) { EntryInternal entry; entry.setProviderId(id()); entry.setUniqueId(content.id()); entry.setStatus(KNS3::Entry::Downloadable); entry.setVersion(content.version()); entry.setReleaseDate(content.updated().date()); entry.setCategory(content.attribute(QStringLiteral("typeid"))); int index = mCachedEntries.indexOf(entry); if (index >= 0) { EntryInternal &cacheEntry = mCachedEntries[index]; // check if updateable if (((cacheEntry.status() == KNS3::Entry::Installed) || (cacheEntry.status() == KNS3::Entry::Updateable)) && ((cacheEntry.version() != entry.version()) || (cacheEntry.releaseDate() != entry.releaseDate()))) { cacheEntry.setStatus(KNS3::Entry::Updateable); cacheEntry.setUpdateVersion(entry.version()); cacheEntry.setUpdateReleaseDate(entry.releaseDate()); } entry = cacheEntry; } else { mCachedEntries.append(entry); } entry.setName(content.name()); entry.setHomepage(content.detailpage()); entry.setRating(content.rating()); entry.setNumberOfComments(content.numberOfComments()); entry.setDownloadCount(content.downloads()); entry.setNumberFans(content.attribute(QStringLiteral("fans")).toInt()); entry.setDonationLink(content.attribute(QStringLiteral("donationpage"))); entry.setKnowledgebaseLink(content.attribute(QStringLiteral("knowledgebasepage"))); entry.setNumberKnowledgebaseEntries(content.attribute(QStringLiteral("knowledgebaseentries")).toInt()); entry.setHomepage(content.detailpage()); entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("1")), EntryInternal::PreviewSmall1); entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("2")), EntryInternal::PreviewSmall2); entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("3")), EntryInternal::PreviewSmall3); entry.setPreviewUrl(content.previewPicture(QStringLiteral("1")), EntryInternal::PreviewBig1); entry.setPreviewUrl(content.previewPicture(QStringLiteral("2")), EntryInternal::PreviewBig2); entry.setPreviewUrl(content.previewPicture(QStringLiteral("3")), EntryInternal::PreviewBig3); entry.setLicense(content.license()); Author author; + author.setId(content.author()); author.setName(content.author()); author.setHomepage(content.attribute(QStringLiteral("profilepage"))); entry.setAuthor(author); entry.setSource(EntryInternal::Online); entry.setSummary(content.description()); entry.setShortSummary(content.summary()); entry.setChangelog(content.changelog()); entry.setTags(content.tags()); entry.clearDownloadLinkInformation(); const QList descs = content.downloadUrlDescriptions(); for (const Attica::DownloadDescription &desc : descs) { EntryInternal::DownloadLinkInformation info; info.name = desc.name(); info.priceAmount = desc.priceAmount(); info.distributionType = desc.distributionType(); info.descriptionLink = desc.link(); info.id = desc.id(); info.size = desc.size(); info.isDownloadtypeLink = desc.type() == Attica::DownloadDescription::LinkDownload; info.tags = desc.tags(); entry.appendDownloadLinkInformation(info); } return entry; } } // namespace diff --git a/src/attica/atticaprovider_p.h b/src/attica/atticaprovider_p.h index 66810968..90eb5c88 100644 --- a/src/attica/atticaprovider_p.h +++ b/src/attica/atticaprovider_p.h @@ -1,129 +1,141 @@ /* Copyright (c) 2009-2010 Frederik Gladhorn 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) any later version. 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 KNEWSTUFF3_ATTICAPROVIDER_P_H #define KNEWSTUFF3_ATTICAPROVIDER_P_H #include #include #include #include #include #include "provider.h" namespace Attica { class BaseJob; } namespace KNSCore { /** * @short KNewStuff Attica Provider class. * * This class provides accessors for the provider object. * It should not be used directly by the application. * This class is the base class and will be instantiated for * websites that implement the Open Collaboration Services. * * @author Frederik Gladhorn * * @internal */ class AtticaProvider: public Provider { Q_OBJECT public: explicit AtticaProvider(const QStringList &categories); AtticaProvider(const Attica::Provider &provider, const QStringList &categories); QString id() const override; /** * set the provider data xml, to initialize the provider */ bool setProviderXML(const QDomElement &xmldata) override; bool isInitialized() const override; void setCachedEntries(const KNSCore::EntryInternal::List &cachedEntries) override; void loadEntries(const KNSCore::Provider::SearchRequest &request) override; void loadEntryDetails(const KNSCore::EntryInternal &entry) override; void loadPayloadLink(const EntryInternal &entry, int linkId) override; + /** + * The slot which causes loading of comments for the Attica provider + * @see Provider::loadComments(const EntryInternal &entry, int commentsPerPage, int page) + */ + Q_SLOT void loadComments(const EntryInternal &entry, int commentsPerPage, int page); + /** + * The slot which causes loading of a person's details + * @see Provider::loadPerson(const QString &username) + */ + Q_SLOT void loadPerson(const QString &username); bool userCanVote() override { return true; } void vote(const EntryInternal &entry, uint rating) override; bool userCanBecomeFan() override { return true; } void becomeFan(const EntryInternal &entry) override; private Q_SLOTS: void providerLoaded(const Attica::Provider &provider); void listOfCategoriesLoaded(Attica::BaseJob *); void categoryContentsLoaded(Attica::BaseJob *job); void downloadItemLoaded(Attica::BaseJob *job); void accountBalanceLoaded(Attica::BaseJob *job); void authenticationCredentialsMissing(const Provider &); void votingFinished(Attica::BaseJob *); void becomeFanFinished(Attica::BaseJob *job); void detailsLoaded(Attica::BaseJob *job); + void loadedComments(Attica::BaseJob *job); + void loadedPerson(Attica::BaseJob *job); private: void checkForUpdates(); EntryInternal::List installedEntries() const; bool jobSuccess(Attica::BaseJob *job) const; Attica::Provider::SortMode atticaSortMode(const SortMode &sortMode); EntryInternal entryFromAtticaContent(const Attica::Content &); // the attica categories we are interested in (e.g. Wallpaper, Application, Vocabulary File...) QHash mCategoryMap; Attica::ProviderManager m_providerManager; Attica::Provider m_provider; KNSCore::EntryInternal::List mCachedEntries; QHash mCachedContent; // Associate job and entry, this is needed when fetching // download links or the account balance in order to continue // when the result is there. QHash > mDownloadLinkJobs; // keep track of the current request QPointer mEntryJob; Provider::SearchRequest mCurrentRequest; QSet m_updateJobs; bool mInitialized; QString m_providerId; Q_DISABLE_COPY(AtticaProvider) }; } #endif diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 9776ccc4..26bb3321 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,121 +1,122 @@ set(KNewStuffCore_SRCS author.cpp + commentsmodel.cpp cache.cpp downloadmanager.cpp engine.cpp entryinternal.cpp imageloader.cpp installation.cpp itemsmodel.cpp provider.cpp security.cpp tagsfilterchecker.cpp xmlloader.cpp errorcode.cpp # A system by which queries can be passed to the user, and responses # gathered, depending on implementation. See question.h for details. question.cpp questionmanager.cpp questionlistener.cpp # A set of minimal KJob based classes, designed to replace the # more powerful KIO based system in places where KIO is not available # for one reason or another. jobs/downloadjob.cpp jobs/filecopyjob.cpp jobs/filecopyworker.cpp jobs/httpjob.cpp jobs/httpworker.cpp ../attica/atticaprovider.cpp ../staticxml/staticxmlprovider.cpp ../upload/atticahelper.cpp ) ecm_qt_declare_logging_category(KNewStuffCore_SRCS HEADER knewstuffcore_debug.h IDENTIFIER KNEWSTUFFCORE CATEGORY_NAME org.kde.knewstuff.core) add_library(KF5NewStuffCore ${KNewStuffCore_SRCS} ) add_library(KF5::NewStuffCore ALIAS KF5NewStuffCore ) generate_export_header(KF5NewStuffCore BASE_NAME KNewStuffCore EXPORT_FILE_NAME knewstuffcore_export.h) # The src/ dir is needed for the entry.h header. This only happens because some # code in Core uses an enum from KNS3::Entry target_include_directories(KF5NewStuffCore PUBLIC "$" INTERFACE "$") target_link_libraries(KF5NewStuffCore PUBLIC KF5::Attica # For interacting with ocs providers, public for uploaddialog slots KF5::CoreAddons Qt5::Xml PRIVATE KF5::Archive # For decompressing archives KF5::I18n # For translations KF5::ConfigCore Qt5::Gui # For QImage ) set_target_properties(KF5NewStuffCore PROPERTIES VERSION ${KNEWSTUFF_VERSION_STRING} SOVERSION ${KNEWSTUFF_SOVERSION} EXPORT_NAME NewStuffCore ) ecm_generate_headers(KNewStuffCore_CamelCase_HEADERS HEADER_NAMES Author Cache DownloadManager Engine EntryInternal ErrorCode Installation ItemsModel Provider Question QuestionListener QuestionManager Security TagsFilterChecker XmlLoader REQUIRED_HEADERS KNewStuffCore_HEADERS PREFIX KNSCore ) install(TARGETS KF5NewStuffCore EXPORT KF5NewStuffCoreTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${KNewStuffCore_CamelCase_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KNewStuff3/KNSCore COMPONENT Devel) install(FILES ${KNewStuffCore_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/knewstuffcore_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KNewStuff3/knscore COMPONENT Devel) if(BUILD_QCH) ecm_add_qch( KF5NewStuffCore_QCH NAME KNewStuffCore BASE_NAME KF5NewStuffCore VERSION ${KF5_VERSION} ORG_DOMAIN org.kde SOURCES ${KNewStuffCore_HEADERS} LINK_QCHS KF5Attica_QCH KF5CoreAddons_QCH BLANK_MACROS KNEWSTUFFCORE_EXPORT KNEWSTUFFCORE_DEPRECATED KNEWSTUFFCORE_DEPRECATED_EXPORT TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME KNewStuffCore LIB_NAME KF5NewStuffCore DEPS "Attica" FILENAME_VAR COREPRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KNewStuff3) install(FILES ${COREPRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) diff --git a/src/core/author.cpp b/src/core/author.cpp index 8db3f157..fa106b0d 100644 --- a/src/core/author.cpp +++ b/src/core/author.cpp @@ -1,63 +1,156 @@ /* This file is part of KNewStuff2. Copyright (c) 2002 Cornelius Schumacher Copyright (c) 2003 - 2007 Josef Spillner 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) any later version. 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 "author.h" +#include + +// BCI: Add a real d-pointer +namespace KNSCore { +struct AuthorPrivate { +public: + QString id; + QString profilepage; + QUrl avatarUrl; + QString description; +}; +} + using namespace KNSCore; +typedef QHash AuthorPrivateHash; +Q_GLOBAL_STATIC(AuthorPrivateHash, d_func) + +static AuthorPrivate *d(const Author *author) +{ + AuthorPrivate *ret = d_func()->value(author); + if (!ret) { + ret = new AuthorPrivate; + d_func()->insert(author, ret); + } + return ret; +} + +static void delete_d(const Author *author) +{ + if (auto d = d_func()) { + delete d->take(author); + } +} + +Author::Author() +{ +} + +KNSCore::Author::Author(const KNSCore::Author &other) +{ + this->setAvatarUrl(other.avatarUrl()); + this->setDescription(other.description()); + this->setEmail(other.email()); + this->setHomepage(other.homepage()); + this->setId(other.id()); + this->setJabber(other.jabber()); + this->setName(other.name()); + this->setProfilepage(other.profilepage()); +} + +Author::~Author() +{ + delete_d(this); +} + +void KNSCore::Author::setId(const QString &id) +{ + d(this)->id = id; +} + +QString KNSCore::Author::id() const +{ + return d(this)->id; +} + void Author::setName(const QString &_name) { mName = _name; } QString Author::name() const { return mName; } void Author::setEmail(const QString &_email) { mEmail = _email; } QString Author::email() const { return mEmail; } void Author::setJabber(const QString &_jabber) { mJabber = _jabber; } QString Author::jabber() const { return mJabber; } void Author::setHomepage(const QString &_homepage) { mHomepage = _homepage; } QString Author::homepage() const { return mHomepage; } +void Author::setProfilepage(const QString &profilepage) +{ + d(this)->profilepage = profilepage; +} + +QString Author::profilepage() const +{ + return d(this)->profilepage; +} + +void Author::setAvatarUrl(const QUrl &avatarUrl) +{ + d(this)->avatarUrl = avatarUrl; +} + +QUrl Author::avatarUrl() const +{ + return d(this)->avatarUrl; +} + +void Author::setDescription(const QString &description) +{ + d(this)->description = description; +} + +QString Author::description() const +{ + return d(this)->description; +} diff --git a/src/core/author.h b/src/core/author.h index cad88281..b689d037 100644 --- a/src/core/author.h +++ b/src/core/author.h @@ -1,99 +1,151 @@ /* This file is part of KNewStuff2. Copyright (c) 2002 Cornelius Schumacher Copyright (c) 2003 - 2007 Josef Spillner 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) any later version. 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 KNEWSTUFF3_AUTHOR_P_H #define KNEWSTUFF3_AUTHOR_P_H #include +#include #include "knewstuffcore_export.h" namespace KNSCore { struct AuthorPrivate; /** * @short KNewStuff author information. * * This class provides accessor methods to the author data * as used by KNewStuff. * It should probably not be used directly by the application. * * @author Josef Spillner (spillner@kde.org) */ class KNEWSTUFFCORE_EXPORT Author { public: + explicit Author(); + Author(const Author &other); + Author& operator=(const Author &other) = default; + Author& operator=(Author&&) = default; + ~Author(); + + /** + * Sets the user ID of the author. + */ + void setId(const QString &id); + + /** + * Retrieve the author's user ID + * @return the author's user ID + */ + QString id() const; + /** * Sets the full name of the author. */ void setName(const QString &name); /** * Retrieve the author's name. * * @return author name */ QString name() const; /** * Sets the email address of the author. */ void setEmail(const QString &email); /** * Retrieve the author's email address. * * @return author email address */ QString email() const; /** * Sets the jabber address of the author. */ void setJabber(const QString &jabber); /** * Retrieve the author's jabber address. * * @return author jabber address */ QString jabber() const; /** * Sets the homepage of the author. */ void setHomepage(const QString &homepage); /** * Retrieve the author's homepage. * * @return author homepage */ QString homepage() const; + /** + * Sets the profile page of the author, usually located on the server hosting the content. + */ + void setProfilepage(const QString &profilepage); + + /** + * Retrieve the author's profile page. + * + * @return author profile page + */ + QString profilepage() const; + + /** + * Sets the url for the user's avatar image + */ + void setAvatarUrl(const QUrl &avatarUrl); + + /** + * Retrieve the url of the user's avatar image + * + * @return a url for the user's avatar (may be empty) + */ + QUrl avatarUrl() const; + + /** + * Retrieve the user's description text + * + * @return A long(ish)-form text describing this user, usually self-entered + */ + QString description() const; + /** + * Set the user's description + */ + void setDescription(const QString &description); private: QString mName; QString mEmail; QString mJabber; QString mHomepage; }; } #endif diff --git a/src/core/commentsmodel.cpp b/src/core/commentsmodel.cpp new file mode 100644 index 00000000..68d7af4f --- /dev/null +++ b/src/core/commentsmodel.cpp @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2019 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 "commentsmodel.h" + +#include "entryinternal.h" +#include "engine.h" +#include "knewstuffcore_debug.h" + +#include + +#include +#include + +namespace KNSCore { +class CommentsModel::Private { +public: + Private(CommentsModel *qq) + : q(qq) + {} + CommentsModel *q = nullptr; + Engine *engine = nullptr; + + EntryInternal entry; + + QList> comments; + + enum FetchOptions { + NoOption, + ClearModel + }; + bool fetchThrottle = false; + void fetch(FetchOptions option = NoOption) { + if (fetchThrottle) { + return; + } + fetchThrottle = true; + QTimer::singleShot(1, q, [this](){ + fetchThrottle = false; + }); + // Sanity checks, because we need a few things to be correct before we can actually fetch comments... + if (!engine) { + qCWarning(KNEWSTUFFCORE) << "CommentsModel must be parented on a KNSCore::Engine instance to be able to fetch comments"; + } + if (!entry.isValid()) { + qCWarning(KNEWSTUFFCORE) << "Without an entry to fetch comments for, CommentsModel cannot fetch comments for it"; + } + + if (engine && entry.isValid()) { + QSharedPointer provider = engine->provider(entry.providerId()); + if (option == ClearModel) { + emit q->beginResetModel(); + comments.clear(); + provider->disconnect(q); + q->connect(provider.data(), &Provider::commentsLoaded, q, [=](const QList> &newComments){ + QList> actualNewComments; + for (std::shared_ptr comment : newComments) { + bool commentIsKnown = false; + for (std::shared_ptr existingComment : comments) { + if (existingComment->id == comment->id) { + commentIsKnown = true; + break; + } + } + if (commentIsKnown) { + continue; + } + actualNewComments << comment; + } + if (actualNewComments.count() > 0) { + q->beginInsertRows(QModelIndex(), comments.count(), comments.count() + actualNewComments.count() - 1); + qCDebug(KNEWSTUFFCORE) << "Appending" << actualNewComments.count() << "new comments"; + comments.append(actualNewComments); + q->endInsertRows(); + } + }); + emit q->endResetModel(); + } + int commentsPerPage = 100; + int pageToLoad = comments.count() / commentsPerPage; + qCDebug(KNEWSTUFFCORE) << "Loading comments, page" << pageToLoad << "with current comment count" << comments.count() << "out of a total of" << entry.numberOfComments(); + provider->loadComments(entry, commentsPerPage, pageToLoad); + } + } +}; +} + +KNSCore::CommentsModel::CommentsModel(Engine *parent) + : QAbstractListModel(parent) + , d(new Private(this)) +{ + d->engine = parent; +} + +KNSCore::CommentsModel::~CommentsModel() +{ + delete d; +} + +QHash KNSCore::CommentsModel::roleNames() const +{ + static const QHash roles{ + {IdRole, "id"}, + {SubjectRole, "subject"}, + {TextRole, "text"}, + {ChildCountRole, "childCound"}, + {UsernameRole, "username"}, + {DateRole, "date"}, + {ScoreRole, "score"}, + {ParentIndexRole, "parentIndex"}, + {DepthRole, "depth"} + }; + return roles; +} + +QVariant KNSCore::CommentsModel::data(const QModelIndex &index, int role) const +{ + QVariant value; + if (checkIndex(index)) { + std::shared_ptr comment = d->comments[index.row()]; + switch (role) + { + case IdRole: + value.setValue(comment->id); + break; + case SubjectRole: + value.setValue(comment->subject); + break; + case TextRole: + value.setValue(comment->text); + break; + case ChildCountRole: + value.setValue(comment->childCount); + break; + case UsernameRole: + value.setValue(comment->username); + break; + case DateRole: + value.setValue(comment->date); + break; + case ScoreRole: + value.setValue(comment->score); + break; + case ParentIndexRole: + { + int idx{-1}; + if (comment->parent) { + d->comments.indexOf(std::shared_ptr(comment->parent)); + } + value.setValue(idx); + } + break; + case DepthRole: + { + int depth{0}; + if (comment->parent) { + std::shared_ptr child = comment->parent; + while (child) { + ++depth; + child = child->parent; + } + } + value.setValue(depth); + } + break; + default: + value.setValue(i18nc("The value returned for an unknown role when requesting data from the model.", "Unknown CommentsModel role")); + break; + } + } + return value; +} + +int KNSCore::CommentsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return d->comments.count(); +} + +bool KNSCore::CommentsModel::canFetchMore(const QModelIndex &parent) const +{ + if (parent.isValid()) + return false; + if (d->entry.numberOfComments() > d->comments.count()) + return true; + return false; +} + +void KNSCore::CommentsModel::fetchMore(const QModelIndex &parent) +{ + if(parent.isValid()) + return; + d->fetch(); +} + +const KNSCore::EntryInternal &KNSCore::CommentsModel::entry() const +{ + return d->entry; +} + +void KNSCore::CommentsModel::setEntry(const KNSCore::EntryInternal &newEntry) +{ + d->entry = newEntry; + d->fetch(Private::ClearModel); + emit entryChanged(); +} diff --git a/src/core/commentsmodel.h b/src/core/commentsmodel.h new file mode 100644 index 00000000..45760308 --- /dev/null +++ b/src/core/commentsmodel.h @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2019 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 KNSCORE_COMMENTSMODEL_H +#define KNSCORE_COMMENTSMODEL_H + +#include +#include + +#include "engine.h" + +#include "knewstuffcore_export.h" + +namespace KNSCore +{ +class EntryInternal; + +struct Comment { + QString id; + QString subject; + QString text; + int childCount = 0; + QString username; + QDateTime date; + int score = 0; + std::shared_ptr parent; +}; + +/** + * @brief A model which takes care of the comments for a single EntryInternal + * + * This model should preferably be constructed by asking the Engine to give a model + * instance to you for a specific entry using the commentsForEntry function. If you + * insist, you can construct an instance yourself as well, but this is not recommended. + * + * @see Engine::commentsForEntry(KNSCore::EntryInternal) + * @since 5.63 + */ +class KNEWSTUFFCORE_EXPORT CommentsModel : public QAbstractListModel +{ + Q_OBJECT + /** + * The Entry for which this model should handle comments + */ + Q_PROPERTY(KNSCore::EntryInternal entry READ entry WRITE setEntry NOTIFY entryChanged) +public: + /** + * Construct a new CommentsModel instance. + * @note The class is intended to be constructed using the Engine::commentsForEntry function + * @see Engine::commentsForEntry(KNSCore::EntryInternal) + */ + explicit CommentsModel(Engine *parent = nullptr); + ~CommentsModel() override; + + enum Roles { + SubjectRole = Qt::DisplayRole, + IdRole = Qt::UserRole + 1, + TextRole, + ChildCountRole, + UsernameRole, + DateRole, + ScoreRole, + ParentIndexRole, + DepthRole + }; + Q_ENUM(Roles) + + QHash< int, QByteArray > roleNames() const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; + + const KNSCore::EntryInternal &entry() const; + void setEntry(const KNSCore::EntryInternal &newEntry); + Q_SIGNAL void entryChanged(); + +private: + class Private; + Private *d; +}; +} + +#endif//KNSCORE_COMMENTSMODEL_H diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 7d0b9948..01e6fa90 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -1,762 +1,812 @@ /* knewstuff3/engine.cpp Copyright (c) 2007 Josef Spillner Copyright (C) 2007-2010 Frederik Gladhorn Copyright (c) 2009 Jeremy Whiting Copyright (c) 2010 Matthias Fuchs 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) any later version. 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 "engine.h" #include "../entry.h" +#include "commentsmodel.h" #include "installation.h" #include "xmlloader.h" #include "imageloader_p.h" #include #include #include #include #include #include #include #include #include #include #if defined(Q_OS_WIN) #include #include #endif // libattica #include #include // own #include "../attica/atticaprovider_p.h" #include "cache.h" #include "../staticxml/staticxmlprovider_p.h" using namespace KNSCore; typedef QHash EngineProviderLoaderHash; Q_GLOBAL_STATIC(QThreadStorage, s_engineProviderLoaders) class EnginePrivate { public: QList categoriesMetadata; Attica::ProviderManager *m_atticaProviderManager = nullptr; QStringList tagFilter; QStringList downloadTagFilter; bool configLocationFallback = true; + QString name; + QMap commentsModels; }; Engine::Engine(QObject *parent) : QObject(parent) , m_installation(new Installation) , m_cache() , m_searchTimer(new QTimer) , d(new EnginePrivate) , m_currentPage(-1) , m_pageSize(20) , m_numDataJobs(0) , m_numPictureJobs(0) , m_numInstallJobs(0) , m_initialized(false) { m_searchTimer->setSingleShot(true); m_searchTimer->setInterval(1000); connect(m_searchTimer, &QTimer::timeout, this, &Engine::slotSearchTimerExpired); connect(m_installation, &Installation::signalInstallationFinished, this, &Engine::slotInstallationFinished); connect(m_installation, &Installation::signalInstallationFailed, this, &Engine::slotInstallationFailed); // Pass along old error signal through new signal for locations which have not been updated yet connect(this, &Engine::signalError, this, [this](const QString& message){ emit signalErrorCode(ErrorCode::UnknownError, message, QVariant()); }); } Engine::~Engine() { if (m_cache) { m_cache->writeRegistry(); } delete d->m_atticaProviderManager; delete m_searchTimer; delete m_installation; delete d; } bool Engine::init(const QString &configfile) { qCDebug(KNEWSTUFFCORE) << "Initializing KNSCore::Engine from '" << configfile << "'"; emit signalBusy(i18n("Initializing")); QScopedPointer conf; /// TODO KF6: This is fallback logic for an old location for the knsrc files. This should be considered deprecated in KF5, /// and it would make a lot of sense to disable it entirely for KF6 bool isRelativeConfig = QFileInfo(configfile).isRelative(); QString actualConfig; if (isRelativeConfig) { // Don't do the expensive search unless the config is relative actualConfig = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString::fromLatin1("knsrcfiles/%1").arg(configfile)); } if (isRelativeConfig && d->configLocationFallback && actualConfig.isEmpty()) { conf.reset(new KConfig(configfile)); qCWarning(KNEWSTUFFCORE) << "Using a deprecated location for the knsrc file" << configfile << " - please contact the author of the software which provides this file to get it updated to use the new location"; } else if (isRelativeConfig) { conf.reset(new KConfig(QString::fromLatin1("knsrcfiles/%1").arg(configfile), KConfig::FullConfig, QStandardPaths::GenericDataLocation)); } else { conf.reset(new KConfig(configfile)); } if (conf->accessMode() == KConfig::NoAccess) { emit signalErrorCode(KNSCore::ConfigFileError, i18n("Configuration file exists, but cannot be opened: \"%1\"", configfile), configfile); qCCritical(KNEWSTUFFCORE) << "The knsrc file '" << configfile << "' was found but could not be opened."; return false; } KConfigGroup group; if (conf->hasGroup("KNewStuff3")) { qCDebug(KNEWSTUFFCORE) << "Loading KNewStuff3 config: " << configfile; group = conf->group("KNewStuff3"); } else if (conf->hasGroup("KNewStuff2")) { qCDebug(KNEWSTUFFCORE) << "Loading KNewStuff2 config: " << configfile; group = conf->group("KNewStuff2"); } else { emit signalErrorCode(KNSCore::ConfigFileError, i18n("Configuration file is invalid: \"%1\"", configfile), configfile); qCCritical(KNEWSTUFFCORE) << configfile << " doesn't contain a KNewStuff3 section."; return false; } + d->name = group.readEntry("Name", QString()); m_categories = group.readEntry("Categories", QStringList()); m_adoptionCommand = group.readEntry("AdoptionCommand", QString()); qCDebug(KNEWSTUFFCORE) << "Categories: " << m_categories; m_providerFileUrl = group.readEntry("ProvidersUrl", QString()); d->tagFilter = group.readEntry("TagFilter", QStringList()); if (d->tagFilter.isEmpty()) { d->tagFilter.append(QStringLiteral("ghns_excluded!=1")); } d->downloadTagFilter = group.readEntry("DownloadTagFilter", QStringList()); const QString configFileName = QFileInfo(QDir::isAbsolutePath(configfile) ? configfile : QStandardPaths::locate(QStandardPaths::GenericConfigLocation, configfile)).baseName(); // let installation read install specific config if (!m_installation->readConfig(group)) { return false; } connect(m_installation, &Installation::signalEntryChanged, this, &Engine::slotEntryChanged); m_cache = Cache::getCache(configFileName); connect(this, &Engine::signalEntryChanged, m_cache.data(), &Cache::registerChangedEntry); m_cache->readRegistry(); m_initialized = true; // load the providers loadProviders(); return true; } +QString KNSCore::Engine::name() const +{ + return d->name; +} + QStringList Engine::categories() const { return m_categories; } QStringList Engine::categoriesFilter() const { return m_currentRequest.categories; } QList Engine::categoriesMetadata() { return d->categoriesMetadata; } void Engine::loadProviders() { if (m_providerFileUrl.isEmpty()) { // it would be nicer to move the attica stuff into its own class qCDebug(KNEWSTUFFCORE) << "Using OCS default providers"; delete d->m_atticaProviderManager; d->m_atticaProviderManager = new Attica::ProviderManager; connect(d->m_atticaProviderManager, &Attica::ProviderManager::providerAdded, this, &Engine::atticaProviderLoaded); connect(d->m_atticaProviderManager, &Attica::ProviderManager::failedToLoad, this, &Engine::slotProvidersFailed); d->m_atticaProviderManager->loadDefaultProviders(); } else { qCDebug(KNEWSTUFFCORE) << "loading providers from " << m_providerFileUrl; emit signalBusy(i18n("Loading provider information")); XmlLoader *loader = s_engineProviderLoaders()->localData().value(m_providerFileUrl); if (!loader) { qCDebug(KNEWSTUFFCORE) << "No xml loader for this url yet, so create one and temporarily store that" << m_providerFileUrl; loader = new XmlLoader(this); s_engineProviderLoaders()->localData().insert(m_providerFileUrl, loader); connect(loader, &XmlLoader::signalLoaded, this, [this](){ s_engineProviderLoaders()->localData().remove(m_providerFileUrl); }); connect(loader, &XmlLoader::signalFailed, this, [this](){ s_engineProviderLoaders()->localData().remove(m_providerFileUrl); }); loader->load(QUrl(m_providerFileUrl)); } connect(loader, &XmlLoader::signalLoaded, this, &Engine::slotProviderFileLoaded); connect(loader, &XmlLoader::signalFailed, this, &Engine::slotProvidersFailed); } } void Engine::slotProviderFileLoaded(const QDomDocument &doc) { qCDebug(KNEWSTUFFCORE) << "slotProvidersLoaded"; bool isAtticaProviderFile = false; // get each provider element, and create a provider object from it QDomElement providers = doc.documentElement(); if (providers.tagName() == QLatin1String("providers")) { isAtticaProviderFile = true; } else if (providers.tagName() != QLatin1String("ghnsproviders") && providers.tagName() != QLatin1String("knewstuffproviders")) { qWarning() << "No document in providers.xml."; emit signalErrorCode(KNSCore::ProviderError, i18n("Could not load get hot new stuff providers from file: %1", m_providerFileUrl), m_providerFileUrl); return; } QDomElement n = providers.firstChildElement(QStringLiteral("provider")); while (!n.isNull()) { qCDebug(KNEWSTUFFCORE) << "Provider attributes: " << n.attribute(QStringLiteral("type")); QSharedPointer provider; if (isAtticaProviderFile || n.attribute(QStringLiteral("type")).toLower() == QLatin1String("rest")) { provider.reset(new AtticaProvider(m_categories)); connect(provider.data(), &Provider::categoriesMetadataLoded, this, [this](const QList &categories){ d->categoriesMetadata = categories; emit signalCategoriesMetadataLoded(categories); }); } else { provider.reset(new StaticXmlProvider); } if (provider->setProviderXML(n)) { addProvider(provider); } else { emit signalErrorCode(KNSCore::ProviderError, i18n("Error initializing provider."), m_providerFileUrl); } n = n.nextSiblingElement(); } emit signalBusy(i18n("Loading data")); } void Engine::atticaProviderLoaded(const Attica::Provider &atticaProvider) { qCDebug(KNEWSTUFFCORE) << "atticaProviderLoaded called"; if (!atticaProvider.hasContentService()) { qCDebug(KNEWSTUFFCORE) << "Found provider: " << atticaProvider.baseUrl() << " but it does not support content"; return; } QSharedPointer provider = QSharedPointer (new AtticaProvider(atticaProvider, m_categories)); connect(provider.data(), &Provider::categoriesMetadataLoded, this, [this](const QList &categories){ d->categoriesMetadata = categories; emit signalCategoriesMetadataLoded(categories); }); addProvider(provider); } void Engine::addProvider(QSharedPointer provider) { qCDebug(KNEWSTUFFCORE) << "Engine addProvider called with provider with id " << provider->id(); m_providers.insert(provider->id(), provider); provider->setTagFilter(d->tagFilter); provider->setDownloadTagFilter(d->downloadTagFilter); connect(provider.data(), &Provider::providerInitialized, this, &Engine::providerInitialized); connect(provider.data(), &Provider::loadingFinished, this, &Engine::slotEntriesLoaded); connect(provider.data(), &Provider::entryDetailsLoaded, this, &Engine::slotEntryDetailsLoaded); connect(provider.data(), &Provider::payloadLinkLoaded, this, &Engine::downloadLinkLoaded); connect(provider.data(), &Provider::signalError, this, &Engine::signalError); connect(provider.data(), &Provider::signalErrorCode, this, &Engine::signalErrorCode); connect(provider.data(), &Provider::signalInformation, this, &Engine::signalIdle); } void Engine::providerJobStarted(KJob *job) { emit jobStarted(job, i18n("Loading data from provider")); } void Engine::slotProvidersFailed() { emit signalErrorCode(KNSCore::ProviderError, i18n("Loading of providers from file: %1 failed", m_providerFileUrl), m_providerFileUrl); } void Engine::providerInitialized(Provider *p) { qCDebug(KNEWSTUFFCORE) << "providerInitialized" << p->name(); p->setCachedEntries(m_cache->registryForProvider(p->id())); updateStatus(); for (const QSharedPointer &p : qAsConst(m_providers)) { if (!p->isInitialized()) { return; } } emit signalProvidersLoaded(); } void Engine::slotEntriesLoaded(const KNSCore::Provider::SearchRequest &request, KNSCore::EntryInternal::List entries) { m_currentPage = qMax(request.page, m_currentPage); qCDebug(KNEWSTUFFCORE) << "loaded page " << request.page << "current page" << m_currentPage << "count:" << entries.count(); if (request.filter == Provider::Updates) { emit signalUpdateableEntriesLoaded(entries); } else { m_cache->insertRequest(request, entries); emit signalEntriesLoaded(entries); } --m_numDataJobs; updateStatus(); } void Engine::reloadEntries() { emit signalResetView(); m_currentPage = -1; m_currentRequest.pageSize = m_pageSize; m_currentRequest.page = 0; m_numDataJobs = 0; for (const QSharedPointer &p : qAsConst(m_providers)) { if (p->isInitialized()) { if (m_currentRequest.filter == Provider::Installed) { // when asking for installed entries, never use the cache p->loadEntries(m_currentRequest); } else { // take entries from cache until there are no more EntryInternal::List cache; EntryInternal::List lastCache = m_cache->requestFromCache(m_currentRequest); while (!lastCache.isEmpty()) { qCDebug(KNEWSTUFFCORE) << "From cache"; cache << lastCache; m_currentPage = m_currentRequest.page; ++m_currentRequest.page; lastCache = m_cache->requestFromCache(m_currentRequest); } // Since the cache has no more pages, reset the request's page if (m_currentPage >= 0) { m_currentRequest.page = m_currentPage; } if (!cache.isEmpty()) { emit signalEntriesLoaded(cache); } else { qCDebug(KNEWSTUFFCORE) << "From provider"; p->loadEntries(m_currentRequest); ++m_numDataJobs; updateStatus(); } } } } } void Engine::setCategoriesFilter(const QStringList &categories) { m_currentRequest.categories = categories; reloadEntries(); } void Engine::setSortMode(Provider::SortMode mode) { if (m_currentRequest.sortMode != mode) { m_currentRequest.page = -1; } m_currentRequest.sortMode = mode; reloadEntries(); } +Provider::SortMode KNSCore::Engine::sortMode() const +{ + return m_currentRequest.sortMode; +} + void KNSCore::Engine::setFilter(Provider::Filter filter) { if (m_currentRequest.filter != filter) { m_currentRequest.page = -1; } m_currentRequest.filter = filter; reloadEntries(); } -void KNSCore::Engine::fetchEntryById(const QString& id) +Provider::Filter KNSCore::Engine::filter() const +{ + return m_currentRequest.filter; +} + +void KNSCore::Engine::fetchEntryById(const QString &id) { m_searchTimer->stop(); m_currentRequest = KNSCore::Provider::SearchRequest(KNSCore::Provider::Newest, KNSCore::Provider::ExactEntryId, id); m_currentRequest.pageSize = m_pageSize; EntryInternal::List cache = m_cache->requestFromCache(m_currentRequest); if (!cache.isEmpty()) { reloadEntries(); } else { m_searchTimer->start(); } } void Engine::setSearchTerm(const QString &searchString) { m_searchTimer->stop(); m_currentRequest.searchTerm = searchString; EntryInternal::List cache = m_cache->requestFromCache(m_currentRequest); if (!cache.isEmpty()) { reloadEntries(); } else { m_searchTimer->start(); } } +QString KNSCore::Engine::searchTerm() const +{ + return m_currentRequest.searchTerm; +} + void Engine::setTagFilter(const QStringList &filter) { d->tagFilter = filter; for (const QSharedPointer &p : qAsConst(m_providers)) { p->setTagFilter(d->tagFilter); } } QStringList Engine::tagFilter() const { return d->tagFilter; } void KNSCore::Engine::addTagFilter(const QString &filter) { d->tagFilter << filter; for (const QSharedPointer &p : qAsConst(m_providers)) { p->setTagFilter(d->tagFilter); } } void Engine::setDownloadTagFilter(const QStringList &filter) { d->downloadTagFilter = filter; for (const QSharedPointer &p : qAsConst(m_providers)) { p->setDownloadTagFilter(d->downloadTagFilter); } } QStringList Engine::downloadTagFilter() const { return d->downloadTagFilter; } void Engine::addDownloadTagFilter(const QString &filter) { d->downloadTagFilter << filter; for (const QSharedPointer &p : qAsConst(m_providers)) { p->setDownloadTagFilter(d->downloadTagFilter); } } void Engine::slotSearchTimerExpired() { reloadEntries(); } void Engine::requestMoreData() { qCDebug(KNEWSTUFFCORE) << "Get more data! current page: " << m_currentPage << " requested: " << m_currentRequest.page; if (m_currentPage < m_currentRequest.page) { return; } m_currentRequest.page++; doRequest(); } void Engine::requestData(int page, int pageSize) { m_currentRequest.page = page; m_currentRequest.pageSize = pageSize; doRequest(); } void Engine::doRequest() { for (const QSharedPointer &p : qAsConst(m_providers)) { if (p->isInitialized()) { p->loadEntries(m_currentRequest); ++m_numDataJobs; updateStatus(); } } } void Engine::install(KNSCore::EntryInternal entry, int linkId) { if (entry.status() == KNS3::Entry::Updateable) { entry.setStatus(KNS3::Entry::Updating); } else { entry.setStatus(KNS3::Entry::Installing); } emit signalEntryChanged(entry); qCDebug(KNEWSTUFFCORE) << "Install " << entry.name() << " from: " << entry.providerId(); QSharedPointer p = m_providers.value(entry.providerId()); if (p) { p->loadPayloadLink(entry, linkId); ++m_numInstallJobs; updateStatus(); } } void Engine::slotInstallationFinished() { --m_numInstallJobs; updateStatus(); } void Engine::slotInstallationFailed(const QString &message) { --m_numInstallJobs; emit signalErrorCode(KNSCore::InstallationError, message, QVariant()); } void Engine::slotEntryDetailsLoaded(const KNSCore::EntryInternal &entry) { emit signalEntryDetailsLoaded(entry); } void Engine::downloadLinkLoaded(const KNSCore::EntryInternal &entry) { m_installation->install(entry); } void Engine::uninstall(KNSCore::EntryInternal entry) { const KNSCore::EntryInternal::List list = m_cache->registryForProvider(entry.providerId()); //we have to use the cached entry here, not the entry from the provider //since that does not contain the list of installed files KNSCore::EntryInternal actualEntryForUninstall; for (const KNSCore::EntryInternal &eInt : list) { if (eInt.uniqueId() == entry.uniqueId()) { actualEntryForUninstall = eInt; break; } } if (!actualEntryForUninstall.isValid()) { qCDebug(KNEWSTUFFCORE) << "could not find a cached entry with following id:" << entry.uniqueId() << " -> using the non-cached version"; return; } entry.setStatus(KNS3::Entry::Installing); actualEntryForUninstall.setStatus(KNS3::Entry::Installing); emit signalEntryChanged(entry); qCDebug(KNEWSTUFFCORE) << "about to uninstall entry " << entry.uniqueId(); // FIXME: change the status? m_installation->uninstall(actualEntryForUninstall); entry.setStatus(KNS3::Entry::Deleted); //status for actual entry gets set in m_installation->uninstall() emit signalEntryChanged(entry); } void Engine::loadDetails(const KNSCore::EntryInternal &entry) { QSharedPointer p = m_providers.value(entry.providerId()); p->loadEntryDetails(entry); } void Engine::loadPreview(const KNSCore::EntryInternal &entry, EntryInternal::PreviewType type) { qCDebug(KNEWSTUFFCORE) << "START preview: " << entry.name() << type; ImageLoader *l = new ImageLoader(entry, type, this); connect(l, &ImageLoader::signalPreviewLoaded, this, &Engine::slotPreviewLoaded); connect(l, &ImageLoader::signalError, this, [this](const KNSCore::EntryInternal &entry, EntryInternal::PreviewType type, const QString &errorText) { emit signalErrorCode(KNSCore::ImageError, errorText, QVariantList() << entry.name() << type); qCDebug(KNEWSTUFFCORE) << "ERROR preview: " << errorText << entry.name() << type; --m_numPictureJobs; updateStatus(); }); l->start(); ++m_numPictureJobs; updateStatus(); } void Engine::slotPreviewLoaded(const KNSCore::EntryInternal &entry, EntryInternal::PreviewType type) { qCDebug(KNEWSTUFFCORE) << "FINISH preview: " << entry.name() << type; emit signalEntryPreviewLoaded(entry, type); --m_numPictureJobs; updateStatus(); } void Engine::contactAuthor(const EntryInternal &entry) { if (!entry.author().email().isEmpty()) { // invoke mail with the address of the author QUrl mailUrl; mailUrl.setScheme(QStringLiteral("mailto")); mailUrl.setPath(entry.author().email()); QUrlQuery query; query.addQueryItem(QStringLiteral("subject"), i18n("Re: %1", entry.name())); mailUrl.setQuery(query); QDesktopServices::openUrl(mailUrl); } else if (!entry.author().homepage().isEmpty()) { QDesktopServices::openUrl(QUrl(entry.author().homepage())); } } void Engine::slotEntryChanged(const KNSCore::EntryInternal &entry) { emit signalEntryChanged(entry); } bool Engine::userCanVote(const EntryInternal &entry) { QSharedPointer p = m_providers.value(entry.providerId()); return p->userCanVote(); } void Engine::vote(const EntryInternal &entry, uint rating) { QSharedPointer p = m_providers.value(entry.providerId()); p->vote(entry, rating); } bool Engine::userCanBecomeFan(const EntryInternal &entry) { QSharedPointer p = m_providers.value(entry.providerId()); return p->userCanBecomeFan(); } void Engine::becomeFan(const EntryInternal &entry) { QSharedPointer p = m_providers.value(entry.providerId()); p->becomeFan(entry); } void Engine::updateStatus() { if (m_numDataJobs > 0) { emit signalBusy(i18n("Loading data")); } else if (m_numPictureJobs > 0) { emit signalBusy(i18np("Loading one preview", "Loading %1 previews", m_numPictureJobs)); } else if (m_numInstallJobs > 0) { emit signalBusy(i18n("Installing")); } else { emit signalIdle(QString()); } } void Engine::checkForUpdates() { for (const QSharedPointer &p : qAsConst(m_providers)) { Provider::SearchRequest request(KNSCore::Provider::Newest, KNSCore::Provider::Updates); p->loadEntries(request); } } void KNSCore::Engine::checkForInstalled() { for (const QSharedPointer &p : qAsConst(m_providers)) { Provider::SearchRequest request(KNSCore::Provider::Newest, KNSCore::Provider::Installed); request.page = 0; request.pageSize = m_pageSize; p->loadEntries(request); } } /** * we look for the directory where all the resources got installed. * assuming it was extracted into a directory */ static QDir sharedDir(QStringList dirs, const QString &rootPath) { while(!dirs.isEmpty()) { const QString currentPath = QDir::cleanPath(dirs.takeLast()); if (!currentPath.startsWith(rootPath)) continue; const QFileInfo current(currentPath); if (!current.isDir()) continue; const QDir dir = current.dir(); if (dir.path()==(rootPath+dir.dirName())) { return dir; } } return {}; } QString Engine::adoptionCommand(const KNSCore::EntryInternal& entry) const { auto adoption = m_adoptionCommand; if(adoption.isEmpty()) return {}; const QLatin1String dirReplace("%d"); if (adoption.contains(dirReplace)) { QString installPath = sharedDir(entry.installedFiles(), m_installation->targetInstallationPath()).path(); adoption.replace(dirReplace, installPath); } const QLatin1String fileReplace("%f"); if (adoption.contains(fileReplace)) { if (entry.installedFiles().isEmpty()) { qCWarning(KNEWSTUFFCORE) << "no installed files to adopt"; } else if (entry.installedFiles().count() != 1) { qCWarning(KNEWSTUFFCORE) << "can only adopt one file, will be using the first" << entry.installedFiles().at(0); } adoption.replace(fileReplace, entry.installedFiles().at(0)); } return adoption; } bool KNSCore::Engine::hasAdoptionCommand() const { return !m_adoptionCommand.isEmpty(); } void KNSCore::Engine::setPageSize(int pageSize) { m_pageSize = pageSize; } QStringList KNSCore::Engine::configSearchLocations(bool includeFallbackLocations) { QStringList ret; if(includeFallbackLocations) { ret += QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation); } QStringList paths = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); for( const QString& path : paths) { ret << QString::fromLocal8Bit("%1/knsrcfiles").arg(path); } return ret; } void KNSCore::Engine::setConfigLocationFallback(bool enableFallback) { d->configLocationFallback = enableFallback; } + +QSharedPointer KNSCore::Engine::provider(const QString &providerId) const +{ + return m_providers.value(providerId); +} + +QSharedPointer KNSCore::Engine::defaultProvider() const +{ + if (m_providers.count() > 0) + return m_providers.constBegin().value(); + return nullptr; +} + +KNSCore::CommentsModel *KNSCore::Engine::commentsForEntry(const KNSCore::EntryInternal &entry) +{ + CommentsModel *model = d->commentsModels[entry]; + if (!model) { + model = new CommentsModel(this); + model->setEntry(entry); + connect(model, &QObject::destroyed, this, [=](){ + d->commentsModels.remove(entry); + }); + d->commentsModels[entry] = model; + } + return model; +} diff --git a/src/core/engine.h b/src/core/engine.h index b78ca932..bb0097d9 100644 --- a/src/core/engine.h +++ b/src/core/engine.h @@ -1,543 +1,597 @@ /* knewstuff3/engine.h. Copyright (c) 2007 Josef Spillner Copyright (C) 2007-2010 Frederik Gladhorn Copyright (c) 2009 Jeremy Whiting 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) any later version. 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 KNEWSTUFF3_ENGINE_P_H #define KNEWSTUFF3_ENGINE_P_H #include #include #include #include #include "provider.h" #include "entryinternal.h" #include "errorcode.h" #include "knewstuffcore_export.h" class QTimer; class KJob; class EnginePrivate; namespace Attica { class ProviderManager; class Provider; } /** * Contains the core functionality for handling interaction with NewStuff providers. * The entrypoint for most things will be the creation of an instance of KNSCore::Engine * which will other classes then either use or get instantiated from directly. * * NOTE: When implementing anything on top of KNSCore, without using either KNS3 or the * Qt Quick components, you will need to implement a custom QuestionListener (see that * class for instructions) * * @see KNSCore::Engine * @see KNSCore::ItemsModel * @see KNSCore::QuestionListener */ namespace KNSCore { class Cache; +class CommentsModel; class Installation; /** * KNewStuff engine. * An engine keeps track of data which is available locally and remote * and offers high-level synchronization calls as well as upload and download * primitives using an underlying GHNS protocol. */ class KNEWSTUFFCORE_EXPORT Engine : public QObject { Q_OBJECT public: /** * Constructor. */ explicit Engine(QObject *parent = nullptr); /** * Destructor. Frees up all the memory again which might be taken * by cached entries and providers. */ ~Engine(); /** * Initializes the engine. This step is application-specific and relies * on an external configuration file, which determines all the details * about the initialization. * * @param configfile KNewStuff2 configuration file (*.knsrc) * @return \b true if any valid configuration was found, \b false otherwise */ bool init(const QString &configfile); + /** + * The name as defined by the knsrc file + * @return The name associated with the engine's configuration file + * @since 5.63 + */ + QString name() const; + /** * Installs an entry's payload file. This includes verification, if * necessary, as well as decompression and other steps according to the * application's *.knsrc file. * * @param entry Entry to be installed * * @see signalInstallationFinished * @see signalInstallationFailed */ void install(KNSCore::EntryInternal entry, int linkId = 1); /** * Uninstalls an entry. It reverses the steps which were performed * during the installation. * * @param entry The entry to deinstall */ void uninstall(KNSCore::EntryInternal entry); /** * Attempt to load a specific preview for the specified entry. * * @param entry The entry to fetch a preview for * @param type The particular preview to fetch * * @see signalEntryPreviewLoaded(KNSCore::EntryInternal, KNSCore::EntryInternal::PreviewType); * @see signalPreviewFailed(); */ void loadPreview(const KNSCore::EntryInternal &entry, EntryInternal::PreviewType type); /** * Get the full details of a specific entry * * @param entry The entry to get full details for * * @see Entry::signalEntryDetailsLoaded(KNSCore::EntryInternal) */ void loadDetails(const KNSCore::EntryInternal &entry); /** * Set the order the search results are returned in. * * Search requests default to showing the newest entries first. * * Note: This will automatically launch a search, which means * you do not need to call requestData manually. * * @see KNSCore::Provider::SearchRequest * @param mode The order you want search results to come back in. */ void setSortMode(Provider::SortMode mode); + /** + * The sort mode set on the current request + * @see setSortMode(Provider::SortMode) + * @since 5.63 + */ + Provider::SortMode sortMode() const; /** * Set a filter for results (defaults to none), which will allow you * to show only installed entries, installed entries which have updates, * or a specific item with a specified ID. The latter further requires * the search term to be the exact ID of the entry you wish to retrieve. * * Note: This will automatically launch a search, which means * you do not need to call requestData manually. * * @see fetchEntryById(QString) * @see setSearchTerm(QString) * @param filter The type of results you wish to see */ void setFilter(Provider::Filter filter); + /** + * The result filter set on the current request + * @see setFilter(Provider::Filter) + * @since 5.63 + */ + Provider::Filter filter() const; /** * Set the categories that will be included in searches * * Note: This will automatically launch a search, which means * you do not need to call requestData manually. * * @see KNSCore::Engine::categories() * @param categories A list of strings of categories */ void setCategoriesFilter(const QStringList &categories); /** * Sets a string search term. * * Note: This will automatically launch a search, which means * you do not need to call requestData manually. * * @param searchString The search term you wish to search for */ void setSearchTerm(const QString &searchString); + /** + * The search term for the current search (empty if none is set) + * @return The current search term + * @since 5.63 + */ + QString searchTerm() const; void reloadEntries(); void requestMoreData(); void requestData(int page, int pageSize); /** * Set a filter for results, which filters out all entries which do not match * the filter, as applied to the tags for the entry. This filters only on the * tags specified for the entry itself. To filter the downloadlinks, use * setDownloadTagFilter(QStringList). * * @note The default filter if one is not set from your knsrc file will filter * out entries marked as ghns_excluded=1. To retain this when setting a custom * filter, add "ghns_excluded!=1" as one of the filters. * * @note Some tags provided by OCS do not supply a value (and are simply passed * as a key). These will be interpreted as having the value 1 for filtering * purposes. An example of this might be ghns_excluded, which in reality will * generally be passed through ocs as "ghns_excluded" rather than "ghns_excluded=1" * * @note As tags are metadata, they are provided in the form of adjectives. They * are never supplied as action verbs or instructions (as an example, a good tag * to suggest that for example a wallpaper is painted would be "painted" as opposed * to "paint", and another example might be that an item should be "excluded" as * opposed to "exclude"). * * == Examples of use == * Value for tag "tagname" must be exactly "tagdata": * tagname==tagdata * * Value for tag "tagname" must be different from "tagdata": * tagname!=tagdata * * == KNSRC entry == * A tag filter line in a .knsrc file, which is a comma semarated list of * tag/value pairs, might look like: * * TagFilter=ghns_excluded!=1,data##mimetype==application/cbr+zip,data##mimetype==application/cbr+rar * which would honour the exclusion and filter out anything that does not * include a comic book archive in either zip or rar format in one or more * of the download items. * Notice in particular that there are two data##mimetype entries. Use this * for when a tag may have multiple values. * * TagFilter=application##architecture==x86_64 * which would not honour the exclusion, and would filter out all entries * which do not mark themselves as having a 64bit application binary in at * least one download item. * * The value does not current suppport wildcards. The list should be considered * a binary AND operation (that is, all filter entries must match for the data * entry to be included in the return data) * * @param filter The filter in the form of a list of strings * @see setDownloadTagFilter(QStringList) * @since 5.51 */ void setTagFilter(const QStringList &filter); /** * Gets the current tag filter list * @see setTagFilter(QStringList) * @since 5.51 */ QStringList tagFilter() const; /** * Add a single filter entry to the entry tag filter. The filter should be in * the same form as the filter lines in the list used by setTagFilter(QStringList) * @param filter The filter in the form of a string * @see setTagFilter(QStringList) * @since 5.51 */ void addTagFilter(const QString &filter); /** * Sets a filter to be applied to the downloads for an entry. The logic is the * same as used in setTagFilter(QStringList), but vitally, only one downloadlink * is required to match the filter for the list to be valid. If you do not wish * to show the others in your client, you must hide them yourself. * * For an entry to be accepted when a download tag filter is set, it must also * be accepted by the entry filter (so, for example, while a list of downloads * might be accepted, if the entry has ghns_excluded set, and the default entry * filter is set, the entry will still be filtered out). * * In your knsrc file, set DownloadTagFilter to the filter you wish to apply, * using the same logic as described for the entry tagfilter. * * @param filter The filter in the form of a list of strings * @see setTagFilter(QStringList) * @since 5.51 */ void setDownloadTagFilter(const QStringList &filter); /** * Gets the current downloadlink tag filter list * @see setDownloadTagFilter(QStringList) * @since 5.51 */ QStringList downloadTagFilter() const; /** * Add a single filter entry to the download tag filter. The filter should be in * the same form as the filter lines in the list used by setDownloadsTagFilter(QStringList) * @param filter The filter in the form of a string * @see setTagFilter(QStringList) * @see setDownloadTagFilter(QStringList) * @since 5.51 */ void addDownloadTagFilter(const QString &filter); /** * Request for packages that are installed and need update * * These will be reported through the signal @see signalUpdateableEntriesLoaded(). */ void checkForUpdates(); /** * Requests installed packages with an up to date state * * @see signalEntriesLoaded() */ void checkForInstalled(); /** * Convenience method to launch a search for one specific entry. * * @note it will reset the engine state * * @param id The ID of the entry you wish to fetch */ void fetchEntryById(const QString &id); /** * Try to contact the author of the entry by email or showing their homepage. */ void contactAuthor(const EntryInternal &entry); /** * Whether or not a user is able to vote on the passed entry. * * @param entry The entry to check votability on * @return True if the user is able to vote on the entry */ bool userCanVote(const EntryInternal &entry); /** * Cast a vote on the passed entry. * * @param entry The entry to vote on * @param rating A number from 0 to 100, 50 being neutral, 0 being most negative and 100 being most positive. */ void vote(const EntryInternal &entry, uint rating); /** * Whether or not the user is allowed to become a fan of * a particular entry. * Not all providers (and consequently entries) support the fan functionality * and you can use this function to determine this ability. * @param entry The entry the user might wish to be a fan of * @return Whether or not it is possible for the user to become a fan of that entry */ bool userCanBecomeFan(const EntryInternal &entry); /** * This will mark the user who is currently authenticated as a fan * of the entry passed to the function. * @param entry The entry the user wants to be a fan of */ void becomeFan(const EntryInternal &entry); // FIXME There is currently no exposed API to remove the fan status /** * The list of the server-side names of the categories handled by this * engine instance. This corresponds directly to the list of categories * in your knsrc file. This is not supposed to be used as user-facing * strings - @see categoriesMetadata() for that. * * @return The categories which this instance of Engine handles */ QStringList categories() const; /** * The list of categories searches will actually show results from. This * is a subset of the categories() list. * * @see KNSCore::Engine::setCategoriesFilter(QString) */ QStringList categoriesFilter() const; /** * The list of metadata for the categories handled by this engine instance. * If you wish to show the categories to the user, this is the data to use. * The category name is the string used to set categories for the filter, * and also what is returned by both categories() and categoriesFilter(). * The human-readable name is displayName, and the only thing which should * be shown to the user. * * @return The metadata for all categories handled by this engine */ QList categoriesMetadata(); /** * The adoption command can be used to allow a user to make use of an entry's * installed data. For example, this command might be used to ask the system to * switch to a wallpaper or icon theme which was installed with KNS. * * The following is how this might look in a knsrc file. The example shows how * an external tool is called on the directory containing the installed file * represented by %d. If you wish to directly point to the installed file, the * substitution variable is %f. *
        AdoptionCommand=/usr/lib64/libexec/plasma-changeicons %d
      * 
* * @param entry The entry to return an adoption command for * @return The command to run to adopt this entry's installed data */ QString adoptionCommand(const KNSCore::EntryInternal &entry) const; /** * Whether or not an adoption command exists for this engine * * @see adoptionCommand(KNSCore::EntryInternal) * @return True if an adoption command exists */ bool hasAdoptionCommand() const; /** * Set the page size for requests not made explicitly with requestData(int,int) * @param pageSize the default number of entries to request from the provider * @see requestData(int,int) */ void setPageSize(int pageSize); /** * Get a list of all the locations which will be used when searching for knsrc * files, in the order in which the search will occur. * * @param includeFallbackLocations Whether or not the deprecated search locations are included * @return The search list for knsrc files * @since 5.57 */ static QStringList configSearchLocations(bool includeFallbackLocations = false); /** * Sets whether or not the config file location discovery fallback should be active. * If enabled (default), if the config file is not found in the knsrcfiles location, * then the engine will also look in the systemwide config location (usually /etc/xdg * on linux). If disabled, this fallback location will not be searched. * * @param enableFallback Whether or not the fallback discovery should be enabled * @since 5.57 */ void setConfigLocationFallback(bool enableFallback); + /** + * The Provider instance with the passed ID + * + * @param providerId The ID of the Provider to fetch + * @return The Provider with the passed ID, or null if non such Provider exists + * @since 5.63 + */ + QSharedPointer provider(const QString &providerId) const; + + /** + * Return the first provider in the providers list (usually the default provider) + * @return The first Provider (or null if the engine is not initialized) + * @since 5.63 + */ + QSharedPointer defaultProvider() const; + + /** + * This function will return an instance of a model which contains comments for + * the entry passed to it. The model may be empty (if there are no comments for + * the entry, which also covers situations where the entry's provider does not + * support commenting) + * + * @param entry The entry to fetch comments for + * @return A model which contains the comments for the specified entry + * @since 5.63 + */ + CommentsModel *commentsForEntry(const KNSCore::EntryInternal &entry); + Q_SIGNALS: /** * Indicates a message to be added to the ui's log, or sent to a messagebox */ void signalMessage(const QString &message); void signalProvidersLoaded(); void signalEntriesLoaded(const KNSCore::EntryInternal::List &entries); void signalUpdateableEntriesLoaded(const KNSCore::EntryInternal::List &entries); void signalEntryChanged(const KNSCore::EntryInternal &entry); void signalEntryDetailsLoaded(const KNSCore::EntryInternal &entry); // a new search result is there, clear the list of items void signalResetView(); void signalEntryPreviewLoaded(const KNSCore::EntryInternal &, KNSCore::EntryInternal::PreviewType); void signalPreviewFailed(); void signalEntryUploadFinished(); void signalEntryUploadFailed(); void signalDownloadDialogDone(KNSCore::EntryInternal::List); void jobStarted(KJob *, const QString &); QT_DEPRECATED void signalError(const QString &); void signalBusy(const QString &); void signalIdle(const QString &); /** * Fires in the case of any critical or serious errors, such as network or API problems. * @param errorCode Represents the specific type of error which has occurred * @param message A human-readable message which can be shown to the end user * @param metadata Any additional data which might be hepful to further work out the details of the error (see KNSCore::EntryInternal::ErrorCode for the metadata details) * @see KNSCore::EntryInternal::ErrorCode * @since 5.53 */ void signalErrorCode(const KNSCore::ErrorCode &errorCode, const QString &message, const QVariant &metadata); void signalCategoriesMetadataLoded(const QList &categories); private Q_SLOTS: // the .knsrc file was loaded void slotProviderFileLoaded(const QDomDocument &doc); // instead of getting providers from knsrc, use what was configured in ocs systemsettings void atticaProviderLoaded(const Attica::Provider &provider); // loading the .knsrc file failed void slotProvidersFailed(); // called when a provider is ready to work void providerInitialized(KNSCore::Provider *); void slotEntriesLoaded(const KNSCore::Provider::SearchRequest &, KNSCore::EntryInternal::List); void slotEntryDetailsLoaded(const KNSCore::EntryInternal &entry); void slotPreviewLoaded(const KNSCore::EntryInternal &entry, KNSCore::EntryInternal::PreviewType type); void slotSearchTimerExpired(); void slotEntryChanged(const KNSCore::EntryInternal &entry); void slotInstallationFinished(); void slotInstallationFailed(const QString &message); void downloadLinkLoaded(const KNSCore::EntryInternal &entry); void providerJobStarted(KJob *); private: /** * load providers from the providersurl in the knsrc file * creates providers based on their type and adds them to the list of providers */ void loadProviders(); /** Add a provider and connect it to the right slots */ void addProvider(QSharedPointer provider); void updateStatus(); void doRequest(); //FIXME KF6: move all of this in EnginePrivate // handle installation of entries Installation *m_installation; // read/write cache of entries QSharedPointer m_cache; QTimer *m_searchTimer; // The url of the file containing information about content providers QString m_providerFileUrl; // Categories from knsrc file QStringList m_categories; QHash > m_providers; QString m_adoptionCommand; // the current request from providers Provider::SearchRequest m_currentRequest; EnginePrivate * const d; // the page that is currently displayed, so it is not requested repeatedly int m_currentPage; // when requesting entries from a provider, how many to ask for int m_pageSize; int m_numDataJobs; int m_numPictureJobs; int m_numInstallJobs; // If the provider is ready to be used bool m_initialized; Q_DISABLE_COPY(Engine) }; } #endif diff --git a/src/core/entryinternal.cpp b/src/core/entryinternal.cpp index 8075ece7..882754f2 100644 --- a/src/core/entryinternal.cpp +++ b/src/core/entryinternal.cpp @@ -1,775 +1,775 @@ /* This file is part of KNewStuff2. Copyright (c) 2002 Cornelius Schumacher Copyright (c) 2003 - 2007 Josef Spillner Copyright (C) 2009 Frederik Gladhorn 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) any later version. 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 "entryinternal.h" #include #include #include #include #include "xmlloader.h" #include "../entry_p.h" // For Entry::Status ONLY! using namespace KNSCore; class EntryInternal::Private : public QSharedData { public: Private() : mReleaseDate(QDate::currentDate()) , mRating(0) , mNumberOfComments(0) , mDownloadCount(0) , mNumberFans(0) , mNumberKnowledgebaseEntries(0) , mStatus(KNS3::Entry::Invalid) , mSource(EntryInternal::Online) { qRegisterMetaType(); } bool operator==(const Private &other) const { return mUniqueId == other.mUniqueId && mProviderId == other.mProviderId; } QString mUniqueId; QString mName; QUrl mHomepage; QString mCategory; QString mLicense; QString mVersion; QDate mReleaseDate; // Version and date if a newer version is found (updateable) QString mUpdateVersion; QDate mUpdateReleaseDate; Author mAuthor; int mRating; int mNumberOfComments; int mDownloadCount; int mNumberFans; int mNumberKnowledgebaseEntries; QString mKnowledgebaseLink; QString mSummary; QString mShortSummary; QString mChangelog; QString mPayload; QStringList mInstalledFiles; QString mProviderId; QStringList mUnInstalledFiles; QString mDonationLink; QStringList mTags; QString mChecksum; QString mSignature; KNS3::Entry::Status mStatus; EntryInternal::Source mSource; QString mPreviewUrl[6]; QImage mPreviewImage[6]; QList mDownloadLinkInformationList; }; EntryInternal::EntryInternal() : d(new Private) { } EntryInternal::EntryInternal(const EntryInternal &other) : d(other.d) { } EntryInternal &EntryInternal::operator=(const EntryInternal &other) { d = other.d; return *this; } bool EntryInternal::operator<(const KNSCore::EntryInternal &other) const { return d->mUniqueId < other.d->mUniqueId; } bool EntryInternal::operator==(const KNSCore::EntryInternal &other) const { return d->mUniqueId == other.d->mUniqueId && d->mProviderId == other.d->mProviderId; } EntryInternal::~EntryInternal() { } bool EntryInternal::isValid() const { return !d->mUniqueId.isEmpty(); } QString EntryInternal::name() const { return d->mName; } void EntryInternal::setName(const QString &name) { d->mName = name; } QString EntryInternal::uniqueId() const { return d->mUniqueId; } void EntryInternal::setUniqueId(const QString &id) { d->mUniqueId = id; } QString EntryInternal::providerId() const { return d->mProviderId; } void EntryInternal::setProviderId(const QString &id) { d->mProviderId = id; } QStringList KNSCore::EntryInternal::tags() const { return d->mTags; } void KNSCore::EntryInternal::setTags(const QStringList &tags) { d->mTags = tags; } QString EntryInternal::category() const { return d->mCategory; } void EntryInternal::setCategory(const QString &category) { d->mCategory = category; } QUrl EntryInternal::homepage() const { return d->mHomepage; } void EntryInternal::setHomepage(const QUrl &page) { d->mHomepage = page; } Author EntryInternal::author() const { return d->mAuthor; } void EntryInternal::setAuthor(const KNSCore::Author &author) { d->mAuthor = author; } QString EntryInternal::license() const { return d->mLicense; } void EntryInternal::setLicense(const QString &license) { d->mLicense = license; } QString EntryInternal::summary() const { return d->mSummary; } void EntryInternal::setSummary(const QString &summary) { d->mSummary = summary; } QString EntryInternal::shortSummary() const { return d->mShortSummary; } void EntryInternal::setShortSummary(const QString &summary) { d->mShortSummary = summary; } void EntryInternal::setChangelog(const QString &changelog) { d->mChangelog = changelog; } QString EntryInternal::changelog() const { return d->mChangelog; } QString EntryInternal::version() const { return d->mVersion; } void EntryInternal::setVersion(const QString &version) { d->mVersion = version; } QDate EntryInternal::releaseDate() const { return d->mReleaseDate; } void EntryInternal::setReleaseDate(const QDate &releasedate) { d->mReleaseDate = releasedate; } QString EntryInternal::payload() const { return d->mPayload; } void EntryInternal::setPayload(const QString &url) { d->mPayload = url; } QDate EntryInternal::updateReleaseDate() const { return d->mUpdateReleaseDate; } void EntryInternal::setUpdateReleaseDate(const QDate &releasedate) { d->mUpdateReleaseDate = releasedate; } QString EntryInternal::updateVersion() const { return d->mUpdateVersion; } void EntryInternal::setUpdateVersion(const QString &version) { d->mUpdateVersion = version; } QString EntryInternal::previewUrl(PreviewType type) const { return d->mPreviewUrl[type]; } void EntryInternal::setPreviewUrl(const QString &url, PreviewType type) { d->mPreviewUrl[type] = url; } QImage EntryInternal::previewImage(PreviewType type) const { return d->mPreviewImage[type]; } void EntryInternal::setPreviewImage(const QImage &image, PreviewType type) { d->mPreviewImage[type] = image; } int EntryInternal::rating() const { return d->mRating; } void EntryInternal::setRating(int rating) { d->mRating = rating; } int EntryInternal::numberOfComments() const { - return d->mRating; + return d->mNumberOfComments; } void EntryInternal::setNumberOfComments (int comments) { d->mNumberOfComments = comments; } int EntryInternal::downloadCount() const { return d->mDownloadCount; } void EntryInternal::setDownloadCount(int downloads) { d->mDownloadCount = downloads; } int EntryInternal::numberFans() const { return d->mNumberFans; } void EntryInternal::setNumberFans(int fans) { d->mNumberFans = fans; } QString EntryInternal::donationLink() const { return d->mDonationLink; } void EntryInternal::setDonationLink(const QString &link) { d->mDonationLink = link; } int EntryInternal::numberKnowledgebaseEntries() const { return d->mNumberKnowledgebaseEntries; } void EntryInternal::setNumberKnowledgebaseEntries(int num) { d->mNumberKnowledgebaseEntries = num; } QString EntryInternal::knowledgebaseLink() const { return d->mKnowledgebaseLink; } void EntryInternal::setKnowledgebaseLink(const QString &link) { d->mKnowledgebaseLink = link; } /* QString EntryInternal::checksum() const { return d->mChecksum; } QString EntryInternal::signature() const { return d->mSignature; } */ EntryInternal::Source EntryInternal::source() const { return d->mSource; } void EntryInternal::setSource(Source source) { d->mSource = source; } KNS3::Entry::Status EntryInternal::status() const { return d->mStatus; } void EntryInternal::setStatus(KNS3::Entry::Status status) { d->mStatus = status; } void KNSCore::EntryInternal::setInstalledFiles(const QStringList &files) { d->mInstalledFiles = files; } QStringList KNSCore::EntryInternal::installedFiles() const { return d->mInstalledFiles; } void KNSCore::EntryInternal::setUnInstalledFiles(const QStringList &files) { d->mUnInstalledFiles = files; } QStringList KNSCore::EntryInternal::uninstalledFiles() const { return d->mUnInstalledFiles; } int KNSCore::EntryInternal::downloadLinkCount() const { return d->mDownloadLinkInformationList.size(); } QList KNSCore::EntryInternal::downloadLinkInformationList() const { return d->mDownloadLinkInformationList; } void KNSCore::EntryInternal::appendDownloadLinkInformation(const KNSCore::EntryInternal::DownloadLinkInformation &info) { d->mDownloadLinkInformationList.append(info); } void EntryInternal::clearDownloadLinkInformation() { d->mDownloadLinkInformationList.clear(); } static QXmlStreamReader::TokenType readNextSkipComments(QXmlStreamReader* xml) { do { xml->readNext(); } while(xml->tokenType() == QXmlStreamReader::Comment || (xml->tokenType() == QXmlStreamReader::Characters && xml->text().trimmed().isEmpty())); return xml->tokenType(); } static QStringRef readText(QXmlStreamReader* xml) { Q_ASSERT(xml->tokenType() == QXmlStreamReader::StartElement); QStringRef ret; const auto token = readNextSkipComments(xml); if (token == QXmlStreamReader::Characters) { ret = xml->text(); } return ret; } static QString readStringTrimmed(QXmlStreamReader* xml) { Q_ASSERT(xml->tokenType() == QXmlStreamReader::StartElement); QString ret = readText(xml).trimmed().toString(); if (xml->tokenType() == QXmlStreamReader::Characters) readNextSkipComments(xml); Q_ASSERT(xml->tokenType() == QXmlStreamReader::EndElement); return ret; } static int readInt(QXmlStreamReader* xml) { Q_ASSERT(xml->tokenType() == QXmlStreamReader::StartElement); int ret = readText(xml).toInt(); xml->readNext(); Q_ASSERT(xml->tokenType() == QXmlStreamReader::EndElement); return ret; } bool KNSCore::EntryInternal::setEntryXML(QXmlStreamReader& reader) { if (reader.name() != QLatin1String("stuff")) { qCWarning(KNEWSTUFFCORE) << "Parsing Entry from invalid XML. Reader tag name was expected to be \"stuff\", but was found as:" << reader.name(); return false; } d->mCategory = reader.attributes().value(QStringLiteral("category")).toString(); while (!reader.atEnd()) { const auto token = readNextSkipComments(&reader); if (token == QXmlStreamReader::EndElement) break; else if (token != QXmlStreamReader::StartElement) continue; if (reader.name() == QLatin1String("name")) { // TODO maybe do something with the language attribute? QString lang = e.attribute("lang"); d->mName = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("author")) { const auto email = reader.attributes().value(QStringLiteral("email")); const auto jabber = reader.attributes().value(QStringLiteral("im")); const auto homepage = reader.attributes().value(QStringLiteral("homepage")); d->mAuthor.setName(readStringTrimmed(&reader)); d->mAuthor.setEmail(email.toString()); d->mAuthor.setJabber(jabber.toString()); d->mAuthor.setHomepage(homepage.toString()); } else if (reader.name() == QLatin1String("providerid")) { d->mProviderId = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("homepage")) { d->mHomepage = QUrl(reader.readElementText(QXmlStreamReader::SkipChildElements)); } else if (reader.name() == QLatin1String("licence")) { // krazy:exclude=spelling d->mLicense = readStringTrimmed(&reader); } else if (reader.name() == QLatin1String("summary")) { d->mSummary = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("changelog")) { d->mChangelog = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("version")) { d->mVersion = readStringTrimmed(&reader); } else if (reader.name() == QLatin1String("releasedate")) { d->mReleaseDate = QDate::fromString(readStringTrimmed(&reader), Qt::ISODate); } else if (reader.name() == QLatin1String("preview")) { // TODO support for all 6 image links d->mPreviewUrl[PreviewSmall1] = readStringTrimmed(&reader); } else if (reader.name() == QLatin1String("previewBig")) { d->mPreviewUrl[PreviewBig1] = readStringTrimmed(&reader); } else if (reader.name() == QLatin1String("payload")) { d->mPayload = readStringTrimmed(&reader); } else if (reader.name() == QLatin1String("rating")) { d->mRating = readInt(&reader); } else if (reader.name() == QLatin1String("downloads")) { d->mDownloadCount = readInt(&reader); } else if (reader.name() == QLatin1String("category")) { d->mCategory = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("signature")) { d->mSignature = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("checksum")) { d->mChecksum = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("installedfile")) { d->mInstalledFiles.append(reader.readElementText(QXmlStreamReader::SkipChildElements)); } else if (reader.name() == QLatin1String("id")) { d->mUniqueId = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("tags")) { d->mTags = reader.readElementText(QXmlStreamReader::SkipChildElements).split(QLatin1Char(',')); } else if (reader.name() == QLatin1String("status")) { const auto statusText = readText(&reader); if (statusText == QLatin1String("installed")) { qCDebug(KNEWSTUFFCORE) << "Found an installed entry in registry"; d->mStatus = KNS3::Entry::Installed; } else if (statusText == QLatin1String("updateable")) { d->mStatus = KNS3::Entry::Updateable; } if (reader.tokenType() == QXmlStreamReader::Characters) readNextSkipComments(&reader); } Q_ASSERT_X(reader.tokenType() == QXmlStreamReader::EndElement, Q_FUNC_INFO, QStringLiteral("token name was %1 and the type was %2").arg(reader.name().toString()).arg(reader.tokenString()).toLocal8Bit().data()); } // Validation if (d->mName.isEmpty()) { qWarning() << "Entry: no name given"; return false; } if (d->mUniqueId.isEmpty()) { if (!d->mPayload.isEmpty()) { d->mUniqueId = d->mPayload; } else { d->mUniqueId = d->mName; } } if (d->mPayload.isEmpty()) { qWarning() << "Entry: no payload URL given for: " << d->mName << " - " << d->mUniqueId; return false; } return true; } bool KNSCore::EntryInternal::setEntryXML(const QDomElement &xmldata) { if (xmldata.tagName() != QLatin1String("stuff")) { qWarning() << "Parsing Entry from invalid XML"; return false; } d->mCategory = xmldata.attribute(QStringLiteral("category")); QDomNode n; for (n = xmldata.firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement e = n.toElement(); if (e.tagName() == QLatin1String("name")) { // TODO maybe do something with the language attribute? QString lang = e.attribute("lang"); d->mName = e.text().trimmed(); } else if (e.tagName() == QLatin1String("author")) { QString email = e.attribute(QStringLiteral("email")); QString jabber = e.attribute(QStringLiteral("im")); QString homepage = e.attribute(QStringLiteral("homepage")); d->mAuthor.setName(e.text().trimmed()); d->mAuthor.setEmail(email); d->mAuthor.setJabber(jabber); d->mAuthor.setHomepage(homepage); } else if (e.tagName() == QLatin1String("providerid")) { d->mProviderId = e.text(); } else if (e.tagName() == QLatin1String("homepage")) { d->mHomepage = QUrl(e.text()); } else if (e.tagName() == QLatin1String("licence")) { // krazy:exclude=spelling d->mLicense = e.text().trimmed(); } else if (e.tagName() == QLatin1String("summary")) { d->mSummary = e.text(); } else if (e.tagName() == QLatin1String("changelog")) { d->mChangelog = e.text(); } else if (e.tagName() == QLatin1String("version")) { d->mVersion = e.text().trimmed(); } else if (e.tagName() == QLatin1String("releasedate")) { d->mReleaseDate = QDate::fromString(e.text().trimmed(), Qt::ISODate); } else if (e.tagName() == QLatin1String("preview")) { // TODO support for all 6 image links d->mPreviewUrl[PreviewSmall1] = e.text().trimmed(); } else if (e.tagName() == QLatin1String("previewBig")) { d->mPreviewUrl[PreviewBig1] = e.text().trimmed(); } else if (e.tagName() == QLatin1String("payload")) { d->mPayload = e.text().trimmed(); } else if (e.tagName() == QLatin1String("rating")) { d->mRating = e.text().toInt(); } else if (e.tagName() == QLatin1String("downloads")) { d->mDownloadCount = e.text().toInt(); } else if (e.tagName() == QLatin1String("category")) { d->mCategory = e.text(); } else if (e.tagName() == QLatin1String("signature")) { d->mSignature = e.text(); } else if (e.tagName() == QLatin1String("checksum")) { d->mChecksum = e.text(); } else if (e.tagName() == QLatin1String("installedfile")) { d->mInstalledFiles.append(e.text()); } else if (e.tagName() == QLatin1String("id")) { d->mUniqueId = e.text(); } else if (e.tagName() == QLatin1String("tags")) { d->mTags = e.text().split(QLatin1Char(',')); } else if (e.tagName() == QLatin1String("status")) { QString statusText = e.text(); if (statusText == QLatin1String("installed")) { qCDebug(KNEWSTUFFCORE) << "Found an installed entry in registry"; d->mStatus = KNS3::Entry::Installed; } else if (statusText == QLatin1String("updateable")) { d->mStatus = KNS3::Entry::Updateable; } } } // Validation if (d->mName.isEmpty()) { qWarning() << "Entry: no name given"; return false; } if (d->mUniqueId.isEmpty()) { if (!d->mPayload.isEmpty()) { d->mUniqueId = d->mPayload; } else { d->mUniqueId = d->mName; } } if (d->mPayload.isEmpty()) { qWarning() << "Entry: no payload URL given for: " << d->mName << " - " << d->mUniqueId; return false; } return true; } /** * get the xml string for the entry */ QDomElement KNSCore::EntryInternal::entryXML() const { Q_ASSERT(!d->mUniqueId.isEmpty()); Q_ASSERT(!d->mProviderId.isEmpty()); QDomDocument doc; QDomElement el = doc.createElement(QStringLiteral("stuff")); el.setAttribute(QStringLiteral("category"), d->mCategory); QString name = d->mName; QDomElement e; e = addElement(doc, el, QStringLiteral("name"), name); // todo: add language attribute (void)addElement(doc, el, QStringLiteral("providerid"), d->mProviderId); QDomElement author = addElement(doc, el, QStringLiteral("author"), d->mAuthor.name()); if (!d->mAuthor.email().isEmpty()) { author.setAttribute(QStringLiteral("email"), d->mAuthor.email()); } if (!d->mAuthor.homepage().isEmpty()) { author.setAttribute(QStringLiteral("homepage"), d->mAuthor.homepage()); } if (!d->mAuthor.jabber().isEmpty()) { author.setAttribute(QStringLiteral("im"), d->mAuthor.jabber()); } // FIXME: 'jabber' or 'im'? consult with kopete guys... addElement(doc, el, QStringLiteral("homepage"), d->mHomepage.url()); (void)addElement(doc, el, QStringLiteral("licence"), d->mLicense); // krazy:exclude=spelling (void)addElement(doc, el, QStringLiteral("version"), d->mVersion); if ((d->mRating > 0) || (d->mDownloadCount > 0)) { (void)addElement(doc, el, QStringLiteral("rating"), QString::number(d->mRating)); (void)addElement(doc, el, QStringLiteral("downloads"), QString::number(d->mDownloadCount)); } if (!d->mSignature.isEmpty()) { (void)addElement(doc, el, QStringLiteral("signature"), d->mSignature); } if (!d->mChecksum.isEmpty()) { (void)addElement(doc, el, QStringLiteral("checksum"), d->mChecksum); } for (const QString &file : qAsConst(d->mInstalledFiles)) { (void)addElement(doc, el, QStringLiteral("installedfile"), file); } if (!d->mUniqueId.isEmpty()) { addElement(doc, el, QStringLiteral("id"), d->mUniqueId); } (void)addElement(doc, el, QStringLiteral("releasedate"), d->mReleaseDate.toString(Qt::ISODate)); e = addElement(doc, el, QStringLiteral("summary"), d->mSummary); e = addElement(doc, el, QStringLiteral("changelog"), d->mChangelog); e = addElement(doc, el, QStringLiteral("preview"), d->mPreviewUrl[PreviewSmall1]); e = addElement(doc, el, QStringLiteral("previewBig"), d->mPreviewUrl[PreviewBig1]); e = addElement(doc, el, QStringLiteral("payload"), d->mPayload); e = addElement(doc, el, QStringLiteral("tags"), d->mTags.join(QLatin1Char(','))); if (d->mStatus == KNS3::Entry::Installed) { (void)addElement(doc, el, QStringLiteral("status"), QStringLiteral("installed")); } if (d->mStatus == KNS3::Entry::Updateable) { (void)addElement(doc, el, QStringLiteral("status"), QStringLiteral("updateable")); } return el; } KNSCore::EntryInternal EntryInternal::fromEntry(const KNS3::Entry &entry) { return entry.d->e; } QString KNSCore::replaceBBCode(const QString &unformattedText) { QString text(unformattedText); text.replace(QLatin1String("[b]"), QLatin1String("")); text.replace(QLatin1String("[/b]"), QLatin1String("")); text.replace(QLatin1String("[i]"), QLatin1String("")); text.replace(QLatin1String("[/i]"), QLatin1String("")); text.replace(QLatin1String("[u]"), QLatin1String("")); text.replace(QLatin1String("[/u]"), QLatin1String("")); text.replace(QLatin1String("\\\""), QLatin1String("\"")); text.replace(QLatin1String("\\\'"), QLatin1String("\'")); text.replace(QLatin1String("[li]"), QLatin1String("* ")); // TODO: better replacement for list elements? text.remove(QStringLiteral("[/li]")); text.remove(QStringLiteral("[url]")); text.remove(QStringLiteral("[/url]")); return text; } diff --git a/src/core/installation.cpp b/src/core/installation.cpp index 6cab27c9..37a9201b 100644 --- a/src/core/installation.cpp +++ b/src/core/installation.cpp @@ -1,671 +1,671 @@ /* This file is part of KNewStuff2. Copyright (c) 2007 Josef Spillner Copyright (C) 2009 Frederik Gladhorn 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) any later version. 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 "installation.h" #include #include #include #include #include #include #include "qmimedatabase.h" #include "karchive.h" #include "kzip.h" #include "ktar.h" #include "krandom.h" #include "kshell.h" #include #include "klocalizedstring.h" #include #include "jobs/filecopyjob.h" #include "question.h" #ifdef Q_OS_WIN #include #include #endif using namespace KNSCore; Installation::Installation(QObject *parent) : QObject(parent) , checksumPolicy(Installation::CheckIfPossible) , signaturePolicy(Installation::CheckIfPossible) , scope(Installation::ScopeUser) , customName(false) , acceptHtml(false) { } bool Installation::readConfig(const KConfigGroup &group) { // FIXME: add support for several categories later on // FIXME: read out only when actually installing as a performance improvement? QString uncompresssetting = group.readEntry("Uncompress", QStringLiteral("never")); // support old value of true as equivalent of always if (uncompresssetting == QLatin1String("subdir")) { uncompresssetting = QStringLiteral("subdir"); } else if (uncompresssetting == QLatin1String("true")) { uncompresssetting = QStringLiteral("always"); } if (uncompresssetting != QLatin1String("always") && uncompresssetting != QLatin1String("archive") && uncompresssetting != QLatin1String("never") && uncompresssetting != QLatin1String("subdir")) { qCCritical(KNEWSTUFFCORE) << "invalid Uncompress setting chosen, must be one of: subdir, always, archive, or never" << endl; return false; } uncompression = uncompresssetting; postInstallationCommand = group.readEntry("InstallationCommand", QString()); uninstallCommand = group.readEntry("UninstallCommand", QString()); standardResourceDirectory = group.readEntry("StandardResource", QString()); targetDirectory = group.readEntry("TargetDir", QString()); xdgTargetDirectory = group.readEntry("XdgTargetDir", QString()); // Provide some compatibility if (standardResourceDirectory == QLatin1String("wallpaper")) { xdgTargetDirectory = QStringLiteral("wallpapers"); } // also, ensure wallpapers are decompressed into subdirectories // this ensures that wallpapers with multiple resolutions continue to function // as expected if (xdgTargetDirectory == QLatin1String("wallpapers")) { uncompression = QStringLiteral("subdir"); } installPath = group.readEntry("InstallPath", QString()); absoluteInstallPath = group.readEntry("AbsoluteInstallPath", QString()); customName = group.readEntry("CustomName", false); acceptHtml = group.readEntry("AcceptHtmlDownloads", false); if (standardResourceDirectory.isEmpty() && targetDirectory.isEmpty() && xdgTargetDirectory.isEmpty() && installPath.isEmpty() && absoluteInstallPath.isEmpty()) { qCCritical(KNEWSTUFFCORE) << "No installation target set"; return false; } QString checksumpolicy = group.readEntry("ChecksumPolicy", QString()); if (!checksumpolicy.isEmpty()) { if (checksumpolicy == QLatin1String("never")) { checksumPolicy = Installation::CheckNever; } else if (checksumpolicy == QLatin1String("ifpossible")) { checksumPolicy = Installation::CheckIfPossible; } else if (checksumpolicy == QLatin1String("always")) { checksumPolicy = Installation::CheckAlways; } else { qCCritical(KNEWSTUFFCORE) << QStringLiteral("The checksum policy '") + checksumpolicy + QStringLiteral("' is unknown.") << endl; return false; } } QString signaturepolicy = group.readEntry("SignaturePolicy", QString()); if (!signaturepolicy.isEmpty()) { if (signaturepolicy == QLatin1String("never")) { signaturePolicy = Installation::CheckNever; } else if (signaturepolicy == QLatin1String("ifpossible")) { signaturePolicy = Installation::CheckIfPossible; } else if (signaturepolicy == QLatin1String("always")) { signaturePolicy = Installation::CheckAlways; } else { qCCritical(KNEWSTUFFCORE) << QStringLiteral("The signature policy '") + signaturepolicy + QStringLiteral("' is unknown.") << endl; return false; } } QString scopeString = group.readEntry("Scope", QString()); if (!scopeString.isEmpty()) { if (scopeString == QLatin1String("user")) { scope = ScopeUser; } else if (scopeString == QLatin1String("system")) { scope = ScopeSystem; } else { qCCritical(KNEWSTUFFCORE) << QStringLiteral("The scope '") + scopeString + QStringLiteral("' is unknown.") << endl; return false; } if (scope == ScopeSystem) { if (!installPath.isEmpty()) { qCCritical(KNEWSTUFFCORE) << "System installation cannot be mixed with InstallPath." << endl; return false; } } } return true; } bool Installation::isRemote() const { if (!installPath.isEmpty()) { return false; } if (!targetDirectory.isEmpty()) { return false; } if (!xdgTargetDirectory.isEmpty()) { return false; } if (!absoluteInstallPath.isEmpty()) { return false; } if (!standardResourceDirectory.isEmpty()) { return false; } return true; } void Installation::install(const EntryInternal& entry) { downloadPayload(entry); } void Installation::downloadPayload(const KNSCore::EntryInternal &entry) { if (!entry.isValid()) { emit signalInstallationFailed(i18n("Invalid item.")); return; } QUrl source = QUrl(entry.payload()); if (!source.isValid()) { qCCritical(KNEWSTUFFCORE) << "The entry doesn't have a payload." << endl; emit signalInstallationFailed(i18n("Download of item failed: no download URL for \"%1\".", entry.name())); return; } // FIXME no clue what this is supposed to do if (isRemote()) { // Remote resource qCDebug(KNEWSTUFFCORE) << "Relaying remote payload '" << source << "'"; install(entry, source.toDisplayString(QUrl::PreferLocalFile)); emit signalPayloadLoaded(source); // FIXME: we still need registration for eventual deletion return; } QString fileName(source.fileName()); QTemporaryFile tempFile(QDir::tempPath() + QStringLiteral("/XXXXXX-") + fileName); if (!tempFile.open()) { return; // ERROR } QUrl destination = QUrl::fromLocalFile(tempFile.fileName()); qCDebug(KNEWSTUFFCORE) << "Downloading payload" << source << "to" << destination; // FIXME: check for validity FileCopyJob *job = FileCopyJob::file_copy(source, destination, -1, JobFlag::Overwrite | JobFlag::HideProgressInfo); connect(job, &KJob::result, this, &Installation::slotPayloadResult); entry_jobs[job] = entry; } void Installation::slotPayloadResult(KJob *job) { // for some reason this slot is getting called 3 times on one job error if (entry_jobs.contains(job)) { EntryInternal entry = entry_jobs[job]; entry_jobs.remove(job); if (job->error()) { emit signalInstallationFailed(i18n("Download of \"%1\" failed, error: %2", entry.name(), job->errorString())); } else { FileCopyJob *fcjob = static_cast(job); // check if the app likes html files - disabled by default as too many bad links have been submitted to opendesktop.org if (!acceptHtml) { QMimeDatabase db; QMimeType mimeType = db.mimeTypeForFile(fcjob->destUrl().toLocalFile()); if (mimeType.inherits(QStringLiteral("text/html")) || mimeType.inherits(QStringLiteral("application/x-php"))) { Question question; question.setQuestion(i18n("The downloaded file is a html file. This indicates a link to a website instead of the actual download. Would you like to open the site with a browser instead?")); question.setTitle(i18n("Possibly bad download link")); if(question.ask() == Question::YesResponse) { QDesktopServices::openUrl(fcjob->srcUrl()); emit signalInstallationFailed(i18n("Downloaded file was a HTML file. Opened in browser.")); entry.setStatus(KNS3::Entry::Invalid); emit signalEntryChanged(entry); return; } } } emit signalPayloadLoaded(fcjob->destUrl()); install(entry, fcjob->destUrl().toLocalFile()); } } } void KNSCore::Installation::install(KNSCore::EntryInternal entry, const QString& downloadedFile) { qCDebug(KNEWSTUFFCORE) << "Install: " << entry.name() << " from " << downloadedFile; if (entry.payload().isEmpty()) { qCDebug(KNEWSTUFFCORE) << "No payload associated with: " << entry.name(); return; } // this means check sum comparison and signature verification // signature verification might take a long time - make async?! /* if (checksumPolicy() != Installation::CheckNever) { if (entry.checksum().isEmpty()) { if (checksumPolicy() == Installation::CheckIfPossible) { qCDebug(KNEWSTUFFCORE) << "Skip checksum verification"; } else { qCCritical(KNEWSTUFFCORE) << "Checksum verification not possible" << endl; return false; } } else { qCDebug(KNEWSTUFFCORE) << "Verify checksum..."; } } if (signaturePolicy() != Installation::CheckNever) { if (entry.signature().isEmpty()) { if (signaturePolicy() == Installation::CheckIfPossible) { qCDebug(KNEWSTUFFCORE) << "Skip signature verification"; } else { qCCritical(KNEWSTUFFCORE) << "Signature verification not possible" << endl; return false; } } else { qCDebug(KNEWSTUFFCORE) << "Verify signature..."; } } */ QString targetPath = targetInstallationPath(); QStringList installedFiles = installDownloadedFileAndUncompress(entry, downloadedFile, targetPath); if (installedFiles.isEmpty()) { if (entry.status() == KNS3::Entry::Installing) { entry.setStatus(KNS3::Entry::Downloadable); } else if (entry.status() == KNS3::Entry::Updating) { entry.setStatus(KNS3::Entry::Updateable); } emit signalEntryChanged(entry); emit signalInstallationFailed(i18n("Could not install \"%1\": file not found.", entry.name())); return; } entry.setInstalledFiles(installedFiles); auto installationFinished = [this, entry]() { EntryInternal newentry = entry; // update version and release date to the new ones if (newentry.status() == KNS3::Entry::Updating) { if (!newentry.updateVersion().isEmpty()) { newentry.setVersion(newentry.updateVersion()); } if (newentry.updateReleaseDate().isValid()) { newentry.setReleaseDate(newentry.updateReleaseDate()); } } newentry.setStatus(KNS3::Entry::Installed); emit signalEntryChanged(newentry); emit signalInstallationFinished(); }; if (!postInstallationCommand.isEmpty()) { QProcess* p = runPostInstallationCommand(installedFiles.size() == 1 ? installedFiles.first() : targetPath); connect(p, static_cast(&QProcess::finished), this, installationFinished); } else { installationFinished(); } } QString Installation::targetInstallationPath() const { QString installdir; if (!isRemote()) { // installdir is the target directory // installpath also contains the file name if it's a single file, otherwise equal to installdir int pathcounter = 0; #if 0 // not available in KF5 if (!standardResourceDirectory.isEmpty()) { if (scope == ScopeUser) { installdir = KStandardDirs::locateLocal(standardResourceDirectory.toUtf8(), "/"); } else { // system scope installdir = KStandardDirs::installPath(standardResourceDirectory.toUtf8()); } pathcounter++; } #endif /* this is a partial reimplementation of the above, it won't ensure a perfect 1:1 porting, but will make many kde4 ksnsrc files work out of the box*/ //wallpaper is already managed in the case of !xdgTargetDirectory.isEmpty() if (!standardResourceDirectory.isEmpty() && standardResourceDirectory != QLatin1String("wallpaper")) { QStandardPaths::StandardLocation location = QStandardPaths::TempLocation; //crude translation KStandardDirs names -> QStandardPaths enum if (standardResourceDirectory == QLatin1String("tmp")) { location = QStandardPaths::TempLocation; } else if (standardResourceDirectory == QLatin1String("config")) { location = QStandardPaths::ConfigLocation; } if (scope == ScopeUser) { installdir = QStandardPaths::writableLocation(location); } else { // system scope installdir = QStandardPaths::standardLocations(location).constLast(); } pathcounter++; } if (!targetDirectory.isEmpty() && targetDirectory != QLatin1String("/")) { if (scope == ScopeUser) { installdir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + targetDirectory + QLatin1Char('/'); } else { // system scope installdir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, targetDirectory, QStandardPaths::LocateDirectory) + QLatin1Char('/'); } pathcounter++; } if (!xdgTargetDirectory.isEmpty() && xdgTargetDirectory != QLatin1String("/")) { installdir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + xdgTargetDirectory + QLatin1Char('/'); pathcounter++; } if (!installPath.isEmpty()) { #if defined(Q_OS_WIN) WCHAR wPath[MAX_PATH + 1]; if (SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, wPath) == S_OK) { installdir = QString::fromUtf16((const ushort *) wPath) + QLatin1Char('/') + installPath + QLatin1Char('/'); } else { installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/'); } #else installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/'); #endif pathcounter++; } if (!absoluteInstallPath.isEmpty()) { installdir = absoluteInstallPath + QLatin1Char('/'); pathcounter++; } if (pathcounter != 1) { qCCritical(KNEWSTUFFCORE) << "Wrong number of installation directories given." << endl; return QString(); } qCDebug(KNEWSTUFFCORE) << "installdir: " << installdir; // create the dir if it doesn't exist (QStandardPaths doesn't create it, unlike KStandardDirs!) QDir().mkpath(installdir); } return installdir; } QStringList Installation::installDownloadedFileAndUncompress(const KNSCore::EntryInternal &entry, const QString &payloadfile, const QString installdir) { // Collect all files that were installed QStringList installedFiles; if (!isRemote()) { bool isarchive = true; // respect the uncompress flag in the knsrc if (uncompression == QLatin1String("always") || uncompression == QLatin1String("archive") || uncompression == QLatin1String("subdir")) { // this is weird but a decompression is not a single name, so take the path instead QMimeDatabase db; QMimeType mimeType = db.mimeTypeForFile(payloadfile); qCDebug(KNEWSTUFFCORE) << "Postinstallation: uncompress the file"; // FIXME: check for overwriting, malicious archive entries (../foo) etc. // FIXME: KArchive should provide "safe mode" for this! QScopedPointer archive; if (mimeType.inherits(QStringLiteral("application/zip"))) { archive.reset(new KZip(payloadfile)); } else if (mimeType.inherits(QStringLiteral("application/tar")) || mimeType.inherits(QStringLiteral("application/x-gzip")) || mimeType.inherits(QStringLiteral("application/x-bzip")) || mimeType.inherits(QStringLiteral("application/x-lzma")) || mimeType.inherits(QStringLiteral("application/x-xz")) || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || mimeType.inherits(QStringLiteral("application/x-compressed-tar"))) { archive.reset(new KTar(payloadfile)); } else { qCCritical(KNEWSTUFFCORE) << "Could not determine type of archive file '" << payloadfile << "'"; if (uncompression == QLatin1String("always")) { return QStringList(); } isarchive = false; } if (isarchive) { bool success = archive->open(QIODevice::ReadOnly); if (!success) { qCCritical(KNEWSTUFFCORE) << "Cannot open archive file '" << payloadfile << "'"; if (uncompression == QLatin1String("always")) { return QStringList(); } // otherwise, just copy the file isarchive = false; } if (isarchive) { const KArchiveDirectory *dir = archive->directory(); //if there is more than an item in the file, and we are requested to do so //put contents in a subdirectory with the same name as the file QString installpath; if (uncompression == QLatin1String("subdir") && dir->entries().count() > 1) { installpath = installdir + QLatin1Char('/') + QFileInfo(archive->fileName()).baseName(); } else { installpath = installdir; } if (dir->copyTo(installpath)) { installedFiles << archiveEntries(installpath, dir); installedFiles << installpath + QLatin1Char('/'); } else qCWarning(KNEWSTUFFCORE) << "could not install" << entry.name() << "to" << installpath; archive->close(); QFile::remove(payloadfile); } } } qCDebug(KNEWSTUFFCORE) << "isarchive: " << isarchive; //some wallpapers are compressed, some aren't if ((!isarchive && standardResourceDirectory == QLatin1String("wallpaper")) || (uncompression == QLatin1String("never") || (uncompression == QLatin1String("archive") && !isarchive))) { // no decompress but move to target /// @todo when using KIO::get the http header can be accessed and it contains a real file name. // FIXME: make naming convention configurable through *.knsrc? e.g. for kde-look.org image names QUrl source = QUrl(entry.payload()); qCDebug(KNEWSTUFFCORE) << "installing non-archive from " << source.url(); QString installfile; QString ext = source.fileName().section(QLatin1Char('.'), -1); if (customName) { installfile = entry.name(); installfile += QLatin1Char('-') + entry.version(); if (!ext.isEmpty()) { installfile += QLatin1Char('.') + ext; } } else { // TODO HACK This is a hack, the correct way of fixing it would be doing the KIO::get // and using the http headers if they exist to get the file name, but as discussed in // Randa this is not going to happen anytime soon (if ever) so go with the hack if (source.url().startsWith(QLatin1String("http://newstuff.kde.org/cgi-bin/hotstuff-access?file="))) { installfile = QUrlQuery(source).queryItemValue(QStringLiteral("file")); int lastSlash = installfile.lastIndexOf(QLatin1Char('/')); if (lastSlash >= 0) { installfile = installfile.mid(lastSlash); } } if (installfile.isEmpty()) { installfile = source.fileName(); } } QString installpath = installdir + QLatin1Char('/') + installfile; qCDebug(KNEWSTUFFCORE) << "Install to file " << installpath; // FIXME: copy goes here (including overwrite checking) // FIXME: what must be done now is to update the cache *again* // in order to set the new payload filename (on root tag only) // - this might or might not need to take uncompression into account // FIXME: for updates, we might need to force an overwrite (that is, deleting before) QFile file(payloadfile); bool success = true; const bool update = ((entry.status() == KNS3::Entry::Updateable) || (entry.status() == KNS3::Entry::Updating)); if (QFile::exists(installpath) && QDir::tempPath() != installdir) { if (!update) { - Question question(Question::ContinueCancelQuestion); - question.setQuestion(i18n("Overwrite existing file?") + QStringLiteral("\n'") + installpath + QLatin1Char('\'')); - question.setTitle(i18n("Download File")); - if(question.ask() != Question::ContinueResponse) { + Question question(Question::YesNoQuestion); + question.setQuestion(i18n("This file already exists on disk (possibly due to an earlier failed download attempt). Continuing means overwriting it. Do you wish to overwrite the existing file?") + QStringLiteral("\n'") + installpath + QLatin1Char('\'')); + question.setTitle(i18n("Overwrite File")); + if(question.ask() != Question::YesResponse) { return QStringList(); } } success = QFile::remove(installpath); } if (success) { //remove in case it's already present and in a temporary directory, so we get to actually use the path again if (installpath.startsWith(QDir::tempPath())) { file.remove(installpath); } success = file.rename(installpath); qCDebug(KNEWSTUFFCORE) << "move: " << file.fileName() << " to " << installpath; } if (!success) { qCCritical(KNEWSTUFFCORE) << "Cannot move file '" << payloadfile << "' to destination '" << installpath << "'"; return QStringList(); } installedFiles << installpath; } } return installedFiles; } QProcess* Installation::runPostInstallationCommand(const QString &installPath) { QString command(postInstallationCommand); QString fileArg(KShell::quoteArg(installPath)); command.replace(QLatin1String("%f"), fileArg); qCDebug(KNEWSTUFFCORE) << "Run command: " << command; QProcess* ret = new QProcess(this); connect(ret, static_cast(&QProcess::finished), this, [this, command](int exitcode, QProcess::ExitStatus status) { if (status == QProcess::CrashExit) { qCCritical(KNEWSTUFFCORE) << "Process crashed with command: " << command; } else if (exitcode) { qCCritical(KNEWSTUFFCORE) << "Command '" << command << "' failed with code" << exitcode; } sender()->deleteLater(); }); QStringList args = KShell::splitArgs(command); ret->setProgram(args.takeFirst()); ret->setArguments(args); ret->start(); return ret; } void Installation::uninstall(EntryInternal entry) { entry.setStatus(KNS3::Entry::Deleted); if (!uninstallCommand.isEmpty()) { const auto lst = entry.installedFiles(); for (const QString &file : lst) { QFileInfo info(file); if (info.isFile()) { QString fileArg(KShell::quoteArg(file)); QString command(uninstallCommand); command.replace(QLatin1String("%f"), fileArg); int exitcode = QProcess::execute(command); if (exitcode) { qCCritical(KNEWSTUFFCORE) << "Command failed" << command; } else { qCDebug(KNEWSTUFFCORE) << "Command executed successfully: " << command; } } } } const auto lst = entry.installedFiles(); for (const QString &file : lst) { if (file.endsWith(QLatin1Char('/'))) { QDir dir; bool worked = dir.rmdir(file); if (!worked) { // Maybe directory contains user created files, ignore it continue; } } else if (file.endsWith(QLatin1String("/*"))) { QDir dir(file.left(file.size()-2)); bool worked = dir.removeRecursively(); if (!worked) { qCWarning(KNEWSTUFFCORE) << "Couldn't remove" << dir.path(); continue; } } else { QFileInfo info(file); if (info.exists() || info.isSymLink()) { bool worked = QFile::remove(file); if (!worked) { qWarning() << "unable to delete file " << file; return; } } else { qWarning() << "unable to delete file " << file << ". file does not exist."; } } } entry.setUnInstalledFiles(entry.installedFiles()); entry.setInstalledFiles(QStringList()); emit signalEntryChanged(entry); } void Installation::slotInstallationVerification(int result) { Q_UNUSED(result) // Deprecated, was wired up to defunct Security class. } QStringList Installation::archiveEntries(const QString &path, const KArchiveDirectory *dir) { QStringList files; const auto lst = dir->entries(); for (const QString &entry : lst) { const auto currentEntry = dir->entry(entry); const QString childPath = path + QLatin1Char('/') + entry; if (currentEntry->isFile()) { files << childPath; } else if (currentEntry->isDirectory()) { files << childPath + QStringLiteral("/*"); } } return files; } diff --git a/src/core/itemsmodel.cpp b/src/core/itemsmodel.cpp index 4fc35064..8ff0f609 100644 --- a/src/core/itemsmodel.cpp +++ b/src/core/itemsmodel.cpp @@ -1,149 +1,154 @@ /* knewstuff3/ui/itemsmodel.cpp. Copyright (C) 2008 Jeremy Whiting 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) any later version. 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 "itemsmodel.h" #include #include "klocalizedstring.h" #include "entryinternal.h" #include "engine.h" #include "imageloader_p.h" namespace KNSCore { ItemsModel::ItemsModel(Engine *engine, QObject *parent) : QAbstractListModel(parent) , m_engine(engine) , m_hasPreviewImages(false) { } ItemsModel::~ItemsModel() { } int ItemsModel::rowCount(const QModelIndex & /*parent*/) const { return m_entries.count(); } QVariant ItemsModel::data(const QModelIndex &index, int role) const { if (role != Qt::UserRole) { return QVariant(); } EntryInternal entry = m_entries[index.row()]; return QVariant::fromValue(entry); } -void ItemsModel::slotEntriesLoaded(const KNSCore::EntryInternal::List& entries) +int ItemsModel::row(const EntryInternal &entry) const +{ + return m_entries.indexOf(entry); +} + +void ItemsModel::slotEntriesLoaded(const KNSCore::EntryInternal::List &entries) { for (const KNSCore::EntryInternal &entry : entries) { addEntry(entry); } } void ItemsModel::addEntry(const EntryInternal &entry) { QString preview = entry.previewUrl(EntryInternal::PreviewSmall1); if (!m_hasPreviewImages && !preview.isEmpty()) { m_hasPreviewImages = true; if (rowCount() > 0) { emit dataChanged(index(0, 0), index(rowCount() - 1, 0)); } } qCDebug(KNEWSTUFFCORE) << "adding entry " << entry.name() << " to the model"; beginInsertRows(QModelIndex(), m_entries.count(), m_entries.count()); m_entries.append(entry); endInsertRows(); if (!preview.isEmpty() && entry.previewImage(EntryInternal::PreviewSmall1).isNull()) { m_engine->loadPreview(entry, EntryInternal::PreviewSmall1); } } void ItemsModel::removeEntry(const EntryInternal &entry) { qCDebug(KNEWSTUFFCORE) << "removing entry " << entry.name() << " from the model"; int index = m_entries.indexOf(entry); if (index > -1) { beginRemoveRows(QModelIndex(), index, index); m_entries.removeAt(index); endRemoveRows(); } } void ItemsModel::slotEntryChanged(const EntryInternal &entry) { int i = m_entries.indexOf(entry); QModelIndex entryIndex = index(i, 0); emit dataChanged(entryIndex, entryIndex); } void ItemsModel::clearEntries() { beginResetModel(); m_entries.clear(); endResetModel(); } void ItemsModel::slotEntryPreviewLoaded(const EntryInternal &entry, EntryInternal::PreviewType type) { // we only care about the first small preview in the list if (type != EntryInternal::PreviewSmall1) { return; } slotEntryChanged(entry); } /* void ItemsModel::slotEntryPreviewLoaded(const QString &url, const QImage & pix) { if (pix.isNull()) { return; } QImage image = pix; m_largePreviewImages.insert(url, image); if (image.width() > PreviewWidth || image.height() > PreviewHeight) { // if the preview is really big, first scale fast to a smaller size, then smooth to desired size if (image.width() > 4 * PreviewWidth || image.height() > 4 * PreviewHeight) { image = image.scaled(2 * PreviewWidth, 2 * PreviewHeight, Qt::KeepAspectRatio, Qt::FastTransformation); } m_previewImages.insert(url, image.scaled(PreviewWidth, PreviewHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } else if (image.width() <= PreviewWidth / 2 && image.height() <= PreviewHeight / 2) { // upscale tiny previews to double size m_previewImages.insert(url, image.scaled(2 * image.width(), 2 * image.height())); } else { m_previewImages.insert(url, image); } QModelIndex thisIndex = m_imageIndexes[url]; emit dataChanged(thisIndex, thisIndex); }*/ bool ItemsModel::hasPreviewImages() const { return m_hasPreviewImages; } } // end KNS namespace diff --git a/src/core/itemsmodel.h b/src/core/itemsmodel.h index 15fb8ccc..6bba4b39 100644 --- a/src/core/itemsmodel.h +++ b/src/core/itemsmodel.h @@ -1,69 +1,75 @@ /* knewstuff3/ui/itemsmodel.h. Copyright (C) 2008 Jeremy Whiting 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) any later version. 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 KNEWSTUFF3_ITEMSMODEL_P_H #define KNEWSTUFF3_ITEMSMODEL_P_H #include #include "entryinternal.h" #include "knewstuffcore_export.h" class KJob; namespace KNSCore { class Engine; class KNEWSTUFFCORE_EXPORT ItemsModel: public QAbstractListModel { Q_OBJECT public: explicit ItemsModel(Engine *engine, QObject *parent = nullptr); ~ItemsModel() override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + /** + * The row of the entry passed to the function, or -1 if the entry is not contained + * within the model. + * @since 5.63 + */ + int row(const EntryInternal &entry) const; void addEntry(const EntryInternal &entry); void removeEntry(const EntryInternal &entry); bool hasPreviewImages() const; bool hasWebService() const; Q_SIGNALS: void jobStarted(KJob *, const QString &label); public Q_SLOTS: void slotEntryChanged(const KNSCore::EntryInternal &entry); void slotEntriesLoaded(const KNSCore::EntryInternal::List &entries); void clearEntries(); void slotEntryPreviewLoaded(const KNSCore::EntryInternal &entry, KNSCore::EntryInternal::PreviewType type); private: Engine *m_engine; // the list of entries QList m_entries; bool m_hasPreviewImages; }; } // end KNS namespace Q_DECLARE_METATYPE(KNSCore::EntryInternal) #endif diff --git a/src/core/provider.h b/src/core/provider.h index 2e91a9f5..fa8563da 100644 --- a/src/core/provider.h +++ b/src/core/provider.h @@ -1,222 +1,260 @@ /* knewstuff3/provider.h This file is part of KNewStuff2. Copyright (c) 2009 Jeremy Whiting Copyright (C) 2009 Frederik Gladhorn 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) any later version. 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 KNEWSTUFF3_PROVIDER_P_H #define KNEWSTUFF3_PROVIDER_P_H #include #include #include #include +#include + #include "entryinternal.h" #include "errorcode.h" #include "knewstuffcore_export.h" class KJob; namespace KNSCore { +struct Comment; /** * @short KNewStuff Base Provider class. * * This class provides accessors for the provider object. * It should not be used directly by the application. * This class is the base class and will be instantiated for * static website providers. * * @author Jeremy Whiting * * @internal */ class KNEWSTUFFCORE_EXPORT Provider: public QObject { Q_OBJECT public: typedef QList List; enum SortMode { Newest, Alphabetical, Rating, Downloads, }; Q_ENUM(SortMode) enum Filter { None, Installed, Updates, ExactEntryId }; Q_ENUM(Filter) /** * used to keep track of a search */ struct SearchRequest { SortMode sortMode; Filter filter; QString searchTerm; QStringList categories; int page; int pageSize; SearchRequest(SortMode sortMode_ = Newest, Filter filter_ = None, const QString &searchTerm_ = QString(), const QStringList &categories_ = QStringList(), int page_ = -1, int pageSize_ = 20) : sortMode(sortMode_), filter(filter_), searchTerm(searchTerm_), categories(categories_), page(page_), pageSize(pageSize_) {} QString hashForRequest() const; }; /** * Describes a category: id/name/disaplayName */ struct CategoryMetadata { QString id; QString name; QString displayName; }; /** * Constructor. */ Provider(); /** * Destructor. */ virtual ~Provider(); /** * A unique Id for this provider (the url in most cases) */ virtual QString id() const = 0; /** * Set the provider data xml, to initialize the provider. * The Provider needs to have it's ID set in this function and cannot change it from there on. */ virtual bool setProviderXML(const QDomElement &xmldata) = 0; virtual bool isInitialized() const = 0; virtual void setCachedEntries(const KNSCore::EntryInternal::List &cachedEntries) = 0; /** * Retrieves the common name of the provider. * * @return provider name */ virtual QString name() const; /** * Retrieves the icon URL for this provider. * * @return icon URL */ virtual QUrl icon() const; // FIXME use QIcon::fromTheme or pixmap? /** * load the given search and return given page * @param sortMode string to select the order in which the results are presented * @param searchstring string to search with * @param page page number to load * * Note: the engine connects to loadingFinished() signal to get the result */ virtual void loadEntries(const KNSCore::Provider::SearchRequest &request) = 0; virtual void loadEntryDetails(const KNSCore::EntryInternal &) {} virtual void loadPayloadLink(const EntryInternal &entry, int linkId) = 0; + /** + * Request a loading of comments from this provider. The engine listens to the + * commentsLoaded() signal for the result + * + * @note Implementation detail: All subclasses should connect to this signal + * and point it at a slot which does the actual work, if they support comments. + * + * TODO: KF6 This should be a virtual function, but can't do it now because BIC + * @see commentsLoaded(const QList> comments) + * @since 5.63 + */ + Q_SIGNAL void loadComments(const EntryInternal &entry, int commentsPerPage, int page); + /** + * Request loading of the details for a specific person with the given username. + * The engine listens to the personLoaded() for the result + * + * @note Implementation detail: All subclasses should connect to this signal + * and point it at a slot which does the actual work, if they support comments. + * + * TODO: KF6 This should be a virtual function, but can't do it now because BIC + * @since 5.63 + */ + Q_SIGNAL void loadPerson(const QString &username); virtual bool userCanVote() { return false; } virtual void vote(const EntryInternal &entry, uint rating) { Q_UNUSED(entry) Q_UNUSED(rating) } virtual bool userCanBecomeFan() { return false; } virtual void becomeFan(const EntryInternal &entry) { Q_UNUSED(entry) } /** * Set the tag filter used for entries by this provider * @param tagFilter The new list of filters * @see Engine::setTagFilter(QStringList) * @since 5.51 */ void setTagFilter(const QStringList &tagFilter); /** * The tag filter used for downloads by this provider * @return The list of filters * @see Engine::setTagFilter(QStringList) * @since 5.51 */ QStringList tagFilter() const; /** * Set the tag filter used for download items by this provider * @param downloadTagFilter The new list of filters * @see Engine::setDownloadTagFilter(QStringList) * @since 5.51 */ void setDownloadTagFilter(const QStringList &downloadTagFilter); /** * The tag filter used for downloads by this provider * @return The list of filters * @see Engine::setDownloadTagFilter(QStringList) * @since 5.51 */ QStringList downloadTagFilter() const; Q_SIGNALS: void providerInitialized(KNSCore::Provider *); void loadingFinished(const KNSCore::Provider::SearchRequest &, const KNSCore::EntryInternal::List &) const; void loadingFailed(const KNSCore::Provider::SearchRequest &); void entryDetailsLoaded(const KNSCore::EntryInternal &); void payloadLinkLoaded(const KNSCore::EntryInternal &); + /** + * Fired when new comments have been loaded + * @param comments The list of newly loaded comments, in a depth-first order + * @since 5.63 + */ + void commentsLoaded(const QList> comments); + /** + * Fired when the details of a person have been loaded + * @param author The person we've just loaded data for + * @since 5.63 + */ + void personLoaded(const std::shared_ptr author); void signalInformation(const QString &) const; void signalError(const QString &) const; void signalErrorCode(const KNSCore::ErrorCode &errorCode, const QString &message, const QVariant &metadata) const; void categoriesMetadataLoded(const QList &categories); protected: QString mName; QUrl mIcon; private: Q_DISABLE_COPY(Provider) }; KNEWSTUFFCORE_EXPORT QDebug operator<<(QDebug, const Provider::SearchRequest &); } #endif diff --git a/src/core/question.cpp b/src/core/question.cpp index cfb061d1..9f89fac1 100644 --- a/src/core/question.cpp +++ b/src/core/question.cpp @@ -1,124 +1,124 @@ /* This file is part of KNewStuffCore. Copyright (c) 2016 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) any later version. 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 "question.h" #include "questionmanager.h" #include using namespace KNSCore; class Question::Private { public: Private() : questionActive(false) , questionType(YesNoQuestion) , response(InvalidResponse) {} QString question; QString title; QStringList list; bool questionActive; Question::QuestionType questionType; Question::Response response; QString textResponse; }; -Question::Question(QuestionType questionType, QObject* parent) +Question::Question(QuestionType questionType, QObject *parent) : QObject(parent) , d(new Private) { d->questionType = questionType; } Question::~Question() { delete d; } Question::Response Question::ask() { d->questionActive = true; emit QuestionManager::instance()->askQuestion(this); while(d->questionActive) { qApp->processEvents(); } return d->response; } Question::QuestionType Question::questionType() const { return d->questionType; } void Question::setQuestionType(Question::QuestionType newType) { d->questionType = newType; } void Question::setQuestion(const QString &newQuestion) { d->question = newQuestion; } QString Question::question() const { return d->question; } void Question::setTitle(const QString &newTitle) { d->title = newTitle; } QString Question::title() const { return d->title; } void Question::setList(const QStringList &newList) { d->list = newList; } QStringList Question::list() const { return d->list; } void Question::setResponse(Response response) { d->response = response; d->questionActive = false; } void Question::setResponse(const QString &response) { d->textResponse = response; } QString Question::response() const { return d->textResponse; } diff --git a/src/core/question.h b/src/core/question.h index b961d164..09858250 100644 --- a/src/core/question.h +++ b/src/core/question.h @@ -1,108 +1,110 @@ /* This file is part of KNewStuffCore. Copyright (c) 2016 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) any later version. 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 KNS3_QUESTION_H #define KNS3_QUESTION_H #include #include "knewstuffcore_export.h" namespace KNSCore { /** * @short A way to ask a user a question from inside a GUI-less library (like KNewStuffCore) * * Rather than using a message box (which is a UI thing), when you want to ask your user * a question, create an instance of this class and use that instead. The consuming library * (in most cases KNewStuff or KNewStuffQuick) will listen to any question being asked, * and act appropriately (that is, KNewStuff will show a dialog with an appropriate dialog * box, and KNewStuffQuick will either request a question be asked if the developer is using * the plugin directly, or ask the question using an appropriate method for Qt Quick based * applications) * * The following is an example of a question asking the user to select an item from a list. * * @code QStringList choices() << "foo" << "bar"; Question question(Question::SelectFromListQuestion); question.setTitle("Pick your option"); question.setQuestion("Please select which option you would like"); question.setList(choices); if(question.ask() == Question::OKResponse) { QString theChoice = question.response(); } @endcode */ class KNEWSTUFFCORE_EXPORT Question : public QObject { Q_OBJECT public: enum Response { InvalidResponse = 0, YesResponse = 1, NoResponse = 2, ContinueResponse = 3, CancelResponse = 4, OKResponse = YesResponse }; + Q_ENUM(Response) enum QuestionType { YesNoQuestion = 0, ContinueCancelQuestion = 1, InputTextQuestion = 2, SelectFromListQuestion = 3, PasswordQuestion = 4 }; + Q_ENUM(QuestionType) - explicit Question(QuestionType = YesNoQuestion, QObject* parent = nullptr); + explicit Question(QuestionType = YesNoQuestion, QObject *parent = nullptr); virtual ~Question(); Response ask(); void setQuestionType(QuestionType newType = YesNoQuestion); QuestionType questionType() const; void setQuestion(const QString &newQuestion); QString question() const; void setTitle(const QString &newTitle); QString title() const; void setList(const QStringList &newList); QStringList list() const; /** * When the user makes a choice on a question, that is a response. This is the return value in ask(). * @param response This will set the response, and mark the question as answered */ void setResponse(Response response); /** * If the user has any way of inputting data to go along with the response above, consider this a part * of the response. As such, you can set, and later get, that response as well. This does NOT mark the * question as answered ( @see setResponse(Response) ). * @param response This sets the string response for the question */ void setResponse(const QString &response); QString response() const; private: class Private; Private* d; }; } #endif//KNS3_QUESTION_H diff --git a/src/qtquick/CMakeLists.txt b/src/qtquick/CMakeLists.txt index 67a12ddc..97b8ae06 100644 --- a/src/qtquick/CMakeLists.txt +++ b/src/qtquick/CMakeLists.txt @@ -1,21 +1,29 @@ set(qmlplugin_SRCS qmlplugin.cpp quickengine.cpp quickitemsmodel.cpp + quickquestionlistener.cpp + author.cpp + categoriesmodel.cpp + commentsmodel.cpp downloadlinkinfo.cpp ) +ecm_qt_declare_logging_category(qmlplugin_SRCS HEADER knewstuffquick_debug.h IDENTIFIER KNEWSTUFFQUICK CATEGORY_NAME org.kde.knewstuff.quick) + add_library (newstuffqmlplugin ${qmlplugin_SRCS}) target_link_libraries (newstuffqmlplugin Qt5::Core Qt5::Qml Qt5::Quick Qt5::Xml + KF5::ConfigCore + KF5::I18n KF5::NewStuffCore ) install (TARGETS newstuffqmlplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/newstuff) install (DIRECTORY qml DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/newstuff) install (FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/newstuff) diff --git a/src/qtquick/author.cpp b/src/qtquick/author.cpp new file mode 100644 index 00000000..b1e3cc3a --- /dev/null +++ b/src/qtquick/author.cpp @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2019 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 "author.h" + +#include "quickengine.h" + +#include "core/author.h" +#include "core/engine.h" +#include "core/provider.h" + +#include "knewstuffquick_debug.h" + +#include + +namespace KNewStuffQuick { + +// This caching will want to eventually go into the Provider level (and be more generalised) +typedef QHash> AllAuthorsHash; +Q_GLOBAL_STATIC(AllAuthorsHash, allAuthors) + +class Author::Private { +public: + Private(Author *qq) + : q(qq) + {} + Author *q; + bool componentCompleted{false}; + Engine *engine{nullptr}; + QString providerId; + QString username; + + QSharedPointer provider; + void resetConnections() { + if (!componentCompleted) { + return; + } + if (provider) { + provider->disconnect(q); + } + if (engine && engine->engine()) { + KNSCore::Engine *coreEngine = qobject_cast(engine->engine()); + if (coreEngine) { + provider = coreEngine->provider(providerId); + } + if (!provider) { + provider = coreEngine->defaultProvider(); + } + } + if (provider) { + connect(provider.get(), &KNSCore::Provider::personLoaded, q, [=](const std::shared_ptr< KNSCore::Author > author){ + allAuthors()->insert(QString::fromLatin1("%1 %2").arg(provider->id()).arg(author->id()), author); + emit q->dataChanged(); + }); + author(); // Check and make sure... + } + } + + std::shared_ptr author() + { + std::shared_ptr ret; + if (provider && !username.isEmpty()) { + ret = allAuthors()->value(QString::fromLatin1("%1 %2").arg(provider->id()).arg(username)); + if(!ret.get()) { + provider->loadPerson(username); + } + } + return ret; + } +}; +} + +using namespace KNewStuffQuick; + +Author::Author(QObject *parent) + : QObject(parent) + , d(new Private(this)) +{ + connect(this, &Author::engineChanged, &Author::dataChanged); + connect(this, &Author::providerIdChanged, &Author::dataChanged); + connect(this, &Author::usernameChanged, &Author::dataChanged); +} + +Author::~Author() +{ + delete d; +} + +void Author::classBegin() +{ } + +void Author::componentComplete() +{ + d->componentCompleted = true; + d->resetConnections(); +} + +QObject *Author::engine() const +{ + return d->engine; +} + +void Author::setEngine(QObject *newEngine) +{ + if (d->engine != newEngine) { + d->engine = qobject_cast(newEngine); + d->resetConnections(); + emit engineChanged(); + } +} + +QString Author::providerId() const +{ + return d->providerId; +} + +void Author::setProviderId(const QString &providerId) +{ + if (d->providerId != providerId) { + d->providerId = providerId; + d->resetConnections(); + emit providerIdChanged(); + } +} + +QString Author::username() const +{ + return d->username; +} + +void Author::setUsername(const QString &username) +{ + if (d->username != username) { + d->username = username; + d->resetConnections(); + emit usernameChanged(); + } +} + +QString Author::name() const +{ + std::shared_ptr author = d->author(); + if (author.get() && !author->name().isEmpty()) { + return author->name(); + } + return d->username; +} + +QString Author::description() const +{ + std::shared_ptr author = d->author(); + if (author.get()) { + return author->description(); + } + return QString{}; +} + +QString Author::homepage() const +{ + std::shared_ptr author = d->author(); + if (author.get()) { + return author->homepage(); + } + return QString{}; +} + +QString Author::profilepage() const +{ + std::shared_ptr author = d->author(); + if (author.get()) { + return author->profilepage(); + } + return QString{}; +} + +QUrl Author::avatarUrl() const +{ + std::shared_ptr author = d->author(); + if (author.get()) { + return author->avatarUrl(); + } + return QUrl{}; +} diff --git a/src/qtquick/author.h b/src/qtquick/author.h new file mode 100644 index 00000000..f8d1c2ce --- /dev/null +++ b/src/qtquick/author.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2019 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 KNSQUICK_AUTHOR_H +#define KNSQUICK_AUTHOR_H + +#include +#include +#include + +namespace KNewStuffQuick +{ +/** + * @short Encapsulates a KNSCore::Author for use in Qt Quick + * + * This class takes care of initialisation of a KNSCore::Author when assigned an engine, provider ID and username. + * If the data is not yet cached, it will be requested from the provider, and updated for display + * @since 5.63 + */ +class Author : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + /** + * The NewStuffQuickEngine to interact with servers through + */ + Q_PROPERTY(QObject *engine READ engine WRITE setEngine NOTIFY engineChanged) + /** + * The ID of the provider which the user is registered on + */ + Q_PROPERTY(QString providerId READ providerId WRITE setProviderId NOTIFY providerIdChanged) + /** + * The user ID for the user this object represents + */ + Q_PROPERTY(QString username READ username WRITE setUsername NOTIFY usernameChanged) + + Q_PROPERTY(QString name READ name NOTIFY dataChanged) + Q_PROPERTY(QString description READ description NOTIFY dataChanged) + Q_PROPERTY(QString homepage READ homepage NOTIFY dataChanged) + Q_PROPERTY(QString profilepage READ profilepage NOTIFY dataChanged) + Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY dataChanged) +public: + explicit Author(QObject *parent = nullptr); + virtual ~Author(); + void classBegin() override; + void componentComplete() override; + + QObject *engine() const; + void setEngine(QObject *newEngine); + Q_SIGNAL void engineChanged(); + + QString providerId() const; + void setProviderId(const QString &providerId); + Q_SIGNAL void providerIdChanged(); + + QString username() const; + void setUsername(const QString &username); + Q_SIGNAL void usernameChanged(); + + QString name() const; + QString description() const; + QString homepage() const; + QString profilepage() const; + QUrl avatarUrl() const; + Q_SIGNAL void dataChanged(); + +private: + class Private; + Private *d; +}; +} + +#endif//KNSQUICK_AUTHOR_H diff --git a/src/qtquick/categoriesmodel.cpp b/src/qtquick/categoriesmodel.cpp new file mode 100644 index 00000000..5c409928 --- /dev/null +++ b/src/qtquick/categoriesmodel.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2019 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 "categoriesmodel.h" + +#include "engine.h" + +#include + +class CategoriesModel::Private { +public: + Private() {} + KNSCore::Engine *engine; +}; + +CategoriesModel::CategoriesModel(Engine *parent) + : QAbstractListModel(parent) + , d(new Private) +{ + d->engine = qobject_cast(parent->engine()); + connect(d->engine, &KNSCore::Engine::signalCategoriesMetadataLoded, this, [this](){ beginResetModel(); endResetModel(); }); +} + +CategoriesModel::~CategoriesModel() +{ + delete d; +} + +QHash CategoriesModel::roleNames() const +{ + static const QHash roles{ + {NameRole, "name"}, + {IdRole, "id"}, + {DisplayNameRole, "displayName"} + }; + return roles; +} + +int CategoriesModel::rowCount(const QModelIndex &parent) const +{ + if(parent.isValid()) { + return 0; + } + return d->engine->categoriesMetadata().count() + 1; +} + +QVariant CategoriesModel::data(const QModelIndex &index, int role) const +{ + QVariant result; + const QList categoriesMetadata = d->engine->categoriesMetadata(); + if (index.isValid()) { + if (index.row() == 0) { + switch (role) { + case NameRole: + result.setValue(QString::fromLatin1("")); + break; + case IdRole: + result.setValue(0); + break; + case DisplayNameRole: + result.setValue(i18nc("The first entry in the category selection list (also the default value)", "Show All Categories")); + break; + default: + result.setValue(QString::fromLatin1("Unknown role")); + break; + } + } else if (index.row() <= categoriesMetadata.count()) { + const KNSCore::Provider::CategoryMetadata category = categoriesMetadata[index.row() - 1]; + switch (role) { + case NameRole: + result.setValue(category.name); + break; + case IdRole: + result.setValue(category.id); + break; + case DisplayNameRole: + result.setValue(category.displayName); + break; + default: + result.setValue(QString::fromLatin1("Unknown role")); + break; + } + } + } + return result; +} + +QString CategoriesModel::idToDisplayName(const QString &id) const +{ + QString dispName = i18nc("The string passed back in the case the requested category is not known", "Unknown Category"); + for (const KNSCore::Provider::CategoryMetadata &cat : d->engine->categoriesMetadata()) { + if (cat.id == id) { + dispName = cat.displayName; + break; + } + } + return dispName; +} diff --git a/src/qtquick/categoriesmodel.h b/src/qtquick/categoriesmodel.h new file mode 100644 index 00000000..beea4724 --- /dev/null +++ b/src/qtquick/categoriesmodel.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2019 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 CATEGORIESMODEL_H +#define CATEGORIESMODEL_H + +#include + +#include "provider.h" +#include "quickengine.h" + +/** + * @short A model which shows the categories found in an Engine + * @since 5.63 + */ +class CategoriesModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit CategoriesModel(Engine *parent = nullptr); + virtual ~CategoriesModel(); + + enum Roles { + NameRole = Qt::UserRole + 1, + IdRole, + DisplayNameRole + }; + Q_ENUMS(Roles) + + QHash< int, QByteArray > roleNames() const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + /** + * Get the display name for the category with the id passed to the function + * + * @param id The ID of the category you want to get the display name for + * @return The display name (or the translated string "Unknown Category" for the requested category + */ + Q_INVOKABLE QString idToDisplayName(const QString &id) const; +private: + class Private; + // TODO KF6: Switch all the pimpls to const std::unique_ptr d; + Private *d; +}; + +#endif//CATEGORIESMODEL_H diff --git a/src/qtquick/commentsmodel.cpp b/src/qtquick/commentsmodel.cpp new file mode 100644 index 00000000..01909a19 --- /dev/null +++ b/src/qtquick/commentsmodel.cpp @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2019 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 "commentsmodel.h" + +#include "quickitemsmodel.h" + +#include "core/commentsmodel.h" + +#include "knewstuffquick_debug.h" + +#include + +namespace KNewStuffQuick { + +class CommentsModel::Private { +public: + Private(CommentsModel *qq) + : q(qq) + {} + CommentsModel *q; + ItemsModel *itemsModel{nullptr}; + int entryIndex{-1}; + bool componentCompleted{false}; + + QSharedPointer provider; + void resetConnections() { + if (componentCompleted && itemsModel) { + q->setSourceModel(qobject_cast(itemsModel->data(itemsModel->index(entryIndex), ItemsModel::CommentsModelRole).value())); + } + } +}; +} + +using namespace KNewStuffQuick; + +CommentsModel::CommentsModel(QObject *parent) + : QIdentityProxyModel(parent) + , d(new Private(this)) +{ +} + +CommentsModel::~CommentsModel() +{ + delete d; +} + +void KNewStuffQuick::CommentsModel::classBegin() +{} + +void KNewStuffQuick::CommentsModel::componentComplete() +{ + d->componentCompleted = true; + d->resetConnections(); +} + +QObject *CommentsModel::itemsModel() const +{ + return d->itemsModel; +} + +void CommentsModel::setItemsModel(QObject *newItemsModel) +{ + if (d->itemsModel != newItemsModel) { + d->itemsModel = qobject_cast(newItemsModel); + d->resetConnections(); + emit itemsModelChanged(); + } +} + +int CommentsModel::entryIndex() const +{ + return d->entryIndex; +} + +void CommentsModel::setEntryIndex(int entryIndex) +{ + if (d->entryIndex != entryIndex) { + d->entryIndex = entryIndex; + d->resetConnections(); + emit entryIndexChanged(); + } +} diff --git a/src/qtquick/commentsmodel.h b/src/qtquick/commentsmodel.h new file mode 100644 index 00000000..1c26d7ca --- /dev/null +++ b/src/qtquick/commentsmodel.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2019 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 KNSQUICK_COMMENTSMODEL_H +#define KNSQUICK_COMMENTSMODEL_H + +#include +#include +#include + +namespace KNewStuffQuick +{ +/** + * @short Encapsulates a KNSCore::CommentsModel for use in Qt Quick + * + * This class takes care of initialisation of a KNSCore::CommentsModel when assigned an engine, + * providerId and entryId. If the data is not yet cached, it will be requested from the provider, + * and updated for display + * @since 5.63 + */ +class CommentsModel : public QIdentityProxyModel, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + /** + * The KNewStufQuick::ItemsModel to interact with servers through + */ + Q_PROPERTY(QObject *itemsModel READ itemsModel WRITE setItemsModel NOTIFY itemsModelChanged) + /** + * The index in the model of the entry to fetch comments for + */ + Q_PROPERTY(int entryIndex READ entryIndex WRITE setEntryIndex NOTIFY entryIndexChanged) +public: + explicit CommentsModel(QObject *parent = nullptr); + ~CommentsModel() override; + void classBegin() override; + void componentComplete() override; + + QObject *itemsModel() const; + void setItemsModel(QObject *newItemsModel); + Q_SIGNAL void itemsModelChanged(); + + int entryIndex() const; + void setEntryIndex(int entryIndex); + Q_SIGNAL void entryIndexChanged(); + + +private: + class Private; + Private *d; +}; +} + +#endif//KNSQUICK_COMMENTSMODEL_H diff --git a/src/qtquick/qml/Button.qml b/src/qtquick/qml/Button.qml new file mode 100644 index 00000000..5a1ff177 --- /dev/null +++ b/src/qtquick/qml/Button.qml @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2019 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 . + * + */ + +/** + * @brief A button which when clicked will open a dialog with a NewStuff.Page at the base + * + * This component is equivalent to the old Button + * @see KNewStuff::Button + * @since 5.63 + */ + +import QtQuick 2.11 +import QtQuick.Controls 2.11 as QtControls + +import org.kde.newstuff 1.62 as NewStuff + +QtControls.Button { + id: component + + /* + * The configuration file is not aliased, because then we end up initialising the + * KNSCore::Engine immediately the Button is shown, which we want to avoid (as that + * is effectively a phone-home scenario, and causes internet traffic in situations + * where it would not seem likely that there should be any). + * If we want, in the future, to add some status display to Button (such as "there + * are updates to be had" or somesuch, then we can do this, but until that choice is + * made, let's not) + */ + /** + * The configuration file to use for this button + */ + property string configFile: ghnsDialog.configFile + + /** + * Set the text that should appear on the button. Will be set as + * i18n("Download New %1"). + * + * @note For the sake of consistency, you should NOT override the text propety, just set this one + */ + property string downloadNewWhat: i18nc("Used to contruct the button's label (which will become Download New 'this value')", "Stuff") + text: i18n("Download New %1").arg(downloadNewWhat) + + /** + * The default view mode of the dialog spawned by this button. This should be + * set using the NewStuff.Page.ViewMode enum + * @see NewStuff.Page.ViewMode + */ + property alias viewMode: ghnsDialog.viewMode + + /** + * emitted when the Hot New Stuff dialog is about to be shown, usually + * as a result of the user having click on the button + */ + signal aboutToShowDialog(); + + /** + * The engine which handles the content in this Button + */ + property alias engine: ghnsDialog.engine + + /** + * Contains the entries which have been changed. + * @note This is cleared when the dialog is shown, so the changed entries are those + * changed since the dialog was opened most recently (rather than the lifetime + * of the instance of the Button component) + */ + property alias changedEntries: component.engine.changedEntries + + /** + * If this is true (default is false), the button will be shown when the Kiosk settings are such + * that Get Hot New Stuff is disallowed (and any other time enabled is set to false). + * Usually you would want to leave this alone, but occasionally you may have a reason to + * leave a button in place that the user is unable to enable. + */ + property bool visibleWhenDisabled: false + + /** + * Show the dialog (same as clicking the button), if allowed by the Kiosk settings + */ + function showDialog() { + if (ghnsDialog.engine.allowedByKiosk) { + ghnsDialog.engine.configFile = component.configFile + component.aboutToShowDialog(); + ghnsDialog.open(); + } else { + // make some noise, because silently doing nothing is a bit annoying + } + } + + onClicked: { showDialog(); } + + icon.name: "get-hot-new-stuff" + visible: enabled || visibleWhenDisabled + enabled: ghnsDialog.engine.allowedByKiosk + onEnabledChanged: { + // If the user resets this when kiosk has disallowed ghns, force enabled back to false + if (enabled === true && ghnsDialog.engine.allowedByKiosk === false) { + enabled = false; + } + } + + NewStuff.Dialog { + id: ghnsDialog + } +} diff --git a/src/qtquick/qml/Dialog.qml b/src/qtquick/qml/Dialog.qml new file mode 100644 index 00000000..66d86f24 --- /dev/null +++ b/src/qtquick/qml/Dialog.qml @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2019 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 . + * + */ + +/** + * @brief A dialog which has a NewStuff.Page at the base + * + * This component is equivalent to the old DownloadDialog, but you should consider + * using NewStuff.Page instead for a more modern style of integration into your + * application's flow. + * @see KNewStuff::DownloadDialog + * @since 5.63 + */ + +import QtQuick 2.11 +import QtQuick.Controls 2.5 as QtControls +import QtQuick.Layouts 1.11 as QtLayouts +import QtQuick.Dialogs 1.3 as QtDialogs + +import org.kde.newstuff 1.62 as NewStuff + +QtDialogs.Dialog { + id: component + + /** + * The configuration file to use for this button + */ + property alias configFile: newStuffPage.configFile + + /** + * Set the text that should appear as the dialog's title. Will be set as + * i18n("Download New %1"). + * + * @default The name defined by your knsrc config file + * @note For the sake of consistency, you should NOT override the text propety, just set this one + */ + property string downloadNewWhat: engine.name + title: i18n("Download New %1").arg(component.downloadNewWhat) + + /** + * The engine which handles the content in this dialog + */ + property alias engine: newStuffPage.engine + + /** + * The default view mode of the dialog spawned by this button. This should be + * set using the NewStuff.Page.ViewMode enum + * @see NewStuff.Page.ViewMode + */ + property alias viewMode: newStuffPage.viewMode + + /** + * emitted when the Hot New Stuff dialog is about to be shown, usually + * as a result of the user having click on the button + */ + signal aboutToShowDialog(); + + /** + * Contains the entries which have been changed. + * @note This is cleared when the dialog is shown, so the changed entries are those + * changed since the dialog was opened most recently (rather than the lifetime + * of the instance of the Dialog component) + */ + property alias changedEntries: component.engine.changedEntries + + onVisibleChanged: { + if (visible === true) { + newStuffPage.engine.resetChangedEntries(); + } + } + + contentItem: QtLayouts.ColumnLayout { + NewStuff.DialogContent { + id: newStuffPage + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.fillHeight: true + downloadNewWhat: component.downloadNewWhat + } + QtControls.DialogButtonBox { + QtLayouts.Layout.fillWidth: true + QtControls.Button { + action: QtControls.Action { + text: i18n("Close") + shortcut: "esc" + onTriggered: { + component.close(); + } + } + QtControls.DialogButtonBox.buttonRole: QtControls.DialogButtonBox.RejectRole // this is a close button, dialog close buttons have a rejection role... + } + } + } +} diff --git a/src/qtquick/qml/DialogContent.qml b/src/qtquick/qml/DialogContent.qml new file mode 100644 index 00000000..159400d0 --- /dev/null +++ b/src/qtquick/qml/DialogContent.qml @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2019 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 . + * + */ + +/** + * @brief The contents of the NewStuff.Dialog component + * + * This component is equivalent to the old DownloadWidget, but you should consider + * using NewStuff.Page instead for a more modern style of integration into your + * application's flow. + * @see KNewStuff::DownloadWidget + * @since 5.63 + */ + +import QtQuick 2.11 +import QtQuick.Layouts 1.11 as QtLayouts + +import org.kde.kirigami 2.7 as Kirigami + +import org.kde.newstuff 1.62 as NewStuff + +Kirigami.ApplicationItem { + id: component + + property alias downloadNewWhat: newStuffPage.title + /** + * The configuration file to use for this button + */ + property alias configFile: newStuffPage.configFile + + /** + * The engine which handles the content in this dialog + */ + property alias engine: newStuffPage.engine + + /** + * The default view mode of the dialog spawned by this button. This should be + * set using the NewStuff.Page.ViewMode enum + * @see NewStuff.Page.ViewMode + */ + property alias viewMode: newStuffPage.viewMode + + QtLayouts.Layout.preferredWidth: Kirigami.Units.gridUnit * 50 + QtLayouts.Layout.preferredHeight: Kirigami.Units.gridUnit * 40 + pageStack.defaultColumnWidth: pageStack.width + pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.Auto + pageStack.initialPage: NewStuff.Page { + id: newStuffPage + onMessage: component.showPassiveNotification(message); + onIdleMessage: component.showPassiveNotification(message); + onBusyMessage: component.showPassiveNotification(message); + onErrorMessage: component.showPassiveNotification(message); + } +} diff --git a/src/qtquick/qml/DownloadItemsSheet.qml b/src/qtquick/qml/DownloadItemsSheet.qml new file mode 100644 index 00000000..d927b66f --- /dev/null +++ b/src/qtquick/qml/DownloadItemsSheet.qml @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2019 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.11 +import QtQuick.Controls 2.11 as QtControls +import QtQuick.Layouts 1.11 as QtLayouts + +import org.kde.kirigami 2.7 as Kirigami + +import org.kde.newstuff 1.62 as NewStuff + +/** + * @brief An overlay sheet for showing a list of download options for one entry + * + * This is used by the NewStuff.Page componet + * @since 5.63 + */ + +Kirigami.OverlaySheet { + id: component + + property string entryId + property alias downloadLinks: itemsView.model + signal itemPicked(string entryId, int downloadItemId, string downloadName) + + showCloseButton: true + header: QtLayouts.ColumnLayout { + spacing: Kirigami.Units.largeSpacing + Kirigami.Heading { + QtLayouts.Layout.fillWidth: true + text: i18n("Pick Your Installation Option") + elide: Text.ElideRight + } + QtControls.Label { + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.margins: Kirigami.Units.largeSpacing + text: i18n("Please select the option you wish to install from the list of downloadable items below. If it is unclear which you should chose out of the available options, please contact the author of this item and ask that they clarify this through the naming of the items.") + wrapMode: Text.Wrap + } + } + contentItem: ListView { + id: itemsView + QtLayouts.Layout.preferredWidth: parent.width - Kirigami.Units.largeSpacing * 2 + delegate: Kirigami.BasicListItem { + anchors { + left: parent.left + right: parent.right + leftMargin: Kirigami.Units.largeSpacing * 2 + rightMargin: Kirigami.Units.largeSpacing * 2 + } + text: modelData.name + icon: "download" + QtControls.ToolButton { + text: i18n("Install") + icon.name: "install" + QtLayouts.Layout.alignment: Qt.AlignRight + onClicked: { + component.close(); + component.itemPicked(component.entryId, modelData.id, modelData.name); + } + } + } + } +} diff --git a/src/qtquick/qml/EntryDetails.qml b/src/qtquick/qml/EntryDetails.qml new file mode 100644 index 00000000..d60c1acf --- /dev/null +++ b/src/qtquick/qml/EntryDetails.qml @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2019 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 . + * + */ + +/** + * @brief A Kirigami.Page component used for displaying the details for a single entry + * + * This component is equivalent to the details view in the old DownloadDialog + * @see KNewStuff::DownloadDialog + * @since 5.63 + */ + +import QtQuick 2.11 +import QtQuick.Controls 2.11 as QtControls +import QtQuick.Layouts 1.11 as QtLayouts + +import org.kde.kirigami 2.7 as Kirigami +import org.kde.kcm 1.2 as KCM + +import org.kde.newstuff 1.62 as NewStuff + +import "private" as Private + +KCM.SimpleKCM { + id: component + property QtObject newStuffModel + property int index + property string name + property var author + property alias shortSummary: shortSummaryItem.text + property alias summary: summaryItem.text; + property alias previews: screenshotsItem.screenshotsModel + property string homepage + property string donationLink + property int status + property int commentsCount + property int rating + property int downloadCount + property var downloadLinks + property string providerId + + NewStuff.DownloadItemsSheet { + id: downloadItemsSheet + onItemPicked: { + var entryName = newStuffModel.data(newStuffModel.index(entryId, 0), NewStuff.ItemsModel.NameRole); + applicationWindow().showPassiveNotification(i18nc("A passive notification shown when installation of an item is initiated", "Installing %1 from %2").arg(downloadName).arg(entryName), 1500); + newStuffModel.installItem(entryId, downloadItemId); + } + } + + Connections { + target: newStuffModel + onEntryChanged: { + var status = newStuffModel.data(newStuffModel.index(index, 0), NewStuff.ItemsModel.StatusRole); + if (status == NewStuff.ItemsModel.DownloadableStatus + || status == NewStuff.ItemsModel.InstalledStatus + || status == NewStuff.ItemsModel.UpdateableStatus + || status == NewStuff.ItemsModel.DeletedStatus) { + statusCard.message = ""; + } else if (status == NewStuff.ItemsModel.InstallingStatus) { + statusCard.message = i18nc("Status message to be shown when the entry is in the process of being installed", "Currently installing the item %1 by %2. Please wait...").arg(component.name).arg(entryAuthor.name); + } else if (status == NewStuff.ItemsModel.UpdatingStatus) { + statusCard.message = i18nc("Status message to be shown when the entry is in the process of being updated", "Currently updating the item %1 by %2. Please wait...").arg(component.name).arg(entryAuthor.name); + } else { + statusCard.message = i18nc("Status message which should only be shown when the entry has been given some unknown or invalid status.", "This item is currently in an invalid or unknown state. Please report this to the KDE Community in a bug report."); + } + } + } + + NewStuff.Author { + id: entryAuthor + engine: component.newStuffModel.engine + providerId: component.providerId + username: author.name + } + title: i18nc("Combined title for the entry details page made of the name of the entry, and the author's name", "%1 by %2").arg(component.name).arg(entryAuthor.name) + titleDelegate: QtLayouts.RowLayout { + implicitHeight: title.height + Kirigami.Heading { + id: title + level: 1 + + QtLayouts.Layout.fillWidth: true; + QtLayouts.Layout.preferredWidth: titleTextMetrics.width + QtLayouts.Layout.minimumWidth: titleTextMetrics.width + opacity: component.isCurrentPage ? 1 : 0.4 + maximumLineCount: 1 + elide: Text.ElideRight + text: component.title + TextMetrics { + id: titleTextMetrics + text: component.title + font: title.font + } + } + QtControls.ToolButton { + text: component.downloadCount == 1 ? i18nc("Request installation of this item, available when there is exactly one downloadable item", "Install") : i18nc("Show installation options, where there is more than one downloadable item", "Install..."); + icon.name: "install" + onClicked: { + if (component.downloadCount == 1) { + newStuffModel.installItem(component.index); + } else { + downloadItemsSheet.downloadLinks = component.downloadLinks; + downloadItemsSheet.entryId = component.index; + downloadItemsSheet.open(); + } + } + enabled: component.status == NewStuff.ItemsModel.DownloadableStatus || component.status == NewStuff.ItemsModel.DeletedStatus; + visible: enabled; + } + QtControls.ToolButton { + text: i18nc("Request updating of this item", "Update"); + icon.name: "update" + onClicked: { newStuffModel.installItem(component.index); } + enabled: component.status == NewStuff.ItemsModel.UpdateableStatus; + visible: enabled; + } + QtControls.ToolButton { + text: i18nc("Request uninstallation of this item", "Uninstall"); + icon.name: "uninstall" + onClicked: { newStuffModel.uninstallItem(component.index); } + enabled: component.status == NewStuff.ItemsModel.InstalledStatus + visible: enabled; + } + } + QtLayouts.ColumnLayout { + spacing: Kirigami.Units.smallSpacing + Item { width: parent.width; height: Kirigami.Units.gridUnit * 3; } + Kirigami.AbstractCard { + id: statusCard + property string message; + visible: opacity > 0 + opacity: message.length > 0 ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration; } } + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.margins: Kirigami.Units.largeSpacing + Item { + id: statusContent + implicitHeight: statusCard.message.length > 0 ? Math.max(statusBusy.height, statusLabel.height) + Kirigami.Units.largeSpacing * 4 : 0 + implicitWidth: statusCard.width + QtControls.BusyIndicator { + id: statusBusy + anchors { + top: parent.top + left: parent.left + } + running: statusCard.opacity > 0 + Rectangle { anchors.fill: parent; color: "red"; opacity: 0.3; } + } + QtControls.Label { + id: statusLabel + anchors { + top: parent.top + left: statusBusy.right + leftMargin: Kirigami.Units.largeSpacing + right: parent.right + } + text: statusCard.message + wrapMode: Text.Wrap + Rectangle { anchors.fill: parent; color: "blue"; opacity: 0.3; } + } + } + } + Private.EntryScreenshots { + id: screenshotsItem + QtLayouts.Layout.fillWidth: true + } + Kirigami.Heading { + id: shortSummaryItem + QtLayouts.Layout.fillWidth: true + } + Kirigami.FormLayout { + QtLayouts.Layout.fillWidth: true + Kirigami.LinkButton { + Kirigami.FormData.label: i18n("Comments and Reviews:") + enabled: component.commentsCount > 0 + text: i18nc("A link which, when clicked, opens a new sub page with reviews (comments) for this entry", "%1 Reviews").arg(component.commentsCount) + onClicked: pageStack.push(commentsPage) + } + Private.Rating { + id: ratingsItem + Kirigami.FormData.label: i18n("Rating:") + rating: Math.floor(component.rating / 10) + } + Kirigami.LinkButton { + Kirigami.FormData.label: i18n("Homepage:") + text: i18nc("A link which, when clicked, opens the website associated with the entry (this could be either one specific to the project, the author's homepage, or any other website they have chosen for the purpose)", "Open the homepage for %2").arg(component.name) + onClicked: Qt.openUrlExternally(component.homepage) + } + Kirigami.LinkButton { + Kirigami.FormData.label: i18n("How To Donate:") + text: i18nc("A link which, when clicked, opens a website with information on donation in support of the entry", "Find out how to donate to this project") + onClicked: Qt.openUrlExternally(component.donationLink) + } + } + QtControls.Label { + id: summaryItem + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.margins: Kirigami.Units.largeSpacing + wrapMode: Text.Wrap + } + } + + Component { + id: commentsPage + Private.EntryCommentsPage { + itemsModel: component.newStuffModel + entryIndex: component.index + entryName: component.name + entryAuthorId: component.author.name + entryProviderId: component.providerId + } + } +} diff --git a/src/qtquick/qml/NewStuffItem.qml b/src/qtquick/qml/NewStuffItem.qml index 991f4d11..8ec75a8a 100644 --- a/src/qtquick/qml/NewStuffItem.qml +++ b/src/qtquick/qml/NewStuffItem.qml @@ -1,132 +1,149 @@ /* * 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.2 -import QtQuick.Controls 1.4 as QtControls +import QtQuick 2.11 +import QtQuick.Controls 2.11 as QtControls +import QtQuick.Layouts 1.11 as QtLayouts import org.kde.kirigami 2.1 as Kirigami -import org.kde.newstuff 1.0 as NewStuff + +import org.kde.newstuff 1.62 as NewStuff Kirigami.SwipeListItem { id: listItem; - height: Kirigami.Units.iconSizes.huge + Kirigami.Units.smallSpacing * 2; + height: Math.max(Kirigami.Units.iconSizes.huge + Kirigami.Units.smallSpacing * 2, nameText.height + descriptionText.height + Kirigami.Units.smallSpacing * 5); property QtObject listModel; enabled: true; actions: [ Kirigami.Action { text: i18nc("Request installation of this item", "Install"); iconName: "list-add" - onTriggered: { listModel.installItem(model.index); } + onTriggered: { listModel.installItem(model.index, 1); } enabled: model.status == NewStuff.ItemsModel.DownloadableStatus || model.status == NewStuff.ItemsModel.DeletedStatus; visible: enabled; }, Kirigami.Action { text: i18nc("Request updating of this item", "Update"); iconName: "refresh" - onTriggered: { listModel.installItem(model.index); } + onTriggered: { listModel.installItem(model.index, 1); } enabled: model.status == NewStuff.ItemsModel.UpdateableStatus; visible: enabled; }, Kirigami.Action { text: i18nc("Request uninstallation of this item", "Uninstall"); iconName: "list-remove" onTriggered: { listModel.uninstallItem(model.index); } enabled: model.status == NewStuff.ItemsModel.InstalledStatus visible: enabled; } ] - Item { - anchors.fill: parent; + QtLayouts.RowLayout { Item { id: previewContainer; - anchors { - top: parent.top; - left: parent.left; - bottom: parent.bottom; - margins: Kirigami.Units.smallSpacing; - } - width: height; + QtLayouts.Layout.preferredHeight: listItem.height - Kirigami.Units.smallSpacing * 2; + QtLayouts.Layout.minimumWidth: Kirigami.Units.iconSizes.huge; + QtLayouts.Layout.maximumWidth: Kirigami.Units.iconSizes.huge; Image { id: previewImage; anchors { fill: parent; margins: Kirigami.Units.smallSpacing; + leftMargin: -Kirigami.Units.smallSpacing; } asynchronous: true; fillMode: Image.PreserveAspectFit; source: model.previewsSmall.length > 0 ? model.previewsSmall[0] : ""; Kirigami.Icon { id: updateAvailableBadge; opacity: (model.status == NewStuff.ItemsModel.UpdateableStatus) ? 1 : 0; Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } anchors { bottom: parent.bottom; right: parent.right; margins: -Kirigami.Units.smallSpacing; } height: Kirigami.Units.iconSizes.smallMedium; width: height; source: "vcs-update-required"; } Kirigami.Icon { id: installedBadge; opacity: (model.status == NewStuff.ItemsModel.InstalledStatus) ? 1 : 0; Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } anchors { bottom: parent.bottom; right: parent.right; margins: -Kirigami.Units.smallSpacing; } height: Kirigami.Units.iconSizes.smallMedium; width: height; source: "vcs-normal"; } } - } - Kirigami.Label { - anchors { - verticalCenter: parent.verticalCenter; - left: previewContainer.right; - leftMargin: Kirigami.Units.largeSpacing; + Rectangle { + anchors.fill: parent + opacity: installIndicator.opacity > 0 ? 0.7 : 0 + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } + visible: opacity > 0 + } + QtControls.BusyIndicator { + id: installIndicator + anchors.centerIn: parent; + opacity: (model.status == NewStuff.ItemsModel.InstallingStatus || model.status == NewStuff.ItemsModel.UpdatingStatus) ? 1 : 0; + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } + running: opacity > 0; + QtControls.Label { + anchors { + horizontalCenter: parent.horizontalCenter; + bottom: parent.bottom; + margins: Kirigami.Units.smallSpacing; + } + text: (model.status == NewStuff.ItemsModel.InstallingStatus) ? "Installing" : ((model.status == NewStuff.ItemsModel.UpdatingStatus) ? "Updating" : ""); + width: paintedWidth; + } } - text: model.name; } - QtControls.BusyIndicator { - anchors { - verticalCenter: parent.verticalCenter; - right: parent.right; - rightMargin: Kirigami.Units.largeSpacing + Kirigami.Units.iconSizes.large; + QtLayouts.ColumnLayout { + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.fillHeight: true + Kirigami.Heading { + id: nameText + QtLayouts.Layout.fillWidth: true + level: 3 + text: model.name + opacity: 1 - installIndicator.opacity } - opacity: (model.status == NewStuff.ItemsModel.InstallingStatus || model.status == NewStuff.ItemsModel.UpdatingStatus) ? 1 : 0; - Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } - running: opacity > 0; - Kirigami.Label { - anchors { - verticalCenter: parent.verticalCenter; - right: parent.left; - rightMargin: Kirigami.Units.smallSpacing; - } - text: (model.status == NewStuff.ItemsModel.InstallingStatus) ? "Installing" : ((model.status == NewStuff.ItemsModel.UpdatingStatus) ? "Updating" : ""); - width: paintedWidth; + QtControls.Label { + id: descriptionText + QtLayouts.Layout.fillWidth: true + text: model.summary.split("\n")[0]; + elide: Text.ElideRight + maximumLineCount: 2 + wrapMode: Text.Wrap + opacity: 1 - installIndicator.opacity + } + Item { + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.fillHeight: true } } } } diff --git a/src/qtquick/qml/NewStuffList.qml b/src/qtquick/qml/NewStuffList.qml index 7097259f..a4662b6a 100644 --- a/src/qtquick/qml/NewStuffList.qml +++ b/src/qtquick/qml/NewStuffList.qml @@ -1,81 +1,128 @@ /* * 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.2 +import QtQuick 2.11 +import QtQuick.Controls 2.11 as QtControls +import QtQuick.Layouts 1.11 as QtLayouts -import org.kde.newstuff 1.0 as NewStuff +import org.kde.newstuff 1.62 as NewStuff /** * To use NewStuffList, simply instantiate it and pass the * local file location of a knsrc file to the configFile property. * The components will, in this case, take care of the rest for you. * If you want more, you can look at what NewStuffItem does with the * various bits, and be inspired by that. * * An (overly simple) example which might be used for managing * wallpapers and just outputting any messages onto the console can * be seen below. Note that you should obviously not be using * hardcoded paths, it is done here to get the idea across. * * \code NewStuff.NewStuffList { configFile: "/some/filesystem/location/wallpaper.knsrc"; onMessage: console.log("KNS Message: " + message); onIdleMessage: console.log("KNS Idle: " + message); onBusyMessage: console.log("KNS Busy: " + message); onErrorMessage: console.log("KNS Error: " + message); } \endcode */ ListView { id: root; /** * @brief The configuration file which describes the application (knsrc) * * The format and location of this file is found in the documentation for * KNS3::DownloadDialog */ property alias configFile: newStuffEngine.configFile; signal message(string message); signal idleMessage(string message); signal busyMessage(string message); signal errorMessage(string message); signal downloadedItemClicked(variant installedFiles); + header: QtLayouts.RowLayout { + anchors { + left: parent.left + right: parent.right + } + QtControls.ComboBox { + id: categoriesCombo + QtLayouts.Layout.fillWidth: true + model: newStuffEngine.categories + textRole: "displayName" + onCurrentIndexChanged: { + newStuffEngine.categoriesFilter = model.data(model.index(currentIndex, 0), NewStuff.CategoriesModel.NameRole); + } + } + QtControls.ComboBox { + id: filterCombo + QtLayouts.Layout.fillWidth: true + model: ListModel {} + Component.onCompleted: { + filterCombo.model.append({ text: i18nc("List option which will set the filter to show everything", "Show Everything") }); + filterCombo.model.append({ text: i18nc("List option which will set the filter so only installed items are shown", "Installed Only") }); + filterCombo.model.append({ text: i18nc("List option which will set the filter so only installed items with updates available are shown", "Updateable Only") }); + filterCombo.currentIndex = newStuffEngine.filter; + } + onCurrentIndexChanged: { + newStuffEngine.filter = currentIndex; + } + } + QtControls.ComboBox { + id: sortCombo + QtLayouts.Layout.fillWidth: true + model: ListModel { } + Component.onCompleted: { + sortCombo.model.append({ text: i18nc("List option which will set the sort order to based on when items were most recently updated", "Show most recent first") }); + sortCombo.model.append({ text: i18nc("List option which will set the sort order to be alphabetical based on the name", "Sort alphabetically") }); + sortCombo.model.append({ text: i18nc("List option which will set the sort order to based on user ratings", "Show highest rated first") }); + sortCombo.model.append({ text: i18nc("List option which will set the sort order to based on number of downloads", "Show most downloaded first") }); + sortCombo.currentIndex = newStuffEngine.sortOrder; + } + onCurrentIndexChanged: { + newStuffEngine.sortOrder = currentIndex; + } + } + } delegate: NewStuffItem { listModel: newStuffModel; onClicked: { if(model.status == NewStuff.ItemsModel.InstalledStatus) { root.downloadedItemClicked(model.installedFiles); } } } model: NewStuff.ItemsModel { id: newStuffModel; - engine: newStuffEngine.engine; + engine: newStuffEngine; } NewStuff.Engine { id: newStuffEngine; onMessage: root.message(message); onIdleMessage: root.idleMessage(message); onBusyMessage: root.busyMessage(message); onErrorMessage: root.errorMessage(message); } + NewStuff.QuestionAsker {} } diff --git a/src/qtquick/qml/Page.qml b/src/qtquick/qml/Page.qml new file mode 100644 index 00000000..cf4c5590 --- /dev/null +++ b/src/qtquick/qml/Page.qml @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2019 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 . + * + */ + +/** + * @brief A Kirigami.Page component used for managing KNS entries + * + * This component is functionally equivalent to the old DownloadDialog + * @see KNewStuff::DownloadDialog + * @since 5.63 + */ + +import QtQuick 2.11 +import QtQuick.Controls 2.11 as QtControls +import QtQuick.Layouts 1.11 as QtLayouts +import QtGraphicalEffects 1.11 as QtEffects + +import org.kde.kcm 1.2 as KCM +import org.kde.kirigami 2.7 as Kirigami + +import org.kde.newstuff 1.62 as NewStuff + +import "private" as Private +import "private/entrygriddelegates" as EntryGridDelegates + +KCM.GridViewKCM { + id: root; + /** + * @brief The configuration file which describes the application (knsrc) + * + * The format and location of this file is found in the documentation for + * KNS3::DownloadDialog + */ + property alias configFile: newStuffEngine.configFile; + readonly property alias engine: newStuffEngine; + + /** + * Any generic message from the NewStuff.Engine + * @param message The message to be shown to the user + */ + signal message(string message); + /** + * A message posted usually describing that whatever action a recent busy + * message said was happening has been completed + * @param message The message to be shown to the user + */ + signal idleMessage(string message); + /** + * A message posted when the engine is busy doing something long duration + * (usually this will be when fetching installation data) + * @param message The message to be shown to the user + */ + signal busyMessage(string message); + /** + * A message posted when something has gone wrong + * @param message The message to be shown to the user + */ + signal errorMessage(string message); + + property string uninstallLabel: i18nc("Request uninstallation of this item", "Uninstall"); + property string useLabel: i18nc("If a knsrc file defines an adoption command, the option to run this command and 'use' an item becomes available. This is the text for an action to do so.", "Use"); + + property int viewMode: Page.ViewMode.Tiles + enum ViewMode { + Tiles, + Icons, + Preview + } + + title: newStuffEngine.name + NewStuff.Engine { + id: newStuffEngine; + onMessage: root.message(message); + onIdleMessage: root.idleMessage(message); + onBusyMessage: root.busyMessage(message); + onErrorMessage: root.errorMessage(message); + } + NewStuff.QuestionAsker {} + + titleDelegate: QtLayouts.RowLayout { + Kirigami.Heading { + id: title + level: 1 + + QtLayouts.Layout.fillWidth: true; + QtLayouts.Layout.preferredWidth: titleTextMetrics.width + QtLayouts.Layout.minimumWidth: titleTextMetrics.width + opacity: root.isCurrentPage ? 1 : 0.4 + maximumLineCount: 1 + elide: Text.ElideRight + text: root.title + TextMetrics { + id: titleTextMetrics + text: root.title + font: title.font + } + } + QtControls.ButtonGroup { + id: displayModeGroup + buttons: [displayModeTiles, displayModeIcons] + } + QtControls.ToolButton { + id: displayModeTiles + icon.name: "view-list-details" + onClicked: { root.viewMode = Page.ViewMode.Tiles; } + checked: root.viewMode == Page.ViewMode.Tiles + } + QtControls.ToolButton { + id: displayModeIcons + icon.name: "view-list-icons" + onClicked: { root.viewMode = Page.ViewMode.Icons; } + checked: root.viewMode == Page.ViewMode.Icons + } + QtControls.ToolButton { + id: displayPreview + icon.name: "view-preview" + onClicked: { root.viewMode = Page.ViewMode.Preview; } + checked: root.viewMode == Page.ViewMode.Preview + } + Kirigami.ActionTextField { + id: searchField + placeholderText: i18n("Search...") + focusSequence: "Ctrl+F" + rightActions: [ + Kirigami.Action { + iconName: "edit-clear" + visible: searchField.text !== "" + onTriggered: { + searchField.text = ""; + searchField.accepted(); + } + } + ] + onAccepted: { + newStuffEngine.searchTerm = searchField.text; + } + } + } + + footer: QtLayouts.RowLayout { + QtControls.ComboBox { + id: categoriesCombo + QtLayouts.Layout.fillWidth: true + model: newStuffEngine.categories + textRole: "displayName" + onCurrentIndexChanged: { + newStuffEngine.categoriesFilter = model.data(model.index(currentIndex, 0), NewStuff.CategoriesModel.NameRole); + } + } + QtControls.ComboBox { + id: filterCombo + QtLayouts.Layout.fillWidth: true + model: ListModel {} + Component.onCompleted: { + filterCombo.model.append({ text: i18nc("List option which will set the filter to show everything", "Show Everything") }); + filterCombo.model.append({ text: i18nc("List option which will set the filter so only installed items are shown", "Installed Only") }); + filterCombo.model.append({ text: i18nc("List option which will set the filter so only installed items with updates available are shown", "Updateable Only") }); + filterCombo.currentIndex = newStuffEngine.filter; + } + onCurrentIndexChanged: { + newStuffEngine.filter = currentIndex; + } + } + QtControls.ComboBox { + id: sortCombo + QtLayouts.Layout.fillWidth: true + model: ListModel { } + Component.onCompleted: { + sortCombo.model.append({ text: i18nc("List option which will set the sort order to based on when items were most recently updated", "Show most recent first") }); + sortCombo.model.append({ text: i18nc("List option which will set the sort order to be alphabetical based on the name", "Sort alphabetically") }); + sortCombo.model.append({ text: i18nc("List option which will set the sort order to based on user ratings", "Show highest rated first") }); + sortCombo.model.append({ text: i18nc("List option which will set the sort order to based on number of downloads", "Show most downloaded first") }); + sortCombo.currentIndex = newStuffEngine.sortOrder; + } + onCurrentIndexChanged: { + newStuffEngine.sortOrder = currentIndex; + } + } + } + + view.model: NewStuff.ItemsModel { + id: newStuffModel; + engine: newStuffEngine; + } + NewStuff.DownloadItemsSheet { + id: downloadItemsSheet + onItemPicked: { + var entryName = newStuffModel.data(newStuffModel.index(entryId, 0), NewStuff.ItemsModel.NameRole); + applicationWindow().showPassiveNotification(i18nc("A passive notification shown when installation of an item is initiated", "Installing %1 from %2").arg(downloadName).arg(entryName), 1500); + newStuffModel.installItem(entryId, downloadItemId); + } + } + + view.implicitCellWidth: root.viewMode == Page.ViewMode.Tiles ? Kirigami.Units.gridUnit * 30 : (root.viewMode == Page.ViewMode.Preview ? Kirigami.Units.gridUnit * 25 : Kirigami.Units.gridUnit * 10) + view.implicitCellHeight: root.viewMode == Page.ViewMode.Tiles ? Math.round(view.implicitCellWidth / 3) : (root.viewMode == Page.ViewMode.Preview ? Kirigami.Units.gridUnit * 25 : Math.round(view.implicitCellWidth / 1.6) + Kirigami.Units.gridUnit*2) + view.delegate: root.viewMode == Page.ViewMode.Tiles ? tileDelegate : (root.viewMode == Page.ViewMode.Preview ? bigPreviewDelegate : thumbDelegate) + + Component { + id: bigPreviewDelegate + EntryGridDelegates.BigPreviewDelegate { } + } + Component { + id: tileDelegate + EntryGridDelegates.TileDelegate { + useLabel: root.useLabel + uninstallLabel: root.uninstallLabel + } + } + Component { + id: thumbDelegate + EntryGridDelegates.ThumbDelegate { + useLabel: root.useLabel + uninstallLabel: root.uninstallLabel + } + } + + Component { + id: detailsPage; + NewStuff.EntryDetails { } + } +} diff --git a/src/qtquick/qml/QuestionAsker.qml b/src/qtquick/qml/QuestionAsker.qml new file mode 100644 index 00000000..20194fc9 --- /dev/null +++ b/src/qtquick/qml/QuestionAsker.qml @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2019 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 . + * + */ + +/** + * @brief A component used to forward questions from KNewStuff's engine to the UI + * + * This component is equivalent to the WidgetQuestionListener + * @see KNewStuff::WidgetQuestionListener + * @see KNewStuffCore::Question + * @since 5.63 + */ + +import QtQuick 2.11 +import QtQuick.Controls 2.11 as QtControls +import QtQuick.Layouts 1.11 as QtLayouts + +import org.kde.kirigami 2.7 as Kirigami + +import org.kde.newstuff.core 1.62 as NewStuffCore +import org.kde.newstuff 1.62 as NewStuff + +QtControls.Dialog { + id: dialog + modal: true + focus: true + property int questionType + anchors.centerIn: QtControls.Overlay.overlay + margins: Kirigami.Units.largeSpacing + padding: Kirigami.Units.largeSpacing + standardButtons: { + switch (questionType) { + case NewStuffCore.Question.SelectFromListQuestion: + case NewStuffCore.Question.InputTextQuestion: + case NewStuffCore.Question.PasswordQuestion: + case NewStuffCore.Question.ContinueCancelQuestion: + // QtControls Dialog standardButtons does not have a Continue button... + return QtControls.Dialog.Ok | QtControls.Dialog.Cancel; + break; + case NewStuffCore.Question.YesNoQuestion: + return QtControls.Dialog.Yes | QtControls.Dialog.No; + break; + default: + break; + } + } + + Connections { + target: NewStuff.QuickQuestionListener + onAskListQuestion: { + dialog.questionType = NewStuffCore.Question.SelectFromListQuestion; + dialog.title = title; + questionLabel.text = question; + for (var i = 0; i < list.length; i++) { + listView.model.append({ text: list[i] }); + } + listView.currentIndex = 0; + listView.visible = true; + dialog.open(); + } + onAskContinueCancelQuestion: { + dialog.questionType = NewStuffCore.Question.ContinueCancelQuestion; + dialog.title = title; + questionLabel.text = question; + dialog.open(); + } + onAskTextInputQuestion: { + dialog.questionType = NewStuffCore.Question.InputTextQuestion; + dialog.title = title; + questionLabel.text = question; + textInput.visible = true; + dialog.open(); + } + onAskPasswordQuestion: { + dialog.questionType = NewStuffCore.Question.PasswordQuestion; + dialog.title = title; + questionLabel.text = question; + textInput.echoMode = QtControls.TextInput.PasswordEchoOnEdit; + textInput.visible = true; + dialog.open(); + } + onAskYesNoQuestion: { + dialog.questionType = NewStuffCore.Question.YesNoQuestion; + dialog.title = title; + questionLabel.text = question; + dialog.open(); + } + } + Connections { + target: applicationWindow() + // Since dialogs in QML don't automatically reject when the application is closed, + // we just do that little job for it (and then we don't end up blocking everything + // when the application is shut without the question being answered) + onClosing: { + if (dialog.opened === true) { + passResponse(false); + } + } + } + function passResponse(responseIsContinue) { + var input = ""; + switch(dialog.questionType) { + case NewStuffCore.Question.SelectFromListQuestion: + input = listView.currentItem.text; + listView.model.clear(); + listView.visible = false; + break; + case NewStuffCore.Question.InputTextQuestion: + input = textInput.text; + textInput.text = ""; + textInput.visible = false; + break; + case NewStuffCore.Question.PasswordQuestion: + input = textInput.text; + textInput.text = ""; + textInput.visible = false; + textInput.echoMode = QtControls.TextInput.Normal; + break; + case NewStuffCore.Question.ContinueCancelQuestion: + case NewStuffCore.Question.YesNoQuestion: + default: + // Nothing special to do for these types of question, we just pass along the positive or negative response + break; + } + NewStuff.QuickQuestionListener.passResponse(responseIsContinue, input); + } + + QtLayouts.ColumnLayout { + anchors.fill: parent + property int maxWidth: applicationWindow().width - (dialog.leftPadding + dialog.leftMargin + dialog.rightMargin + dialog.rightPadding) + QtControls.Label { + id: questionLabel + QtLayouts.Layout.maximumWidth: parent.maxWidth + wrapMode: Text.Wrap + } + ListView { + id: listView + visible: false + QtLayouts.Layout.maximumWidth: parent.maxWidth + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.minimumHeight: Kirigami.Units.gridUnit * 6 + model: ListModel { } + delegate: Kirigami.BasicListItem { + reserveSpaceForIcon: false + text: model.text + } + } + QtControls.TextField { + id: textInput + visible: false + QtLayouts.Layout.maximumWidth: parent.maxWidth + QtLayouts.Layout.fillWidth: true + } + } + onAccepted: { + passResponse(true); + } + onRejected: { + passResponse(false); + } +} diff --git a/src/qtquick/qml/private/ConditionalLoader.qml b/src/qtquick/qml/private/ConditionalLoader.qml new file mode 100644 index 00000000..cb0828b3 --- /dev/null +++ b/src/qtquick/qml/private/ConditionalLoader.qml @@ -0,0 +1,35 @@ +/*************************************************************************** + * Copyright © 2015 Aleix Pol Gonzalez * + * * + * 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 . * + ***************************************************************************/ + +import QtQuick 2.11 +import QtQuick.Layouts 1.11 + +Loader +{ + id: root + + property Component componentTrue + property Component componentFalse + property bool condition + + Layout.minimumHeight: item && item.Layout ? item.Layout.minimumHeight : 0 + Layout.minimumWidth: item && item.Layout ? item.Layout.minimumWidth : 0 + sourceComponent: condition ? componentTrue : componentFalse +} diff --git a/src/qtquick/qml/private/EntryCommentDelegate.qml b/src/qtquick/qml/private/EntryCommentDelegate.qml new file mode 100644 index 00000000..65107460 --- /dev/null +++ b/src/qtquick/qml/private/EntryCommentDelegate.qml @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2019 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 . + * + */ + +/** + * @brief A card based delegate for showing a comment from a KNewStuffQuick::QuickCommentsModel + */ + +import QtQuick 2.11 +import QtQuick.Controls 2.11 as QtControls +import QtQuick.Layouts 1.11 as QtLayouts + +import org.kde.kirigami 2.7 as Kirigami + +import org.kde.newstuff 1.62 as NewStuff + +QtLayouts.RowLayout { + id: component + + /** + * The KNSQuick Engine object which handles all our content + */ + property QtObject engine + + /** + * The username of the author of whatever the comment is attached to + */ + property string entryAuthorId + /** + * The provider ID as supplied by the entry the comment is attached to + */ + property string entryProviderId + + /** + * The username of the comment's author + */ + property string author + /** + * The OCS score, an integer from 1 to 100. It will be interpreted + * as a 5 star rating, with half star support (0-10) + */ + property int score + /** + * The title or subject line for the comment + */ + property string title + /** + * The actual text of the comment + */ + property alias reviewText: reviewLabel.text + /** + * The depth of the comment (in essence, how many parents the comment has) + */ + property int depth + + spacing: 0 + + property QtObject commentAuthor: NewStuff.Author { + engine: component.engine + providerId: component.entryProviderId + username: component.author + } + + anchors { + left: parent.left + right: parent.right + leftMargin: Kirigami.Units.largeSpacing + rightMargin: Kirigami.Units.largeSpacing + } + + Repeater { + model: component.depth + delegate: Rectangle { + QtLayouts.Layout.fillHeight: true + QtLayouts.Layout.minimumWidth: Kirigami.Units.largeSpacing + QtLayouts.Layout.maximumWidth: Kirigami.Units.largeSpacing + color: Qt.tint(Kirigami.Theme.textColor, Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.8)) + Rectangle { + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + } + width: 1 + color: Kirigami.Theme.backgroundColor + } + } + } + + QtLayouts.ColumnLayout { + Item { + visible: component.depth === 0 + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.minimumHeight: Kirigami.Units.largeSpacing + QtLayouts.Layout.maximumHeight: Kirigami.Units.largeSpacing + } + + Kirigami.Separator { + QtLayouts.Layout.fillWidth: true + } + + QtLayouts.RowLayout { + visible: (component.title !== "" || component.score !== 0) + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.leftMargin: Kirigami.Units.largeSpacing + Kirigami.Heading { + id: titleLabel + text: ((component.title === "") ? i18nc("Placeholder title for when a comment has no subject, but does have a rating", "(no title)") : component.title) + level: 4 + QtLayouts.Layout.fillWidth: true + } + Rating { + id: ratingStars + rating: Math.floor(component.score / 10) + } + Item { + QtLayouts.Layout.minimumWidth: Kirigami.Units.largeSpacing + QtLayouts.Layout.maximumWidth: Kirigami.Units.largeSpacing + } + } + + QtControls.Label { + id: reviewLabel + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.leftMargin: Kirigami.Units.largeSpacing + QtLayouts.Layout.rightMargin: Kirigami.Units.largeSpacing + wrapMode: Text.Wrap + } + + QtLayouts.RowLayout { + QtLayouts.Layout.fillWidth: true + Item { + QtLayouts.Layout.fillWidth: true + } + Kirigami.UrlButton { + id: authorLabel + visible: (url !== "") + url: (component.commentAuthor.homepage === "") ? component.commentAuthor.profilepage : component.commentAuthor.homepage + text: (component.author === component.entryAuthorId) ? i18nc("The author label in case the comment was written by the author of the content entry the comment is attached to", "%1 (author)").arg(component.commentAuthor.name) : component.commentAuthor.name + } + QtControls.Label { + visible: !authorLabel.visible + text: authorLabel.text + } + Image { + id: authorIcon + QtLayouts.Layout.maximumWidth: height + QtLayouts.Layout.minimumWidth: height + QtLayouts.Layout.preferredHeight: Kirigami.Units.iconSizes.medium + fillMode: Image.PreserveAspectFit + source: component.commentAuthor.avatarUrl + Kirigami.Icon { + anchors.fill: parent; + source: "user" + visible: opacity > 0 + opacity: authorIcon.status == Image.Ready ? 0 : 1 + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } + } + } + Item { + QtLayouts.Layout.minimumWidth: Kirigami.Units.largeSpacing + QtLayouts.Layout.maximumWidth: Kirigami.Units.largeSpacing + } + } + Item { + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.minimumHeight: Kirigami.Units.largeSpacing + QtLayouts.Layout.maximumHeight: Kirigami.Units.largeSpacing + } + + } +} diff --git a/src/qtquick/qml/private/EntryCommentsPage.qml b/src/qtquick/qml/private/EntryCommentsPage.qml new file mode 100644 index 00000000..95c31504 --- /dev/null +++ b/src/qtquick/qml/private/EntryCommentsPage.qml @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2019 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 . + * + */ + +/** + * @brief A Kirigami.Page component used for displaying a NewStuff entry's comments + */ + +import QtQuick 2.11 +import QtQuick.Controls 2.11 as QtControls +import QtQuick.Layouts 1.11 as QtLayouts + +import org.kde.kirigami 2.7 as Kirigami + +import org.kde.newstuff 1.62 as NewStuff + +Kirigami.ScrollablePage { + id: component + property string entryName + property string entryAuthorId + property string entryProviderId + property alias entryIndex: commentsModel.entryIndex + property alias itemsModel: commentsModel.itemsModel + title: i18nc("Title for the page containing a view of the comments for the entry", "Comments and Reviews for %1").arg(component.entryName) + ListView { + id: commentsView + model: NewStuff.CommentsModel { + id: commentsModel + } + QtLayouts.Layout.fillWidth: true + header: Item { + anchors { + left: parent.left + right: parent.right + } + height: Kirigami.Units.largeSpacing + } + delegate: EntryCommentDelegate { + engine: component.itemsModel.engine + entryAuthorId: component.entryAuthorId + entryProviderId: component.entryProviderId + author: model.username + score: model.score + title: model.subject + reviewText: model.text + depth: model.depth + } + } +} diff --git a/src/qtquick/qml/private/EntryScreenshots.qml b/src/qtquick/qml/private/EntryScreenshots.qml new file mode 100644 index 00000000..067d5578 --- /dev/null +++ b/src/qtquick/qml/private/EntryScreenshots.qml @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2012 Aleix Pol Gonzalez + * Copyright (C) 2019 Dan Leinir Turthra Jensen + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library/Lesser General Public License + * version 2, or (at your option) any later version, as published by the + * Free Software Foundation + * + * 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 Library/Lesser General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.11 +import QtQuick.Controls 2.11 +import QtQuick.Layouts 1.11 +import QtGraphicalEffects 1.11 + +import org.kde.kirigami 2.5 as Kirigami + +Flickable { + id: root + property alias screenshotsModel: screenshotsRep.model + readonly property alias count: screenshotsRep.count + property int currentIndex: -1 + property Item currentItem: screenshotsRep.itemAt(currentIndex) + Layout.preferredHeight: Kirigami.Units.gridUnit * 13 + contentHeight: height + contentWidth: screenshotsLayout.width + + Popup { + id: overlay + parent: applicationWindow().overlay + modal: true + clip: false + + x: (parent.width - width)/2 + y: (parent.height - height)/2 + readonly property real proportion: overlayImage.sourceSize.width>1 ? overlayImage.sourceSize.height/overlayImage.sourceSize.width : 1 + height: overlayImage.status == Image.Loading ? Kirigami.Units.gridUnit * 5 : Math.min(parent.height * 0.9, (parent.width * 0.9) * proportion, overlayImage.sourceSize.height) + width: height/proportion + + BusyIndicator { + id: indicator + visible: running + running: overlayImage.status == Image.Loading + anchors.fill: parent + } + + Image { + id: overlayImage + anchors.fill: parent + source: root.currentItem ? root.currentItem.imageSource : "" + fillMode: Image.PreserveAspectFit + smooth: true + } + + Button { + anchors { + right: parent.left + verticalCenter: parent.verticalCenter + } + visible: leftAction.visible + icon.name: leftAction.iconName + onClicked: leftAction.triggered(null) + } + + Button { + anchors { + left: parent.right + verticalCenter: parent.verticalCenter + } + visible: rightAction.visible + icon.name: rightAction.iconName + onClicked: rightAction.triggered(null) + } + + Kirigami.Action { + id: leftAction + icon.name: "arrow-left" + enabled: overlay.visible && visible + visible: root.currentIndex >= 1 && !indicator.running + onTriggered: root.currentIndex = (root.currentIndex - 1) % root.count + } + + Kirigami.Action { + id: rightAction + icon.name: "arrow-right" + enabled: overlay.visible && visible + visible: root.currentIndex < (root.count - 1) && !indicator.running + onTriggered: root.currentIndex = (root.currentIndex + 1) % root.count + } + } + + Row { + id: screenshotsLayout + height: root.contentHeight + spacing: Kirigami.Units.largeSpacing + focus: overlay.visible + + Keys.onLeftPressed: if (leftAction.visible) leftAction.trigger() + Keys.onRightPressed: if (rightAction.visible) rightAction.trigger() + + Repeater { + id: screenshotsRep + + delegate: MouseArea { + readonly property url imageSource: modelData + readonly property real proportion: thumbnail.sourceSize.width>1 ? thumbnail.sourceSize.height/thumbnail.sourceSize.width : 1 + width: Math.max(50, height/proportion) + height: screenshotsLayout.height + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.currentIndex = index + overlay.open() + } + + DropShadow { + source: thumbnail + anchors.fill: thumbnail + verticalOffset: Kirigami.Units.largeSpacing + horizontalOffset: 0 + radius: 12.0 + samples: 25 + color: Kirigami.Theme.disabledTextColor + cached: true + } + + BusyIndicator { + visible: running + running: thumbnail.status == Image.Loading + anchors.centerIn: parent + } + + Image { + id: thumbnail + source: modelData + height: parent.height + fillMode: Image.PreserveAspectFit + smooth: true + } + } + } + } + clip: true + readonly property var leftShadow: Shadow { + parent: root + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + edge: Qt.LeftEdge + width: Math.max(0, Math.min(root.width/5, root.contentX)) + } + + readonly property var rightShadow: Shadow { + parent: root + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + } + edge: Qt.RightEdge + width: Math.max(0, Math.min(root.contentWidth - root.contentX - root.width)/5) + } +} diff --git a/src/qtquick/qml/private/GridTileDelegate.qml b/src/qtquick/qml/private/GridTileDelegate.qml new file mode 100644 index 00000000..2be578d0 --- /dev/null +++ b/src/qtquick/qml/private/GridTileDelegate.qml @@ -0,0 +1,183 @@ +/* + Copyright (c) 2015 Marco Martin + Copyright (c) 2019 Dan Leinir Turthra Jensen + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +import QtQuick 2.11 +import QtQuick.Controls 2.11 as Controls +import QtQuick.Templates 2.11 as T2 +import QtQuick.Layouts 1.11 +import QtGraphicalEffects 1.11 + +import org.kde.kirigami 2.2 as Kirigami + +/** + * Base delegate for KControlmodules based on Grid views of thumbnails + * Use the onClicked signal handler for managing the main action when + * the user clicks on the tile, modified from the original GridDelegate + * from the KCM module + * @inherits QtQuick.Templates.ItemDelegate + */ +T2.ItemDelegate { + id: delegate + + /** + * toolTip: string + * string for a tooltip for the whole delegate + */ + property string toolTip + + /** + * tile: Item + * the item actually implementing the tile: the visualization is up to the implementation + */ + property alias tile: thumbnailArea.data + + /** + * thumbnailAvailable: bool + * Set it to true when a tile is actually available: when false, + * a default icon will be shown instead of the actual tile. + */ + property bool thumbnailAvailable: false + + /** + * actions: list + * A list of extra actions for the thumbnails. They will be shown as + * icons on the bottom-right corner of the tile on mouse over + */ + property list actions + + /** + * actionsAnchors: anchors + * The anchors of the actions listing + */ + property alias actionsAnchors: actionsScope.anchors + + width: GridView.view.cellWidth + height: GridView.view.cellHeight + hoverEnabled: true + + Rectangle { + id: tile + anchors.centerIn: parent + width: Kirigami.Settings.isMobile ? delegate.width - Kirigami.Units.gridUnit : Math.min(delegate.GridView.view.implicitCellWidth, delegate.width - Kirigami.Units.gridUnit) + height: Math.min(delegate.GridView.view.implicitCellHeight, delegate.height - Kirigami.Units.gridUnit) + radius: Kirigami.Units.smallSpacing + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.View + + color: { + if (delegate.GridView.isCurrentItem) { + return Kirigami.Theme.highlightColor; + } else if (parent.hovered) { + return Kirigami.Theme.highlightColor; + } else { + return Kirigami.Theme.backgroundColor; + } + } + Behavior on color { + ColorAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.OutQuad + } + } + + Rectangle { + id: thumbnailArea + radius: Kirigami.Units.smallSpacing/2 + anchors { + fill: parent + margins: Kirigami.Units.smallSpacing + } + + color: Kirigami.Theme.backgroundColor + Kirigami.Icon { + visible: !delegate.thumbnailAvailable + anchors.centerIn: parent + width: Kirigami.Units.iconSizes.large + height: width + source: delegate.text === i18n("None") ? "edit-none" : "view-preview" + } + } + + Rectangle { + anchors.fill: thumbnailArea + visible: actionsColumn.children.length > 0 + opacity: Kirigami.Settings.isMobile || delegate.hovered || (actionsScope.focus) ? 1 : 0 + radius: Kirigami.Units.smallSpacing + color: Kirigami.Settings.isMobile ? "transparent" : Qt.rgba(1, 1, 1, 0.2) + + Behavior on opacity { + NumberAnimation { + duration: Kirigami.Units.longDuration + easing.type: Easing.OutQuad + } + } + + FocusScope { + id: actionsScope + + anchors { + right: parent.right + rightMargin: Kirigami.Units.smallSpacing + top: parent.top + topMargin: Kirigami.Units.smallSpacing + } + width: actionsColumn.width + height: actionsColumn.height + + ColumnLayout { + id: actionsColumn + + Repeater { + model: delegate.actions + delegate: Controls.Button { + icon.name: modelData.iconName + text: modelData.text + activeFocusOnTab: focus || delegate.focus + onClicked: { + delegate.clicked() + modelData.trigger() + } + enabled: modelData.enabled + visible: modelData.visible + //NOTE: there aren't any global settings where to take "official" tooltip timeouts + Controls.ToolTip.delay: 1000 + Controls.ToolTip.timeout: 5000 + Controls.ToolTip.visible: (Kirigami.Settings.isMobile ? pressed : hovered) && modelData.tooltip.length > 0 + Controls.ToolTip.text: modelData.tooltip + } + } + } + } + } + // Bug 397367: explicitly using "delegate" as otherwise it crashes when switching between KCMs + layer.enabled: delegate.GraphicsInfo.api === GraphicsInfo.OpenGL + layer.effect: DropShadow { + horizontalOffset: 0 + verticalOffset: 2 + radius: 10 + samples: 32 + color: Qt.rgba(0, 0, 0, 0.3) + } + } + + Controls.ToolTip.delay: 1000 + Controls.ToolTip.timeout: 5000 + Controls.ToolTip.visible: hovered && delegate.toolTip.length > 0 + Controls.ToolTip.text: toolTip +} diff --git a/src/qtquick/qml/private/Rating.qml b/src/qtquick/qml/private/Rating.qml new file mode 100644 index 00000000..f1fb4e1c --- /dev/null +++ b/src/qtquick/qml/private/Rating.qml @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2012 Aleix Pol Gonzalez + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library/Lesser General Public License + * version 2, or (at your option) any later version, as published by the + * Free Software Foundation + * + * 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 Library/Lesser General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.11 +import QtQuick.Layouts 1.11 + +import org.kde.kirigami 2.0 as Kirigami + +RowLayout +{ + id: view + property bool editable: false + property int max: 10 + property int rating: 0 + property real starSize: Kirigami.Units.gridUnit + + clip: true + spacing: 0 + + readonly property var ratingIndex: (theRepeater.count/view.max)*view.rating + + Repeater { + id: theRepeater + model: 5 + delegate: Kirigami.Icon { + Layout.minimumWidth: view.starSize + Layout.minimumHeight: view.starSize + Layout.preferredWidth: view.starSize + Layout.preferredHeight: view.starSize + + width: height + source: "rating" + opacity: (view.editable && mouse.item.containsMouse ? 0.7 + : index>=view.ratingIndex ? 0.2 + : 1) + + ConditionalLoader { + id: mouse + + anchors.fill: parent + condition: view.editable + componentTrue: MouseArea { + hoverEnabled: true + onClicked: rating = (max/theRepeater.model*(index+1)) + } + componentFalse: null + } + } + } +} diff --git a/src/qtquick/qml/private/Shadow.qml b/src/qtquick/qml/private/Shadow.qml new file mode 100644 index 00000000..9a74b3b9 --- /dev/null +++ b/src/qtquick/qml/private/Shadow.qml @@ -0,0 +1,49 @@ +/* + * Copyright 2018 Aleix Pol Gonzalez + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * 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 Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.11 +import QtGraphicalEffects 1.11 + +import org.kde.kirigami 2.2 + +LinearGradient { + id: shadow + property int edge: Qt.LeftEdge + + width: Units.gridUnit/2 + height: Units.gridUnit/2 + + start: Qt.point((edge !== Qt.RightEdge ? 0 : width), (edge !== Qt.BottomEdge ? 0 : height)) + end: Qt.point((edge !== Qt.LeftEdge ? 0 : width), (edge !== Qt.TopEdge ? 0 : height)) + gradient: Gradient { + GradientStop { + position: 0.0 + color: Theme.backgroundColor + } + GradientStop { + position: 0.3 + color: Qt.rgba(0, 0, 0, 0.1) + } + GradientStop { + position: 1.0 + color: "transparent" + } + } +} + diff --git a/src/qtquick/qml/private/entrygriddelegates/BigPreviewDelegate.qml b/src/qtquick/qml/private/entrygriddelegates/BigPreviewDelegate.qml new file mode 100644 index 00000000..e84ec2b4 --- /dev/null +++ b/src/qtquick/qml/private/entrygriddelegates/BigPreviewDelegate.qml @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2019 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.11 +import QtQuick.Controls 2.11 as QtControls +import QtQuick.Layouts 1.11 as QtLayouts +import QtGraphicalEffects 1.11 as QtEffects + +import org.kde.kirigami 2.7 as Kirigami + +import org.kde.newstuff 1.62 as NewStuff + +import ".." as Private + +Private.GridTileDelegate { + id: component + actionsAnchors.topMargin: bigPreview.height + Kirigami.Units.smallSpacing * 2 + function showDetails() { + pageStack.push(detailsPage, { + newStuffModel: GridView.view.model, + index: model.index, + name: model.name, + author: model.author, + previews: model.previews, + shortSummary: model.shortSummary, + summary: model.summary, + homepage: model.homepage, + donationLink: model.donationLink, + status: model.status, + commentsCount: model.numberOfComments, + rating: model.rating, + downloadCount: model.downloadCount, + downloadLinks: model.downloadLinks, + providerId: model.providerId + }); + } + actions: [ + Kirigami.Action { + text: root.useLabel + iconName: "dialog-ok-apply" + onTriggered: { model.adopt(model.index); } + enabled: (model.status == NewStuff.ItemsModel.InstalledStatus || model.status == NewStuff.ItemsModel.UpdateableStatus) && newStuffEngine.hasAdoptionCommand + visible: enabled + }, + Kirigami.Action { + text: model.downloadCount == 1 ? i18nc("Request installation of this item, available when there is exactly one downloadable item", "Install") : i18nc("Show installation options, where there is more than one downloadable item", "Install..."); + iconName: "install" + onTriggered: { + if (model.downloadCount == 1) { + newStuffModel.installItem(model.index, 1); + } else { + downloadItemsSheet.downloadLinks = model.downloadLinks; + downloadItemsSheet.entryId = model.index; + downloadItemsSheet.open(); + } + } + enabled: model.status == NewStuff.ItemsModel.DownloadableStatus || model.status == NewStuff.ItemsModel.DeletedStatus; + visible: enabled; + }, + Kirigami.Action { + text: i18nc("Request updating of this item", "Update"); + iconName: "update" + onTriggered: { newStuffModel.installItem(model.index); } + enabled: model.status == NewStuff.ItemsModel.UpdateableStatus; + visible: enabled; + }, + Kirigami.Action { + text: root.uninstallLabel + iconName: "uninstall" + onTriggered: { newStuffModel.uninstallItem(model.index); } + enabled: model.status == NewStuff.ItemsModel.InstalledStatus + visible: enabled; + } + ] + thumbnailAvailable: model.previewsSmall.length > 0 + tile: Item { + anchors { + fill: parent + margins: Kirigami.Units.smallSpacing + } + QtLayouts.ColumnLayout { + anchors.fill: parent; + Item { + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.minimumHeight: width / 1.8 + QtLayouts.Layout.maximumHeight: width / 1.8 + Image { + id: bigPreview + asynchronous: true; + fillMode: Image.PreserveAspectCrop; + source: thumbnailAvailable ? model.previews[0] : ""; + anchors.fill: parent + } + QtEffects.DropShadow { + anchors.fill: bigPreview + horizontalOffset: 0 + verticalOffset: 0 + radius: Kirigami.Units.largeSpacing + samples: radius * 2 + color: "#80000000" + source: bigPreview + } + Kirigami.Icon { + id: updateAvailableBadge; + opacity: (model.status == NewStuff.ItemsModel.UpdateableStatus) ? 1 : 0; + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } + anchors { + top: parent.top; + left: parent.left; + margins: -Kirigami.Units.smallSpacing; + } + height: Kirigami.Units.iconSizes.medium; + width: height; + source: "package-installed-outdated"; + } + Kirigami.Icon { + id: installedBadge; + opacity: (model.status == NewStuff.ItemsModel.InstalledStatus) ? 1 : 0; + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } + anchors { + top: parent.top; + left: parent.left; + } + height: Kirigami.Units.iconSizes.medium; + width: height; + source: "package-installed-updated"; + } + Item { + anchors { + top: parent.top; + right: parent.right; + } + height: Kirigami.Units.iconSizes.large; + width: height; + Kirigami.Icon { + id: infoIcon + source: "documentinfo" + anchors.fill: parent + } + MouseArea { + anchors.fill: parent + onClicked: { component.showDetails(); } + } + QtEffects.Glow { + anchors.fill: infoIcon + radius: 1 + samples: 2 + spread: 0.3 + color: "white" + source: infoIcon + } + } + } + Private.Rating { + QtLayouts.Layout.fillWidth: true + rating: model.rating / 10 + } + Kirigami.Heading { + QtLayouts.Layout.fillWidth: true + level: 5 + elide: Text.ElideRight + text: i18nc("The number of times the item has been downloaded", "%1 downloads").arg(model.downloadCount) + } + Kirigami.Heading { + QtLayouts.Layout.fillWidth: true + elide: Text.ElideRight + level: 3 + text: model.name + } + Kirigami.Heading { + QtLayouts.Layout.fillWidth: true + elide: Text.ElideRight + level: 4 + textFormat: Text.StyledText + text: i18nc("Subheading for the tile view, located immediately underneath the name of the item", "By %1").arg(model.author.name) + } + QtControls.Label { + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.fillHeight: true + wrapMode: Text.Wrap + text: model.shortSummary.length > 0 ? model.shortSummary : model.summary + elide: Text.ElideRight + } + } + Item { + anchors.fill: parent + opacity: (model.status == NewStuff.ItemsModel.InstallingStatus || model.status == NewStuff.ItemsModel.UpdatingStatus) ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration; } } + Rectangle { + anchors.fill: parent + color: Kirigami.Theme.backgroundColor + opacity: 0.5; + } + QtControls.BusyIndicator { + anchors.centerIn: parent + running: parent.opacity > 0 + } + } + MouseArea { + anchors.fill: parent; + onClicked: component.showDetails(); + } + } +} diff --git a/src/qtquick/qml/private/entrygriddelegates/ThumbDelegate.qml b/src/qtquick/qml/private/entrygriddelegates/ThumbDelegate.qml new file mode 100644 index 00000000..c30fe866 --- /dev/null +++ b/src/qtquick/qml/private/entrygriddelegates/ThumbDelegate.qml @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2019 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.11 +import QtQuick.Controls 2.11 as QtControls +import QtQuick.Layouts 1.11 as QtLayouts +import QtGraphicalEffects 1.11 as QtEffects + +import org.kde.kcm 1.2 as KCM +import org.kde.kirigami 2.7 as Kirigami + +import org.kde.newstuff 1.62 as NewStuff + +import ".." as Private + +KCM.GridDelegate { + id: component + property string useLabel + property string uninstallLabel + text: model.name +// onClicked: pageStack.push(detailsPage, { +// name: model.name, +// author: model.author, +// previews: model.previews, +// shortSummary: model.shortSummary, +// summary: model.summary, +// homepage: model.homepage, +// donationLink: model.donationLink +// }); + actions: [ + Kirigami.Action { + text: component.useLabel + iconName: "dialog-ok-apply" + onTriggered: { model.adopt(model.index); } + enabled: (model.status == NewStuff.ItemsModel.InstalledStatus || model.status == NewStuff.ItemsModel.UpdateableStatus) && newStuffEngine.hasAdoptionCommand + visible: enabled + }, + Kirigami.Action { + text: model.downloadCount == 1 ? i18nc("Request installation of this item, available when there is exactly one downloadable item", "Install") : i18nc("Show installation options, where there is more than one downloadable item", "Install..."); + iconName: "install" + onTriggered: { + if (model.downloadCount == 1) { + newStuffModel.installItem(model.index); + } else { + downloadItemsSheet.downloadLinks = model.downloadLinks; + downloadItemsSheet.entryId = model.index; + downloadItemsSheet.open(); + } + } + enabled: model.status == NewStuff.ItemsModel.DownloadableStatus || model.status == NewStuff.ItemsModel.DeletedStatus; + visible: enabled; + }, + Kirigami.Action { + text: i18nc("Request updating of this item", "Update"); + iconName: "update" + onTriggered: { newStuffModel.installItem(model.index); } + enabled: model.status == NewStuff.ItemsModel.UpdateableStatus; + visible: enabled; + }, + Kirigami.Action { + text: component.uninstallLabel + iconName: "uninstall" + onTriggered: { newStuffModel.uninstallItem(model.index); } + enabled: model.status == NewStuff.ItemsModel.InstalledStatus + visible: enabled; + } + ] + thumbnailAvailable: model.previewsSmall.length > 0 + thumbnail: Image { + anchors { + fill: parent; + margins: Kirigami.Units.smallSpacing; + } + asynchronous: true; + fillMode: Image.PreserveAspectFit; + source: thumbnailAvailable ? model.previewsSmall[0] : ""; + Kirigami.Icon { + id: updateAvailableBadge; + opacity: (model.status == NewStuff.ItemsModel.UpdateableStatus) ? 1 : 0; + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } + anchors { + top: parent.top; + right: parent.right; + margins: -Kirigami.Units.smallSpacing; + } + height: Kirigami.Units.iconSizes.smallMedium; + width: height; + source: "package-installed-outdated"; + } + Kirigami.Icon { + id: installedBadge; + opacity: (model.status == NewStuff.ItemsModel.InstalledStatus) ? 1 : 0; + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } + anchors { + top: parent.top; + right: parent.right; + margins: -Kirigami.Units.smallSpacing; + } + height: Kirigami.Units.iconSizes.smallMedium; + width: height; + source: "package-installed-updated"; + } + Item { + anchors.fill: parent + opacity: (model.status == NewStuff.ItemsModel.InstallingStatus || model.status == NewStuff.ItemsModel.UpdatingStatus) ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration; } } + Rectangle { + anchors.fill: parent + color: Kirigami.Theme.backgroundColor + opacity: 0.5; + } + QtControls.BusyIndicator { + anchors.centerIn: parent + running: parent.opacity > 0 + } + } + MouseArea { + anchors.fill: parent; + onClicked: pageStack.push(detailsPage, { + newStuffModel: GridView.view.model, + index: model.index, + name: model.name, + author: model.author, + previews: model.previews, + shortSummary: model.shortSummary, + summary: model.summary, + homepage: model.homepage, + donationLink: model.donationLink, + status: model.status, + commentsCount: model.numberOfComments, + rating: model.rating, + downloadCount: model.downloadCount, + downloadLinks: model.downloadLinks, + providerId: model.providerId + }); + } + } +} diff --git a/src/qtquick/qml/private/entrygriddelegates/TileDelegate.qml b/src/qtquick/qml/private/entrygriddelegates/TileDelegate.qml new file mode 100644 index 00000000..114c6988 --- /dev/null +++ b/src/qtquick/qml/private/entrygriddelegates/TileDelegate.qml @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2019 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.11 +import QtQuick.Controls 2.11 as QtControls +import QtQuick.Layouts 1.11 as QtLayouts +import QtGraphicalEffects 1.11 as QtEffects + +import org.kde.kirigami 2.7 as Kirigami + +import org.kde.newstuff 1.62 as NewStuff + +import ".." as Private + +Private.GridTileDelegate { + id: component + property string useLabel + property string uninstallLabel + function showDetails() { + pageStack.push(detailsPage, { + newStuffModel: GridView.view.model, + index: model.index, + name: model.name, + author: model.author, + previews: model.previews, + shortSummary: model.shortSummary, + summary: model.summary, + homepage: model.homepage, + donationLink: model.donationLink, + status: model.status, + commentsCount: model.numberOfComments, + rating: model.rating, + downloadCount: model.downloadCount, + downloadLinks: model.downloadLinks, + providerId: model.providerId + }); + } + actions: [ + Kirigami.Action { + text: component.useLabel + iconName: "dialog-ok-apply" + onTriggered: { model.adopt(model.index); } + enabled: (model.status == NewStuff.ItemsModel.InstalledStatus || model.status == NewStuff.ItemsModel.UpdateableStatus) && newStuffEngine.hasAdoptionCommand + visible: enabled + }, + Kirigami.Action { + text: model.downloadCount == 1 ? i18nc("Request installation of this item, available when there is exactly one downloadable item", "Install") : i18nc("Show installation options, where there is more than one downloadable item", "Install..."); + iconName: "install" + onTriggered: { + if (model.downloadCount == 1) { + newStuffModel.installItem(model.index, 1); + } else { + downloadItemsSheet.downloadLinks = model.downloadLinks; + downloadItemsSheet.entryId = model.index; + downloadItemsSheet.open(); + } + } + enabled: model.status == NewStuff.ItemsModel.DownloadableStatus || model.status == NewStuff.ItemsModel.DeletedStatus; + visible: enabled; + }, + Kirigami.Action { + text: i18nc("Request updating of this item", "Update"); + iconName: "update" + onTriggered: { newStuffModel.installItem(model.index); } + enabled: model.status == NewStuff.ItemsModel.UpdateableStatus; + visible: enabled; + }, + Kirigami.Action { + text: component.uninstallLabel + iconName: "uninstall" + onTriggered: { newStuffModel.uninstallItem(model.index); } + enabled: model.status == NewStuff.ItemsModel.InstalledStatus + visible: enabled; + }, + Kirigami.Action { + text: i18nc("Show a page with details for this item", "Details...") + iconName: "documentinfo" + onTriggered: { component.showDetails(); } + } + ] + thumbnailAvailable: model.previewsSmall.length > 0 + tile: Item { + anchors { + fill: parent + margins: Kirigami.Units.smallSpacing + } + QtLayouts.GridLayout { + anchors.fill: parent; + columns: 2 + QtLayouts.ColumnLayout { + QtLayouts.Layout.minimumWidth: view.implicitCellWidth / 5 + QtLayouts.Layout.maximumWidth: view.implicitCellWidth / 5 + Item { + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.minimumHeight: width + QtLayouts.Layout.maximumHeight: width + Image { + id: tilePreview + asynchronous: true; + fillMode: Image.PreserveAspectFit; + source: thumbnailAvailable ? model.previewsSmall[0] : ""; + anchors { + fill: parent + margins: Kirigami.Units.smallSpacing + } + verticalAlignment: Image.AlignTop + } + QtEffects.DropShadow { + anchors.fill: tilePreview + horizontalOffset: 0 + verticalOffset: 0 + radius: Kirigami.Units.largeSpacing + samples: radius * 2 + color: "#80000000" + source: tilePreview + } + Kirigami.Icon { + id: updateAvailableBadge; + opacity: (model.status == NewStuff.ItemsModel.UpdateableStatus) ? 1 : 0; + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } + anchors { + top: parent.top; + left: parent.left; + margins: -Kirigami.Units.smallSpacing; + } + height: Kirigami.Units.iconSizes.smallMedium; + width: height; + source: "package-installed-outdated"; + } + Kirigami.Icon { + id: installedBadge; + opacity: (model.status == NewStuff.ItemsModel.InstalledStatus) ? 1 : 0; + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } + anchors { + top: parent.top; + left: parent.left; + margins: -Kirigami.Units.smallSpacing; + } + height: Kirigami.Units.iconSizes.smallMedium; + width: height; + source: "package-installed-updated"; + } + } + Item { + QtLayouts.Layout.fillHeight: true + } + } + QtLayouts.ColumnLayout { + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.fillHeight: true + Kirigami.Heading { + QtLayouts.Layout.fillWidth: true + elide: Text.ElideRight + level: 3 + text: model.name + } + Kirigami.Heading { + QtLayouts.Layout.fillWidth: true + elide: Text.ElideRight + level: 4 + textFormat: Text.StyledText + text: i18nc("Subheading for the tile view, located immediately underneath the name of the item", "By %1").arg(model.author.name) + } + QtControls.Label { + QtLayouts.Layout.fillWidth: true + QtLayouts.Layout.fillHeight: true + wrapMode: Text.Wrap + text: model.shortSummary.length > 0 ? model.shortSummary : model.summary + elide: Text.ElideRight + } + } + Private.Rating { + QtLayouts.Layout.fillWidth: true + rating: model.rating / 10 + } + Kirigami.Heading { + QtLayouts.Layout.fillWidth: true + level: 5 + elide: Text.ElideRight + text: i18nc("The number of times the item has been downloaded", "%1 downloads").arg(model.downloadCount) + } + } + Item { + anchors.fill: parent + opacity: (model.status == NewStuff.ItemsModel.InstallingStatus || model.status == NewStuff.ItemsModel.UpdatingStatus) ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration; } } + Rectangle { + anchors.fill: parent + color: Kirigami.Theme.backgroundColor + opacity: 0.5; + } + QtControls.BusyIndicator { + anchors.centerIn: parent + running: parent.opacity > 0 + } + } + MouseArea { + anchors.fill: parent; + onClicked: component.showDetails(); + } + } +} diff --git a/src/qtquick/qmldir b/src/qtquick/qmldir index 2bbe78b7..94de5d0a 100644 --- a/src/qtquick/qmldir +++ b/src/qtquick/qmldir @@ -1,4 +1,11 @@ module org.kde.newstuff plugin newstuffqmlplugin NewStuffList 1.0 qml/NewStuffList.qml NewStuffItem 1.0 qml/NewStuffItem.qml +Button 1.1 qml/Button.qml +Dialog 1.1 qml/Dialog.qml +DialogContent 1.1 qml/DialogContent.qml +DownloadItemsSheet 1.1 qml/DownloadItemsSheet.qml +EntryDetails 1.1 qml/EntryDetails.qml +Page 1.1 qml/Page.qml +QuestionAsker 1.1 qml/QuestionAsker.qml diff --git a/src/qtquick/qmlplugin.cpp b/src/qtquick/qmlplugin.cpp index a1e8bcd6..6f60aeaa 100644 --- a/src/qtquick/qmlplugin.cpp +++ b/src/qtquick/qmlplugin.cpp @@ -1,41 +1,58 @@ /* * Copyright (C) 2016 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 "quickengine.h" #include "quickitemsmodel.h" +#include "quickquestionlistener.h" +#include "author.h" +#include "categoriesmodel.h" +#include "commentsmodel.h" #include "downloadlinkinfo.h" +#include "provider.h" +#include "question.h" + #include #include void QmlPlugins::initializeEngine(QQmlEngine *engine, const char *) { Q_UNUSED(engine); } void QmlPlugins::registerTypes(const char *uri) { qmlRegisterType(uri, 1, 0, "Engine"); qmlRegisterType(uri, 1, 0, "ItemsModel"); + qmlRegisterType(uri, 1, 62, "Author"); + qmlRegisterType(uri, 1, 62, "CommentsModel"); qmlRegisterUncreatableType(uri, 1, 0, "DownloadLinkInfo", QStringLiteral("This should only be created by the ItemsModel, and is associated with one entry in that model")); + qmlRegisterUncreatableType(uri, 1, 0, "CategoriesModel", QStringLiteral("This should only be created by the Engine, and provides the categories available in that engine")); + qmlRegisterUncreatableMetaObject(KNSCore::Provider::staticMetaObject, "org.kde.newstuff.core", 1, 62, "Provider", QLatin1String("Error: this only exists to forward enums")); + qmlRegisterUncreatableMetaObject(KNSCore::Question::staticMetaObject, "org.kde.newstuff.core", 1, 62, "Question", QLatin1String("Error: this only exists to forward enums")); + qmlRegisterSingletonType(uri, 1, 62, "QuickQuestionListener", [](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject * { + Q_UNUSED(scriptEngine) + engine->setObjectOwnership(KNewStuffQuick::QuickQuestionListener::instance(), QQmlEngine::CppOwnership); + return KNewStuffQuick::QuickQuestionListener::instance(); + }); } diff --git a/src/qtquick/quickengine.cpp b/src/qtquick/quickengine.cpp index 69bcfa2c..14124a10 100644 --- a/src/qtquick/quickengine.cpp +++ b/src/qtquick/quickengine.cpp @@ -1,71 +1,219 @@ /* * Copyright (C) 2016 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 "quickengine.h" +#include +#include + +#include "categoriesmodel.h" +#include "quickquestionlistener.h" + #include "engine.h" class Engine::Private { public: Private() : engine(nullptr) + , categoriesModel(nullptr) {} - KNSCore::Engine* engine; + KNSCore::Engine *engine; + CategoriesModel *categoriesModel; QString configFile; + KNSCore::EntryInternal::List changedEntries; }; -Engine::Engine(QObject* parent) +Engine::Engine(QObject *parent) : QObject(parent) , d(new Private) { } Engine::~Engine() { delete d; } +bool Engine::allowedByKiosk() const +{ + return KAuthorized::authorize(QStringLiteral("ghns")); +} + QString Engine::configFile() const { return d->configFile; } -void Engine::setConfigFile(const QString& newFile) +void Engine::setConfigFile(const QString &newFile) { - d->configFile = newFile; - emit configFileChanged(); + if (d->configFile != newFile) { + d->configFile = newFile; + emit configFileChanged(); - if(!d->engine) { - d->engine = new KNSCore::Engine(this); - connect(d->engine, &KNSCore::Engine::signalMessage, this, &Engine::message); - connect(d->engine, &KNSCore::Engine::signalIdle, this, &Engine::idleMessage); - connect(d->engine, &KNSCore::Engine::signalBusy, this, &Engine::busyMessage); - connect(d->engine, &KNSCore::Engine::signalError, this, &Engine::errorMessage); - emit engineChanged(); + if (allowedByKiosk()) { + if (!d->engine) { + d->engine = new KNSCore::Engine(this); + connect(d->engine, &KNSCore::Engine::signalMessage, this, &Engine::message); + connect(d->engine, &KNSCore::Engine::signalIdle, this, &Engine::idleMessage); + connect(d->engine, &KNSCore::Engine::signalBusy, this, &Engine::busyMessage); + connect(d->engine, &KNSCore::Engine::signalError, this, &Engine::errorMessage); + connect(d->engine, &KNSCore::Engine::signalErrorCode, this, [=](const KNSCore::ErrorCode &/*errorCode*/, const QString &message, const QVariant &/*metadata*/) { + emit errorMessage(message); + }); + connect(d->engine, &KNSCore::Engine::signalEntryChanged, this, [this](const KNSCore::EntryInternal &entry){ + d->changedEntries << entry; + emit changedEntriesChanged(); + }); + emit engineChanged(); + KNewStuffQuick::QuickQuestionListener::instance(); + d->categoriesModel = new CategoriesModel(this); + emit categoriesChanged(); + } + d->engine->init(d->configFile); + d->engine->setSortMode(KNSCore::Provider::Downloads); + emit engineInitialized(); + } else { + // This is not an error message in the proper sense, and the message is not intended to look like an error (as there is really + // nothing the user can do to fix it, and we just tell them so they're not wondering what's wrong) + emit message(i18nc("An informational message which is shown to inform the user they are not authorized to use GetHotNewStuff functionality", "You are not authorized to Get Hot New Stuff. If you think this is in error, please contact the person in charge of your permissions.")); + } } - d->engine->init(d->configFile); } -QObject * Engine::engine() const +QObject *Engine::engine() const { return d->engine; } + +bool Engine::hasAdoptionCommand() const +{ + if (d->engine) { + return d->engine->hasAdoptionCommand(); + } + return false; +} + +QString Engine::name() const +{ + if (d->engine) { + return d->engine->name(); + } + return QString{}; +} + +QObject *Engine::categories() const +{ + return d->categoriesModel; +} + +QStringList Engine::categoriesFilter() const +{ + if (d->engine) { + return d->engine->categoriesFilter(); + } + return QStringList{}; +} + +void Engine::setCategoriesFilter(const QStringList &newCategoriesFilter) +{ + if (d->engine) { + // This ensures that if we somehow end up with any empty entries (such as the default + // option in the categories dropdowns), our list will remain empty. + QStringList filter{newCategoriesFilter}; + filter.removeAll({}); + if (d->engine->categoriesFilter() != filter) { + d->engine->setCategoriesFilter(filter); + emit categoriesFilterChanged(); + } + } +} + +void Engine::resetCategoriesFilter() +{ + if (d->engine) { + d->engine->setCategoriesFilter(d->engine->categories()); + } +} + +int Engine::filter() const +{ + if (d->engine) { + d->engine->filter(); + } + return 0; +} + +void Engine::setFilter(int newFilter) +{ + if (d->engine && d->engine->filter() != newFilter) { + d->engine->setFilter(static_cast(newFilter)); + emit filterChanged(); + } +} + +int Engine::sortOrder() const +{ + if (d->engine) { + return d->engine->sortMode(); + } + return 0; +} + +void Engine::setSortOrder(int newSortOrder) +{ + if (d->engine && d->engine->sortMode() != newSortOrder) { + d->engine->setSortMode(static_cast(newSortOrder)); + emit sortOrderChanged(); + } +} + +QString Engine::searchTerm() const +{ + if (d->engine) { + return d->engine->searchTerm(); + } + return QString{}; +} + +void Engine::setSearchTerm(const QString &newSearchTerm) +{ + if (d->engine && d->engine->searchTerm() != newSearchTerm) { + d->engine->setSearchTerm(newSearchTerm); + emit searchTermChanged(); + } +} + +void Engine::resetSearchTerm() +{ + setSearchTerm(QString{}); +} + +KNSCore::EntryInternal::List Engine::changedEntries() const +{ + return d->changedEntries; +} + +void Engine::resetChangedEntries() +{ + d->changedEntries.clear(); + emit changedEntriesChanged(); +} diff --git a/src/qtquick/quickengine.h b/src/qtquick/quickengine.h index f6860616..e45c0e07 100644 --- a/src/qtquick/quickengine.h +++ b/src/qtquick/quickengine.h @@ -1,62 +1,102 @@ /* * Copyright (C) 2016 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 ENGINE_H #define ENGINE_H #include +#include /** * @short Encapsulates a KNSCore::Engine for use in Qt Quick * * This class takes care of initialisation of a KNSCore::Engine when assigned a config file. * The actual KNSCore:Engine can be read through the Engine::engine property. * * @see ItemsModel */ class Engine : public QObject { Q_OBJECT + Q_PROPERTY(bool allowedByKiosk READ allowedByKiosk CONSTANT) Q_PROPERTY(QString configFile READ configFile WRITE setConfigFile NOTIFY configFileChanged) Q_PROPERTY(QObject* engine READ engine NOTIFY engineChanged) + Q_PROPERTY(bool hasAdoptionCommand READ hasAdoptionCommand NOTIFY engineInitialized) + Q_PROPERTY(QString name READ name NOTIFY engineInitialized) + Q_PROPERTY(QObject* categories READ categories NOTIFY categoriesChanged) + Q_PROPERTY(QStringList categoriesFilter READ categoriesFilter WRITE setCategoriesFilter RESET resetCategoriesFilter NOTIFY categoriesFilterChanged) + Q_PROPERTY(int filter READ filter WRITE setFilter NOTIFY filterChanged) + Q_PROPERTY(int sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged) + Q_PROPERTY(QString searchTerm READ searchTerm WRITE setSearchTerm RESET resetSearchTerm NOTIFY searchTermChanged) + Q_PROPERTY(KNSCore::EntryInternal::List changedEntries READ changedEntries RESET resetChangedEntries NOTIFY changedEntriesChanged) public: - explicit Engine(QObject* parent = nullptr); + explicit Engine(QObject *parent = nullptr); virtual ~Engine(); + bool allowedByKiosk() const; + QString configFile() const; - void setConfigFile(const QString& newFile); + void setConfigFile(const QString &newFile); Q_SIGNAL void configFileChanged(); - QObject* engine() const; + QObject *engine() const; Q_SIGNAL void engineChanged(); + bool hasAdoptionCommand() const; + QString name() const; + Q_SIGNAL void engineInitialized(); + + QObject *categories() const; + Q_SIGNAL void categoriesChanged(); + + QStringList categoriesFilter() const; + void setCategoriesFilter(const QStringList &newCategoriesFilter); + Q_INVOKABLE void resetCategoriesFilter(); + Q_SIGNAL void categoriesFilterChanged(); + + int filter() const; + void setFilter(int newFilter); + Q_SIGNAL void filterChanged(); + + int sortOrder() const; + void setSortOrder(int newSortOrder); + Q_SIGNAL void sortOrderChanged(); + + QString searchTerm() const; + void setSearchTerm(const QString &newSearchTerm); + Q_INVOKABLE void resetSearchTerm(); + Q_SIGNAL void searchTermChanged(); + + KNSCore::EntryInternal::List changedEntries() const; + Q_INVOKABLE void resetChangedEntries(); + Q_SIGNAL void changedEntriesChanged(); Q_SIGNALS: - void message(const QString& message); - void idleMessage(const QString& message); - void busyMessage(const QString& message); - void errorMessage(const QString& message); + void message(const QString &message); + void idleMessage(const QString &message); + void busyMessage(const QString &message); + void errorMessage(const QString &message); private: class Private; - Private* d; + Private *d; }; #endif//ENGINE_H diff --git a/src/qtquick/quickitemsmodel.cpp b/src/qtquick/quickitemsmodel.cpp index 8da6397d..4551816f 100644 --- a/src/qtquick/quickitemsmodel.cpp +++ b/src/qtquick/quickitemsmodel.cpp @@ -1,367 +1,421 @@ /* * Copyright (C) 2016 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 "quickitemsmodel.h" #include "quickengine.h" +#include "knewstuffquick_debug.h" #include "itemsmodel.h" #include "engine.h" #include "downloadlinkinfo.h" +#include "core/commentsmodel.h" + +#include +#include +#include class ItemsModel::Private { public: - Private(ItemsModel* qq) + Private(ItemsModel *qq) : q(qq) , model(nullptr) , engine(nullptr) + , coreEngine(nullptr) {} - ItemsModel* q; - KNSCore::ItemsModel* model; - KNSCore::Engine* engine; + ~Private() + { + qDeleteAll(commentsModels); + } + ItemsModel *q; + KNSCore::ItemsModel *model; + Engine *engine; + KNSCore::Engine *coreEngine; + + QHash commentsModels; bool initModel() { - if(model) { + if (model) { return true; } - if(!engine) { + if (!coreEngine) { return false; } - model = new KNSCore::ItemsModel(engine, q); + model = new KNSCore::ItemsModel(coreEngine, q); - q->connect(engine, &KNSCore::Engine::signalProvidersLoaded, engine, &KNSCore::Engine::reloadEntries); + q->connect(coreEngine, &KNSCore::Engine::signalProvidersLoaded, coreEngine, &KNSCore::Engine::reloadEntries); // Entries have been fetched and should be shown: - q->connect(engine, &KNSCore::Engine::signalEntriesLoaded, model, &KNSCore::ItemsModel::slotEntriesLoaded); + q->connect(coreEngine, &KNSCore::Engine::signalEntriesLoaded, model, &KNSCore::ItemsModel::slotEntriesLoaded); // An entry has changes - eg because it was installed - q->connect(engine, &KNSCore::Engine::signalEntryChanged, model, &KNSCore::ItemsModel::slotEntryChanged); + q->connect(coreEngine, &KNSCore::Engine::signalEntryChanged, model, &KNSCore::ItemsModel::slotEntryChanged); + q->connect(coreEngine, &KNSCore::Engine::signalEntryChanged, q, [=](const KNSCore::EntryInternal &entry){ + emit q->entryChanged(model->row(entry)); + }); - q->connect(engine, &KNSCore::Engine::signalResetView, model, &KNSCore::ItemsModel::clearEntries); - q->connect(engine, &KNSCore::Engine::signalEntryPreviewLoaded, model, &KNSCore::ItemsModel::slotEntryPreviewLoaded); + q->connect(coreEngine, &KNSCore::Engine::signalResetView, model, &KNSCore::ItemsModel::clearEntries); + q->connect(coreEngine, &KNSCore::Engine::signalEntryPreviewLoaded, model, &KNSCore::ItemsModel::slotEntryPreviewLoaded); q->connect(model, &KNSCore::ItemsModel::rowsInserted, q, &ItemsModel::rowsInserted); q->connect(model, &KNSCore::ItemsModel::rowsRemoved, q, &ItemsModel::rowsRemoved); q->connect(model, &KNSCore::ItemsModel::dataChanged, q, &ItemsModel::dataChanged); q->connect(model, &KNSCore::ItemsModel::modelReset, q, &ItemsModel::modelReset); return true; } }; -ItemsModel::ItemsModel(QObject* parent) +ItemsModel::ItemsModel(QObject *parent) : QAbstractListModel(parent) , d(new Private(this)) { } ItemsModel::~ItemsModel() { delete d; } QHash ItemsModel::roleNames() const { static const QHash roles = QHash{ {Qt::DisplayRole, "display"}, {NameRole, "name"}, {UniqueIdRole, "uniqueId"}, {CategoryRole, "category"}, {HomepageRole, "homepage"}, {AuthorRole, "author"}, {LicenseRole, "license"}, {ShortSummaryRole, "shortSummary"}, {SummaryRole, "summary"}, {ChangelogRole, "changelog"}, {VersionRole, "version"}, {ReleaseDateRole, "releaseDate"}, {UpdateVersionRole, "updateVersion"}, {UpdateReleaseDateRole, "updateReleaseDate"}, {PayloadRole, "payload"}, {Qt::DecorationRole, "decoration"}, {PreviewsSmallRole, "previewsSmall"}, {PreviewsRole, "previews"}, {InstalledFilesRole, "installedFiles"}, {UnInstalledFilesRole, "uninstalledFiles"}, {RatingRole, "rating"}, {NumberOfCommentsRole, "numberOfComments"}, {DownloadCountRole, "downloadCount"}, {NumberFansRole, "numberFans"}, {NumberKnowledgebaseEntriesRole, "numberKnowledgebaseEntries"}, {KnowledgebaseLinkRole, "knowledgebaseLink"}, {DownloadLinksRole, "downloadLinks"}, {DonationLinkRole, "donationLink"}, {ProviderIdRole, "providerId"}, {SourceRole, "source"}, {StatusRole, "status"} }; return roles; } int ItemsModel::rowCount(const QModelIndex& parent) const { - if(parent.isValid()) + if (parent.isValid()) return 0; - if(d->initModel()) + if (d->initModel()) return d->model->rowCount(QModelIndex()); return 0; } -QVariant ItemsModel::data(const QModelIndex& index, int role) const +QVariant ItemsModel::data(const QModelIndex &index, int role) const { QVariant data; - if(index.isValid() && d->initModel()) + if (index.isValid() && d->initModel()) { KNSCore::EntryInternal entry = d->model->data(d->model->index(index.row()), Qt::UserRole).value(); - switch(role) + switch (role) { case NameRole: case Qt::DisplayRole: data.setValue(entry.name()); break; case UniqueIdRole: data.setValue(entry.uniqueId()); break; case CategoryRole: data.setValue(entry.category()); break; case HomepageRole: data.setValue(entry.homepage()); break; case AuthorRole: { KNSCore::Author author = entry.author(); QVariantMap returnAuthor; + returnAuthor[QStringLiteral("id")] = author.id(); returnAuthor[QStringLiteral("name")] = author.name(); returnAuthor[QStringLiteral("email")] = author.email(); returnAuthor[QStringLiteral("homepage")] = author.homepage(); returnAuthor[QStringLiteral("jabber")] = author.jabber(); + returnAuthor[QStringLiteral("avatarUrl")] = author.avatarUrl(); + returnAuthor[QStringLiteral("description")] = author.description(); data.setValue<>(returnAuthor); } break; case LicenseRole: data.setValue(entry.license()); break; case ShortSummaryRole: data.setValue(entry.shortSummary()); break; case SummaryRole: data.setValue(entry.summary()); break; case ChangelogRole: data.setValue(entry.changelog()); break; case VersionRole: data.setValue(entry.version()); break; case ReleaseDateRole: data.setValue(entry.releaseDate()); break; case UpdateVersionRole: data.setValue(entry.updateVersion()); break; case UpdateReleaseDateRole: data.setValue(entry.updateReleaseDate()); break; case PayloadRole: data.setValue(entry.payload()); break; case Qt::DecorationRole: data.setValue(entry.previewUrl(KNSCore::EntryInternal::PreviewSmall1)); break; case PreviewsSmallRole: { QStringList previews; previews << entry.previewUrl(KNSCore::EntryInternal::PreviewSmall1); previews << entry.previewUrl(KNSCore::EntryInternal::PreviewSmall2); previews << entry.previewUrl(KNSCore::EntryInternal::PreviewSmall3); while(!previews.isEmpty() && previews.last().isEmpty()) { previews.takeLast(); } data.setValue(previews); } break; case PreviewsRole: { QStringList previews; previews << entry.previewUrl(KNSCore::EntryInternal::PreviewBig1); previews << entry.previewUrl(KNSCore::EntryInternal::PreviewBig2); previews << entry.previewUrl(KNSCore::EntryInternal::PreviewBig3); while(!previews.isEmpty() && previews.last().isEmpty()) { previews.takeLast(); } data.setValue(previews); } break; case InstalledFilesRole: data.setValue(entry.installedFiles()); break; case UnInstalledFilesRole: data.setValue(entry.uninstalledFiles()); break; case RatingRole: data.setValue(entry.rating()); break; case NumberOfCommentsRole: data.setValue(entry.numberOfComments()); break; case DownloadCountRole: data.setValue(entry.downloadCount()); break; case NumberFansRole: data.setValue(entry.numberFans()); break; case NumberKnowledgebaseEntriesRole: data.setValue(entry.numberKnowledgebaseEntries()); break; case KnowledgebaseLinkRole: data.setValue(entry.knowledgebaseLink()); break; case DownloadLinksRole: { // This would be good to cache... but it also needs marking as dirty, somehow... const QList dllinks = entry.downloadLinkInformationList(); QObjectList list; - for(const KNSCore::EntryInternal::DownloadLinkInformation& link : dllinks) + for(const KNSCore::EntryInternal::DownloadLinkInformation &link : dllinks) { - DownloadLinkInfo* info = new DownloadLinkInfo(); + DownloadLinkInfo *info = new DownloadLinkInfo(); info->setData(link); list.append(info); } data.setValue(list); } break; case DonationLinkRole: data.setValue(entry.donationLink()); break; case ProviderIdRole: data.setValue(entry.providerId()); break; case SourceRole: { KNSCore::EntryInternal::Source src = entry.source(); switch(src) { case KNSCore::EntryInternal::Cache: data.setValue(QStringLiteral("Cache")); break; case KNSCore::EntryInternal::Online: data.setValue(QStringLiteral("Online")); break; case KNSCore::EntryInternal::Registry: data.setValue(QStringLiteral("Registry")); break; default: data.setValue(QStringLiteral("Unknown source - shouldn't be possible")); break; } } break; case StatusRole: { KNS3::Entry::Status status = entry.status(); switch(status) { case KNS3::Entry::Downloadable: data.setValue(ItemsModel::DownloadableStatus); break; case KNS3::Entry::Installed: data.setValue(ItemsModel::InstalledStatus); break; case KNS3::Entry::Updateable: data.setValue(ItemsModel::UpdateableStatus); break; case KNS3::Entry::Deleted: data.setValue(ItemsModel::DeletedStatus); break; case KNS3::Entry::Installing: data.setValue(ItemsModel::InstallingStatus); break; case KNS3::Entry::Updating: data.setValue(ItemsModel::UpdatingStatus); break; case KNS3::Entry::Invalid: default: data.setValue(ItemsModel::InvalidStatus); break; } } break; + case CommentsModelRole: + { + KNSCore::CommentsModel *commentsModel{nullptr}; + if (!d->commentsModels.contains(entry.uniqueId())) { + commentsModel = d->coreEngine->commentsForEntry(entry); + d->commentsModels[entry.uniqueId()] = commentsModel; + } else { + commentsModel = d->commentsModels[entry.uniqueId()]; + } + data.setValue(commentsModel); + } + break; default: data.setValue(QStringLiteral("Unknown role")); break; } } return data; } -bool ItemsModel::canFetchMore(const QModelIndex& parent) const +bool ItemsModel::canFetchMore(const QModelIndex &parent) const { - if(parent.isValid()) { - return false; + if (!parent.isValid() && d->coreEngine && d->coreEngine->categoriesMetadata().count() > 0) { + return true; } - return true; + return false; } -void ItemsModel::fetchMore(const QModelIndex& parent) +void ItemsModel::fetchMore(const QModelIndex &parent) { - if(parent.isValid()) { + if (parent.isValid() || !d->coreEngine) { return; } - d->engine->requestMoreData(); + d->coreEngine->requestMoreData(); } -QObject * ItemsModel::engine() const +QObject *ItemsModel::engine() const { return d->engine; } -void ItemsModel::setEngine(QObject* newEngine) +void ItemsModel::setEngine(QObject *newEngine) { - beginResetModel(); - Engine* test = qobject_cast(newEngine); - if(test) { - d->engine = qobject_cast(test->engine()); - } - else { - d->engine = qobject_cast(newEngine); + if (d->engine != newEngine) { + beginResetModel(); + d->engine = qobject_cast(newEngine); + d->model->deleteLater(); + d->model = nullptr; + d->coreEngine = nullptr; + if (d->engine) { + d->coreEngine = qobject_cast(d->engine->engine()); + } + connect(d->engine, &Engine::engineChanged, this, [this](){ + beginResetModel(); + d->model->deleteLater(); + d->model = nullptr; + d->coreEngine = qobject_cast(d->engine->engine()); + endResetModel(); + }); + emit engineChanged(); + endResetModel(); } - emit engineChanged(); - endResetModel(); } -void ItemsModel::installItem(int index) +void ItemsModel::installItem(int index, int linkId) { - if(d->engine) { + if (d->coreEngine) { KNSCore::EntryInternal entry = d->model->data(d->model->index(index), Qt::UserRole).value(); if(entry.isValid()) { - d->engine->install(entry); + d->coreEngine->install(entry, linkId); } } } void ItemsModel::uninstallItem(int index) { - if(d->engine) { + if (d->coreEngine) { KNSCore::EntryInternal entry = d->model->data(d->model->index(index), Qt::UserRole).value(); if(entry.isValid()) { - d->engine->uninstall(entry); + d->coreEngine->uninstall(entry); + } + } +} + +void ItemsModel::adoptItem(int index) +{ + if (d->coreEngine) { + KNSCore::EntryInternal entry = d->model->data(d->model->index(index), Qt::UserRole).value(); + if (entry.isValid()) { + QStringList args = KShell::splitArgs(d->coreEngine->adoptionCommand(entry)); + qCDebug(KNEWSTUFFQUICK) << "executing AdoptionCommand" << args; + QProcess::startDetached(args.takeFirst(), args); + d->engine->idleMessage(i18n("Using %1").arg(entry.name())); } } } diff --git a/src/qtquick/quickitemsmodel.h b/src/qtquick/quickitemsmodel.h index cbb84397..8f6d4647 100644 --- a/src/qtquick/quickitemsmodel.h +++ b/src/qtquick/quickitemsmodel.h @@ -1,148 +1,172 @@ /* * Copyright (C) 2016 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 ITEMSMODEL_H #define ITEMSMODEL_H #include /** * @short A model which shows the contents found in an Engine * * Use an instance of this model to show the content items represented by the configuration * file passed to an engine. The following sample assumes you are using the Engine component, * however it is also possible to pass a KNSCore::Engine instance created from C++ to this * property, if you have specific requirements not covered by the convenience component. * * Most data in the model is simple, but the DownloadLinks role will return a list of * DownloadLinkInfo entries, which you will need to manage in some way. * - * You might also look at NewStuffList and NewStuffItem to see some more detail on what can be - * done with the data. + * You might also look at NewStuffList, NewStuffItem, and the other items, to see some more + * detail on what can be done with the data. * * @see NewStuffList * @see NewStuffItem + * @see NewStuffPage + * @see NewStuffEntryDetails + * @see NewStuffEntryComments * * \code import org.kde.newstuff 1.0 as NewStuff Item { NewStuff.ItemsModel { id: newStuffModel; engine: newStuffEngine.engine; } NewStuff.Engine { id: newStuffEngine; configFile: "/some/filesystem/location/wallpaper.knsrc"; onMessage: console.log("KNS Message: " + message); onIdleMessage: console.log("KNS Idle: " + message); onBusyMessage: console.log("KNS Busy: " + message); onErrorMessage: console.log("KNS Error: " + message); } } \endcode */ class ItemsModel : public QAbstractListModel { Q_OBJECT + /** + * The NewStuffQuickEngine to show items from + */ Q_PROPERTY(QObject* engine READ engine WRITE setEngine NOTIFY engineChanged) public: - explicit ItemsModel(QObject* parent = nullptr); + explicit ItemsModel(QObject *parent = nullptr); virtual ~ItemsModel(); enum Roles { NameRole = Qt::UserRole + 1, UniqueIdRole, CategoryRole, HomepageRole, AuthorRole, LicenseRole, ShortSummaryRole, SummaryRole, ChangelogRole, VersionRole, ReleaseDateRole, UpdateVersionRole, UpdateReleaseDateRole, PayloadRole, PreviewsSmallRole, ///@< this will return a list here, rather than be tied so tightly to the remote api PreviewsRole, ///@< this will return a list here, rather than be tied so tightly to the remote api InstalledFilesRole, UnInstalledFilesRole, RatingRole, NumberOfCommentsRole, DownloadCountRole, NumberFansRole, NumberKnowledgebaseEntriesRole, KnowledgebaseLinkRole, DownloadLinksRole, DonationLinkRole, ProviderIdRole, SourceRole, - StatusRole + StatusRole, + CommentsModelRole }; + Q_ENUM(Roles) enum ItemStatus { InvalidStatus, DownloadableStatus, InstalledStatus, UpdateableStatus, DeletedStatus, InstallingStatus, UpdatingStatus }; Q_ENUM(ItemStatus) QHash< int, QByteArray > roleNames() const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; bool canFetchMore(const QModelIndex & parent) const override; void fetchMore(const QModelIndex & parent) override; - QObject* engine() const; - void setEngine(QObject* newEngine); + QObject *engine() const; + void setEngine(QObject *newEngine); Q_SIGNAL void engineChanged(); /** * @brief This will install (or update, if already installed) the item at the given index * * There are no side effects of this function if it is called on an item which cannot be * installed or updated (that is, if the status is not one such that these are possible, * the function will simply return without performing any actions) * * @param index The index of the item to install or update */ - Q_INVOKABLE void installItem(int index); + Q_INVOKABLE void installItem(int index, int linkId); /** * @brief Uninstall an already installed item * * There are no side effects of this function if it is called on an item which cannot be * uninstalled (that is, if the status is not one such that this is possible, * the function will simply return without performing any actions) * * @param index The index of the item to be uninstalled */ Q_INVOKABLE void uninstallItem(int index); + + /** + * @brief Run the adoption command on an already installed item + * + * @note This will simply fail quietly if the item is not installed + * + * @param index The intex of the item to be adopted + */ + Q_INVOKABLE void adoptItem(int index); + + /** + * @brief Fired when an entry's data changes + * + * @param index The index of the item which has changed + */ + Q_SIGNAL void entryChanged(int index); private: class Private; - Private* d; + Private *d; }; Q_DECLARE_METATYPE(ItemsModel::ItemStatus) #endif//ITEMSMODEL_H diff --git a/src/qtquick/quickquestionlistener.cpp b/src/qtquick/quickquestionlistener.cpp new file mode 100644 index 00000000..f60d90ff --- /dev/null +++ b/src/qtquick/quickquestionlistener.cpp @@ -0,0 +1,129 @@ +/* + This file is part of KNewStuffQuick. + + Copyright (c) 2016 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) any later version. + + 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 "quickquestionlistener.h" + +#include "core/question.h" + +#include + +using namespace KNewStuffQuick; + +class QuickQuestionListenerHelper { +public: + QuickQuestionListenerHelper() : q(nullptr) {} + ~QuickQuestionListenerHelper() { } + QuickQuestionListenerHelper(const QuickQuestionListenerHelper &) = delete; + QuickQuestionListenerHelper& operator=(const QuickQuestionListenerHelper &) = delete; + QuickQuestionListener *q; +}; +Q_GLOBAL_STATIC(QuickQuestionListenerHelper, s_kns3_quickQuestionListener) + +class QuickQuestionListener::Private { +public: + Private() {} + KNSCore::Question *question = nullptr; +}; + +QuickQuestionListener *QuickQuestionListener::instance() +{ + if(!s_kns3_quickQuestionListener()->q) { + new QuickQuestionListener; + } + return s_kns3_quickQuestionListener()->q; +} + +QuickQuestionListener::QuickQuestionListener() + : KNSCore::QuestionListener(nullptr) + , d(new Private) +{ + setParent(qApp); + s_kns3_quickQuestionListener()->q = this; +} + +QuickQuestionListener::~QuickQuestionListener() +{ + if (d->question) { + d->question->setResponse(KNSCore::Question::CancelResponse); + } + delete d; +} + +void QuickQuestionListener::askQuestion(KNSCore::Question *question) +{ + d->question = question; + switch(question->questionType()) + { + case KNSCore::Question::SelectFromListQuestion: + emit askListQuestion(question->title(), question->question(), question->list()); + break; + case KNSCore::Question::ContinueCancelQuestion: + emit askContinueCancelQuestion(d->question->title(), d->question->question()); + break; + case KNSCore::Question::InputTextQuestion: + emit askTextInputQuestion(d->question->title(), d->question->question()); + break; + case KNSCore::Question::PasswordQuestion: + emit askPasswordQuestion(d->question->title(), d->question->question()); + break; + case KNSCore::Question::YesNoQuestion: + default: + emit askYesNoQuestion(d->question->title(), d->question->question()); + break; + } +} + +void KNewStuffQuick::QuickQuestionListener::passResponse(bool responseIsContinue, QString input) +{ + if (d->question) { + if (responseIsContinue) { + d->question->setResponse(input); + switch(d->question->questionType()) + { + case KNSCore::Question::ContinueCancelQuestion: + d->question->setResponse(KNSCore::Question::ContinueResponse); + break; + case KNSCore::Question::YesNoQuestion: + d->question->setResponse(KNSCore::Question::YesResponse); + break; + case KNSCore::Question::SelectFromListQuestion: + case KNSCore::Question::InputTextQuestion: + case KNSCore::Question::PasswordQuestion: + default: + d->question->setResponse(KNSCore::Question::OKResponse); + break; + } + } else { + switch(d->question->questionType()) + { + case KNSCore::Question::YesNoQuestion: + d->question->setResponse(KNSCore::Question::NoResponse); + break; + case KNSCore::Question::SelectFromListQuestion: + case KNSCore::Question::InputTextQuestion: + case KNSCore::Question::PasswordQuestion: + case KNSCore::Question::ContinueCancelQuestion: + default: + d->question->setResponse(KNSCore::Question::CancelResponse); + break; + } + } + d->question = nullptr; + } +} diff --git a/src/qtquick/quickquestionlistener.h b/src/qtquick/quickquestionlistener.h new file mode 100644 index 00000000..42e0bcf5 --- /dev/null +++ b/src/qtquick/quickquestionlistener.h @@ -0,0 +1,51 @@ +/* + This file is part of KNewStuffQuick. + + Copyright (c) 2019 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) any later version. + + 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 KNSQ_QUICKQUESTIONLISTENER_H +#define KNSQ_QUICKQUESTIONLISTENER_H + +#include "core/questionlistener.h" + +namespace KNewStuffQuick +{ +class QuickQuestionListener : public KNSCore::QuestionListener +{ + Q_OBJECT + Q_DISABLE_COPY(QuickQuestionListener) +public: + static QuickQuestionListener *instance(); + virtual ~QuickQuestionListener(); + + Q_SLOT void askQuestion(KNSCore::Question *question) override; + + Q_SIGNAL void askListQuestion(QString title, QString question, QStringList list); + Q_SIGNAL void askContinueCancelQuestion(QString title, QString question); + Q_SIGNAL void askTextInputQuestion(QString title, QString question); + Q_SIGNAL void askPasswordQuestion(QString title, QString question); + Q_SIGNAL void askYesNoQuestion(QString title, QString question); + + Q_SLOT void passResponse(bool responseIsContinue, QString input); +private: + QuickQuestionListener(); + class Private; + Private *d; +}; +} + +#endif//KNSQ_QUICKQUESTIONLISTENER_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d9170e73..e3f8fbc4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,33 +1,34 @@ include(ECMMarkAsTest) find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Test Widgets Gui Quick) # Widgets for KMoreTools and Quick for the interactive KNS test configure_file(khotnewstuff_test.knsrc.in khotnewstuff_test.knsrc @ONLY) macro(knewstuff_executable_tests) foreach(_testname ${ARGN}) add_executable(${_testname} ${_testname}.cpp ../src/knewstuff_debug.cpp ../src/core/knewstuffcore_debug.cpp ../src/staticxml/staticxmlprovider.cpp) target_link_libraries(${_testname} KF5::NewStuffCore KF5::NewStuff KF5::I18n Qt5::Xml Qt5::Test Qt5::Quick Qt5::Gui) target_compile_definitions(${_testname} PRIVATE KNSSRCDIR="${CMAKE_CURRENT_SOURCE_DIR}/" KNSBUILDDIR="${CMAKE_CURRENT_BINARY_DIR}") endforeach() endmacro() knewstuff_executable_tests( khotnewstuff khotnewstuff_upload khotnewstuff_test + khotnewstuff-dialog ) # FIXME: port to new API #knewstuff_executable_tests( # knewstuff2_download # knewstuff2_standard # knewstuff2_cache #) # KMoreTools: add_executable(kmoretoolstest_interactive kmoretools/kmoretoolstest_interactive.cpp ../src/knewstuff_debug.cpp) ecm_mark_as_test(kmoretoolstest_interactive) target_link_libraries(kmoretoolstest_interactive Qt5::Test KF5::NewStuff KF5::I18n Qt5::Widgets) diff --git a/tests/khotnewstuff-dialog-ui/main.qml b/tests/khotnewstuff-dialog-ui/main.qml new file mode 100644 index 00000000..30cbef76 --- /dev/null +++ b/tests/khotnewstuff-dialog-ui/main.qml @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 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.7 +import QtQuick.Dialogs 1.3 as QtDialogs +import QtQuick.Layouts 1.12 as QtLayouts +import org.kde.kirigami 2.5 as Kirigami +import org.kde.newstuff 1.62 as NewStuff + +Kirigami.ApplicationWindow { + id: root; + title: "KNewStuff Dialog" + + globalDrawer: Kirigami.GlobalDrawer { + title: "KNewStuff Dialog" + titleIcon: "get-hot-new-stuff" + drawerOpen: true; + modal: false; + topContent: NewStuff.Button { + id: newStuffButton + QtLayouts.Layout.fillWidth: true + configFile: knsrcfile + } + + actions: [ + Kirigami.Action { + text: "Find Different Hot New Stuff..." + icon.name: "document-open" + onTriggered: { + fileDialog.open(); + } + } + ] + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + + pageStack.defaultColumnWidth: pageStack.width + Component { + id: mainPageComponent + NewStuff.Page { } + } + Component.onCompleted: { + pageStack.push(mainPageComponent, { configFile: knsrcfile } ); + } + + QtDialogs.FileDialog { + id: fileDialog + title: "Open KNewStuff configuration file" + folder: knsrcFilesLocation + nameFilters: [ "KNewStuff Configuration Files (*.knsrc)", "All Files (*)" ] + onAccepted: { + newStuffButton.configFile = fileDialog.fileUrl.toString().substring(7) + pageStack.clear(); + pageStack.push(mainPageComponent, { configFile: newStuffButton.configFile }); + } + } +} diff --git a/tests/khotnewstuff-dialog.cpp b/tests/khotnewstuff-dialog.cpp new file mode 100644 index 00000000..f2fc5739 --- /dev/null +++ b/tests/khotnewstuff-dialog.cpp @@ -0,0 +1,52 @@ +/* + This file is part of KNewStuff2. + Copyright (c) 2019 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) any later version. + + 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 + +#include +#include +#include +#include +#include + +#include "engine.h" + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + QCoreApplication::setApplicationName(QStringLiteral("khotnewstuff-dialog")); + QCoreApplication::setApplicationVersion(QStringLiteral("0.1")); + QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); + + QCommandLineParser *parser = new QCommandLineParser; + parser->addHelpOption(); + parser->addPositionalArgument(QStringLiteral("knsrcfile"), i18n("The KNSRC file you want to show. If none is passed, we will use khotnewstuff_test.knsrc, which must be installed.")); + parser->process(app); + + QQmlApplicationEngine *appengine = new QQmlApplicationEngine(); + if (parser->positionalArguments().count() > 0) { + appengine->rootContext()->setContextProperty(QLatin1String("knsrcfile"), parser->positionalArguments().first()); + } else { + appengine->rootContext()->setContextProperty(QLatin1String("knsrcfile"), QString::fromLatin1("%1/khotnewstuff_test.knsrc").arg(QStringLiteral(KNSBUILDDIR))); + } + appengine->rootContext()->setContextProperty(QLatin1String("knsrcFilesLocation"), KNSCore::Engine::configSearchLocations().last()); + + appengine->load(QUrl::fromLocalFile(QString::fromLatin1("%1/khotnewstuff-dialog-ui/main.qml").arg(QStringLiteral(KNSSRCDIR)))); + + return app.exec(); +}