diff --git a/src/attica/atticaprovider.cpp b/src/attica/atticaprovider.cpp index cd334efc..79b6b62c 100644 --- a/src/attica/atticaprovider.cpp +++ b/src/attica/atticaprovider.cpp @@ -1,611 +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()); + emit signalErrorCode(KNSCore::OcsError, i18n("The Open Collaboration Services instance %1 does not support the attempted function.", 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/qtquick/qml/Dialog.qml b/src/qtquick/qml/Dialog.qml index 66d86f24..dee51dff 100644 --- a/src/qtquick/qml/Dialog.qml +++ b/src/qtquick/qml/Dialog.qml @@ -1,110 +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) + title: i18n("Download New %1", 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/EntryDetails.qml b/src/qtquick/qml/EntryDetails.qml index 9834d993..c10307ef 100644 --- a/src/qtquick/qml/EntryDetails.qml +++ b/src/qtquick/qml/EntryDetails.qml @@ -1,215 +1,215 @@ /* * 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); + applicationWindow().showPassiveNotification(i18nc("A passive notification shown when installation of an item is initiated", "Installing %1 from %2", downloadName, 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); + 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...", component.name, 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); + 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...", component.name, 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) + title: i18nc("Combined title for the entry details page made of the name of the entry, and the author's name", "%1 by %2", component.name, entryAuthor.name) actions { contextualActions: [ Kirigami.Action { 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" onTriggered: { 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; }, Kirigami.Action { text: i18nc("Request updating of this item", "Update"); icon.name: "update" onTriggered: { newStuffModel.installItem(component.index); } enabled: component.status == NewStuff.ItemsModel.UpdateableStatus; visible: enabled; }, Kirigami.Action { text: i18nc("Request uninstallation of this item", "Uninstall"); icon.name: "uninstall" onTriggered: { 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 comments (comments with or without ratings) for this entry", "%1 Reviews and Comments").arg(component.commentsCount) + text: i18nc("A link which, when clicked, opens a new sub page with comments (comments with or without ratings) for this entry", "%1 Reviews and Comments", 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) + 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 %1", 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/private/EntryCommentDelegate.qml b/src/qtquick/qml/private/EntryCommentDelegate.qml index 65107460..910f6b3f 100644 --- a/src/qtquick/qml/private/EntryCommentDelegate.qml +++ b/src/qtquick/qml/private/EntryCommentDelegate.qml @@ -1,189 +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 + 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)", 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 index 4615b672..74948fb6 100644 --- a/src/qtquick/qml/private/EntryCommentsPage.qml +++ b/src/qtquick/qml/private/EntryCommentsPage.qml @@ -1,88 +1,88 @@ /* * 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) + title: i18nc("Title for the page containing a view of the comments for the entry", "Comments and Reviews for %1", component.entryName) actions { contextualActions: [ Kirigami.Action { text: i18nc("Title for the item which is checked when all comments should be shown", "Show All Comments") checked: commentsModel.includedComments == NewStuff.CommentsModel.IncludeAllComments checkable: true onTriggered: commentsModel.includedComments = NewStuff.CommentsModel.IncludeAllComments }, Kirigami.Action { text: i18nc("Title for the item which is checked when only comments which are reviews should be shown", "Show Reviews Only") checked: commentsModel.includedComments == NewStuff.CommentsModel.IncludeOnlyReviews checkable: true onTriggered: commentsModel.includedComments = NewStuff.CommentsModel.IncludeOnlyReviews }, Kirigami.Action { text: i18nc("Title for the item which is checked when comments which are reviews, and their children should be shown", "Show Reviews and Replies") checked: commentsModel.includedComments == NewStuff.CommentsModel.IncludeReviewsAndReplies checkable: true onTriggered: commentsModel.includedComments = NewStuff.CommentsModel.IncludeReviewsAndReplies } ] } 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/entrygriddelegates/BigPreviewDelegate.qml b/src/qtquick/qml/private/entrygriddelegates/BigPreviewDelegate.qml index a0ef210e..3f0c36ad 100644 --- a/src/qtquick/qml/private/entrygriddelegates/BigPreviewDelegate.qml +++ b/src/qtquick/qml/private/entrygriddelegates/BigPreviewDelegate.qml @@ -1,213 +1,213 @@ /* * 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.downloadLinks.length === 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.downloadLinks.length === 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) + text: i18nc("The number of times the item has been downloaded", "%1 downloads", 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) + text: i18nc("Subheading for the tile view, located immediately underneath the name of the item", "By %1", 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 } } FeedbackOverlay { anchors.fill: parent newStuffModel: component.GridView.view.model } MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: component.showDetails(); } } } diff --git a/src/qtquick/qml/private/entrygriddelegates/TileDelegate.qml b/src/qtquick/qml/private/entrygriddelegates/TileDelegate.qml index d87c0c9a..97241c6c 100644 --- a/src/qtquick/qml/private/entrygriddelegates/TileDelegate.qml +++ b/src/qtquick/qml/private/entrygriddelegates/TileDelegate.qml @@ -1,211 +1,211 @@ /* * 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.downloadLinks.length === 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.downloadLinks.length === 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) + text: i18nc("Subheading for the tile view, located immediately underneath the name of the item", "By %1", 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) + text: i18nc("The number of times the item has been downloaded", "%1 downloads", model.downloadCount) } } FeedbackOverlay { anchors.fill: parent newStuffModel: component.GridView.view.model } MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: component.showDetails(); } } } diff --git a/src/qtquick/quickitemsmodel.cpp b/src/qtquick/quickitemsmodel.cpp index fd4d164a..f389a382 100644 --- a/src/qtquick/quickitemsmodel.cpp +++ b/src/qtquick/quickitemsmodel.cpp @@ -1,431 +1,431 @@ /* * 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) : q(qq) , model(nullptr) , engine(nullptr) , coreEngine(nullptr) {} ~Private() { qDeleteAll(commentsModels); } ItemsModel *q; KNSCore::ItemsModel *model; Engine *engine; KNSCore::Engine *coreEngine; QHash commentsModels; bool isLoadingData{false}; bool initModel() { if (model) { return true; } if (!coreEngine) { return false; } model = new KNSCore::ItemsModel(coreEngine, q); q->connect(coreEngine, &KNSCore::Engine::signalBusy, q, [=](){ isLoadingData = true; emit q->isLoadingDataChanged(); }); q->connect(coreEngine, &KNSCore::Engine::signalIdle, q, [=](){ isLoadingData = false; emit q->isLoadingDataChanged(); }); q->connect(coreEngine, &KNSCore::Engine::signalProvidersLoaded, coreEngine, &KNSCore::Engine::reloadEntries); // Entries have been fetched and should be shown: q->connect(coreEngine, &KNSCore::Engine::signalEntriesLoaded, model, &KNSCore::ItemsModel::slotEntriesLoaded); // An entry has changes - eg because it was installed 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(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) : 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()) return 0; if (d->initModel()) return d->model->rowCount(QModelIndex()); return 0; } QVariant ItemsModel::data(const QModelIndex &index, int role) const { QVariant data; if (index.isValid() && d->initModel()) { KNSCore::EntryInternal entry = d->model->data(d->model->index(index.row()), Qt::UserRole).value(); 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) { 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 { if (!parent.isValid() && d->coreEngine && d->coreEngine->categoriesMetadata().count() > 0) { return true; } return false; } void ItemsModel::fetchMore(const QModelIndex &parent) { if (parent.isValid() || !d->coreEngine) { return; } d->coreEngine->requestMoreData(); } QObject *ItemsModel::engine() const { return d->engine; } void ItemsModel::setEngine(QObject *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(); } } bool ItemsModel::isLoadingData() const { return d->isLoadingData; } void ItemsModel::installItem(int index, int linkId) { if (d->coreEngine) { KNSCore::EntryInternal entry = d->model->data(d->model->index(index), Qt::UserRole).value(); if(entry.isValid()) { d->coreEngine->install(entry, linkId); } } } void ItemsModel::uninstallItem(int index) { if (d->coreEngine) { KNSCore::EntryInternal entry = d->model->data(d->model->index(index), Qt::UserRole).value(); if(entry.isValid()) { 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())); + d->engine->idleMessage(i18n("Using %1", entry.name())); } } }