diff --git a/discover/main.cpp b/discover/main.cpp index ee39d087..91886bb6 100644 --- a/discover/main.cpp +++ b/discover/main.cpp @@ -1,169 +1,167 @@ /* * 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. */ // #define QT_QML_DEBUG #include #include #include #include #include #include #include #include #include "DiscoverObject.h" #include #include "DiscoverVersion.h" #include #include typedef QHash StringCompactMode; Q_GLOBAL_STATIC_WITH_ARGS(StringCompactMode, s_decodeCompactMode, (StringCompactMode { { QLatin1String("auto"), DiscoverObject::Auto }, { QLatin1String("compact"), DiscoverObject::Compact }, { QLatin1String("full"), DiscoverObject::Full } })) QCommandLineParser* createParser() { QCommandLineParser* parser = new QCommandLineParser; parser->addOption(QCommandLineOption(QStringLiteral("application"), i18n("Directly open the specified application by its package name."), QStringLiteral("name"))); parser->addOption(QCommandLineOption(QStringLiteral("mime"), i18n("Open with a program that can deal with the given mimetype."), QStringLiteral("name"))); parser->addOption(QCommandLineOption(QStringLiteral("category"), i18n("Display a list of entries with a category."), QStringLiteral("name"))); parser->addOption(QCommandLineOption(QStringLiteral("mode"), i18n("Open Discover in a said mode. Modes correspond to the toolbar buttons."), QStringLiteral("name"))); parser->addOption(QCommandLineOption(QStringLiteral("listmodes"), i18n("List all the available modes."))); parser->addOption(QCommandLineOption(QStringLiteral("compact"), i18n("Compact Mode (auto/compact/full)."), QStringLiteral("mode"), QStringLiteral("auto"))); parser->addOption(QCommandLineOption(QStringLiteral("local-filename"), i18n("Local package file to install"), QStringLiteral("package"))); parser->addOption(QCommandLineOption(QStringLiteral("listbackends"), i18n("List all the available backends."))); parser->addOption(QCommandLineOption(QStringLiteral("search"), i18n("Search string."), QStringLiteral("text"))); parser->addOption(QCommandLineOption(QStringLiteral("test"), QStringLiteral("Test file"), QStringLiteral("file.qml"))); parser->addPositionalArgument(QStringLiteral("urls"), i18n("Supports appstream: url scheme")); DiscoverBackendsFactory::setupCommandLine(parser); KAboutData::applicationData().setupCommandLine(parser); - parser->addHelpOption(); - parser->addVersionOption(); return parser; } void processArgs(QCommandLineParser* parser, DiscoverObject* mainWindow) { if(parser->isSet(QStringLiteral("application"))) mainWindow->openApplication(QUrl(parser->value(QStringLiteral("application")))); else if(parser->isSet(QStringLiteral("mime"))) mainWindow->openMimeType(parser->value(QStringLiteral("mime"))); else if(parser->isSet(QStringLiteral("category"))) mainWindow->openCategory(parser->value(QStringLiteral("category"))); if(parser->isSet(QStringLiteral("mode"))) mainWindow->openMode(parser->value(QStringLiteral("mode"))); if(parser->isSet(QStringLiteral("search"))) Q_EMIT mainWindow->openSearch(parser->value(QStringLiteral("search"))); if(parser->isSet(QStringLiteral("local-filename"))) mainWindow->openLocalPackage(QUrl::fromUserInput(parser->value(QStringLiteral("local-filename")), {}, QUrl::AssumeLocalFile)); foreach(const QString &arg, parser->positionalArguments()) { const QUrl url = QUrl::fromUserInput(arg, {}, QUrl::AssumeLocalFile); if (url.isLocalFile()) mainWindow->openLocalPackage(url); else if (url.scheme() == QLatin1String("apt")) Q_EMIT mainWindow->openSearch(url.host()); else mainWindow->openApplication(url); } } int main(int argc, char** argv) { QApplication app(argc, argv); app.setWindowIcon(QIcon::fromTheme(QStringLiteral("plasmadiscover"))); app.setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); KCrash::initialize(); KQuickAddons::QtQuickSettings::init(); KLocalizedString::setApplicationDomain("plasma-discover"); KAboutData about(QStringLiteral("discover"), i18n("Discover"), version, i18n("An application explorer"), KAboutLicense::GPL, i18n("© 2010-2019 Plasma Development Team")); about.addAuthor(i18n("Aleix Pol Gonzalez"), QString(), QStringLiteral("aleixpol@kde.org")); about.addAuthor(i18n("Jonathan Thomas"), QString(), QStringLiteral("echidnaman@kubuntu.org")); about.setProductName("discover/discover"); about.setProgramLogo(app.windowIcon()); about.setTranslator( i18ndc(nullptr, "NAME OF TRANSLATORS", "Your names"), i18ndc(nullptr, "EMAIL OF TRANSLATORS", "Your emails")); KAboutData::setApplicationData(about); DiscoverObject *mainWindow = nullptr; { QScopedPointer parser(createParser()); parser->process(app); about.processCommandLine(parser.data()); DiscoverBackendsFactory::processCommandLine(parser.data(), parser->isSet(QStringLiteral("test"))); if(parser->isSet(QStringLiteral("listbackends"))) { QTextStream(stdout) << i18n("Available backends:\n"); DiscoverBackendsFactory f; foreach(const QString& name, f.allBackendNames(false, true)) QTextStream(stdout) << " * " << name << '\n'; return 0; } if (parser->isSet(QStringLiteral("test"))) { QStandardPaths::setTestModeEnabled(true); } KDBusService* service = new KDBusService(KDBusService::Unique, &app); mainWindow = new DiscoverObject(s_decodeCompactMode->value(parser->value(QStringLiteral("compact")), DiscoverObject::Full)); QObject::connect(&app, &QCoreApplication::aboutToQuit, mainWindow, &DiscoverObject::deleteLater); QObject::connect(service, &KDBusService::activateRequested, mainWindow, [mainWindow](const QStringList &arguments, const QString &/*workingDirectory*/){ if (!mainWindow->rootObject()) QCoreApplication::instance()->quit(); mainWindow->rootObject()->raise(); if (arguments.isEmpty()) return; QScopedPointer parser(createParser()); parser->parse(arguments); processArgs(parser.data(), mainWindow); }); processArgs(parser.data(), mainWindow); if(parser->isSet(QStringLiteral("listmodes"))) { QTextStream(stdout) << i18n("Available modes:\n"); foreach(const QString& mode, mainWindow->modes()) QTextStream(stdout) << " * " << mode << '\n'; delete mainWindow; return 0; } if (parser->isSet(QStringLiteral("test"))) { const QUrl testFile = QUrl::fromUserInput(parser->value(QStringLiteral("test")), {}, QUrl::AssumeLocalFile); Q_ASSERT(!testFile.isEmpty() && testFile.isLocalFile()); mainWindow->loadTest(testFile); } } return app.exec(); } diff --git a/discover/qml/ApplicationDelegate.qml b/discover/qml/ApplicationDelegate.qml index 8af357b6..c0453bc7 100644 --- a/discover/qml/ApplicationDelegate.qml +++ b/discover/qml/ApplicationDelegate.qml @@ -1,131 +1,131 @@ /* * 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.1 import QtQuick.Controls 2.1 import QtQuick.Layouts 1.1 import QtQuick.Window 2.1 import "navigation.js" as Navigation import org.kde.kirigami 2.6 as Kirigami Kirigami.AbstractCard { id: delegateArea property alias application: installButton.application property bool compact: false property bool showRating: true showClickFeedback: true function trigger() { if (delegateRecycler.ListView.view) delegateRecycler.ListView.view.currentIndex = index Navigation.openApplication(application) } - highlighted: delegateRecycler.ListView.isCurrentItem + highlighted: delegateRecycler && delegateRecycler.ListView.isCurrentItem Keys.onReturnPressed: trigger() onClicked: trigger() contentItem: Item { implicitHeight: delegateArea.compact ? Kirigami.Units.gridUnit * 2 : Kirigami.Units.gridUnit * 4 Kirigami.Icon { id: resourceIcon source: application.icon readonly property real contHeight: delegateArea.compact ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 5 height: contHeight width: contHeight anchors { verticalCenter: parent.verticalCenter left: parent.left } } GridLayout { columnSpacing: delegateArea.compact ? 0 : 5 rowSpacing: delegateArea.compact ? 0 : 5 anchors { verticalCenter: parent.verticalCenter right: parent.right left: resourceIcon.right leftMargin: Kirigami.Units.largeSpacing } columns: 2 rows: delegateArea.compact ? 4 : 3 RowLayout { Layout.fillWidth: true Kirigami.Heading { id: head level: delegateArea.compact ? 3 : 2 Layout.fillWidth: !category.visible elide: Text.ElideRight text: delegateArea.application.name maximumLineCount: 1 } Kirigami.Heading { id: category level: 5 Layout.fillWidth: true elide: Text.ElideRight text: i18nc("Part of a string like this: ' - '", "- %1", delegateArea.application.categoryDisplay) maximumLineCount: 1 opacity: 0.6 visible: delegateArea.application.categoryDisplay !== page.title } } InstallApplicationButton { id: installButton Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.rowSpan: delegateArea.compact ? 3 : 1 } RowLayout { visible: showRating spacing: Kirigami.Units.largeSpacing Layout.fillWidth: true Rating { rating: delegateArea.application.rating ? delegateArea.application.rating.sortableRating : 0 starSize: delegateArea.compact ? summary.font.pointSize : head.font.pointSize } Label { Layout.fillWidth: true text: delegateArea.application.rating ? i18np("%1 rating", "%1 ratings", delegateArea.application.rating.ratingCount) : i18n("No ratings yet") visible: delegateArea.application.rating || delegateArea.application.backend.reviewsBackend.isResourceSupported(delegateArea.application) opacity: 0.5 elide: Text.ElideRight } } Label { Layout.columnSpan: delegateArea.compact ? 1 : 2 id: summary Layout.fillWidth: true bottomPadding: Kirigami.Units.smallSpacing elide: Text.ElideRight text: delegateArea.application.comment maximumLineCount: 1 textFormat: Text.PlainText } } } } diff --git a/discover/qml/ApplicationPage.qml b/discover/qml/ApplicationPage.qml index 1cb07ec5..1f7b93e4 100644 --- a/discover/qml/ApplicationPage.qml +++ b/discover/qml/ApplicationPage.qml @@ -1,474 +1,474 @@ /* * 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.5 import QtQuick.Controls 2.3 import QtQuick.Window 2.1 import QtQuick.Layouts 1.1 import org.kde.discover 2.0 import org.kde.discover.app 1.0 import org.kde.kirigami 2.6 as Kirigami import "navigation.js" as Navigation DiscoverPage { id: appInfo property QtObject application: null readonly property int visibleReviews: 3 clip: true // Usually this page is not the top level page, but when we are, isHome being // true will ensure that the search field suggests we are searching in the list // of available apps, not inside the app page itself. This will happen when // Discover is launched e.g. from krunner or otherwise requested to show a // specific application on launch. readonly property bool isHome: true function searchFor(text) { if (text.length === 0) return; Navigation.openCategory(null, "") } background: Rectangle { color: Kirigami.Theme.backgroundColor Kirigami.Theme.colorSet: Kirigami.Theme.View Kirigami.Theme.inherit: false } ReviewsPage { id: reviewsSheet model: ReviewsModel { id: reviewsModel resource: appInfo.application } } contextualActions: [originsMenuAction] ActionGroup { id: sourcesGroup exclusive: true } Kirigami.Action { id: originsMenuAction text: i18n("Sources") visible: children.length>1 children: sourcesGroup.actions readonly property var r0: Instantiator { model: ResourcesProxyModel { id: alternativeResourcesModel allBackends: true resourcesUrl: appInfo.application.url } delegate: Action { ActionGroup.group: sourcesGroup - text: i18n("%1 - %2", displayOrigin, model.application.availableVersion) + text: model.application.availableVersion ? i18n("%1 - %2", displayOrigin, model.application.availableVersion) : displayOrigin icon.name: sourceIcon checkable: true checked: appInfo.application === model.application onTriggered: if(index>=0) { var res = model.application console.assert(res) window.stack.pop() Navigation.openApplication(res) } } } } Kirigami.Action { id: invokeAction visible: application.isInstalled && application.canExecute && !appbutton.isActive text: application.executeLabel icon.name: "media-playback-start" onTriggered: application.invokeApplication() } actions { main: appbutton.action right: appbutton.isActive ? appbutton.cancelAction : invokeAction } InstallApplicationButton { id: appbutton Layout.rightMargin: Kirigami.Units.smallSpacing application: appInfo.application visible: false } leftPadding: Kirigami.Units.largeSpacing * (applicationWindow().wideScreen ? 2 : 1) rightPadding: Kirigami.Units.largeSpacing * (applicationWindow().wideScreen ? 2 : 1) // Icon, name, caption, screenshots, description and reviews ColumnLayout { spacing: 0 RowLayout { Kirigami.Icon { Layout.preferredHeight: 80 Layout.preferredWidth: 80 source: appInfo.application.icon Layout.rightMargin: Kirigami.Units.smallSpacing * 2 } ColumnLayout { spacing: 0 Kirigami.Heading { level: 1 text: appInfo.application.name lineHeight: 1.0 maximumLineCount: 1 elide: Text.ElideRight Layout.fillWidth: true Layout.alignment: Text.AlignBottom } RowLayout { spacing: Kirigami.Units.largeSpacing Rating { rating: appInfo.application.rating ? appInfo.application.rating.sortableRating : 0 starSize: summary.font.pointSize } Label { text: appInfo.application.rating ? i18np("%1 rating", "%1 ratings", appInfo.application.rating.ratingCount) : i18n("No ratings yet") opacity: 0.5 } } Kirigami.Heading { id: summary level: 4 text: appInfo.application.comment maximumLineCount: 2 lineHeight: lineCount > 1 ? 0.75 : 1.2 elide: Text.ElideRight Layout.fillWidth: true Layout.alignment: Qt.AlignTop } } Layout.bottomMargin: Kirigami.Units.largeSpacing } ApplicationScreenshots { Layout.fillWidth: true visible: count > 0 resource: appInfo.application ScrollBar.horizontal: screenshotsScrollbar } ScrollBar { id: screenshotsScrollbar Layout.fillWidth: true } - LinkLabel { + Label { Layout.topMargin: Kirigami.Units.largeSpacing Layout.fillWidth: true wrapMode: Text.WordWrap text: appInfo.application.longDescription onLinkActivated: Qt.openUrlExternally(link); } Kirigami.Heading { Layout.topMargin: Kirigami.Units.largeSpacing text: i18n("What's New") level: 2 visible: changelogLabel.text.length > 0 } Rectangle { color: Kirigami.Theme.linkColor Layout.fillWidth: true height: 1 visible: changelogLabel.text.length > 0 } Label { id: changelogLabel Layout.topMargin: Kirigami.Units.largeSpacing Layout.fillWidth: true wrapMode: Text.WordWrap Component.onCompleted: appInfo.application.fetchChangelog() Connections { target: appInfo.application onChangelogFetched: { changelogLabel.text = changelog } } } Kirigami.LinkButton { id: addonsButton text: i18n("Addons") visible: addonsView.containsAddons onClicked: addonsView.sheetOpen = true } RowLayout { Layout.topMargin: Kirigami.Units.largeSpacing Layout.fillWidth: true Kirigami.Heading { Layout.fillWidth: true text: i18n("Reviews") Layout.alignment: Qt.AlignLeft | Qt.AlignBottom level: 2 visible: rep.count > 0 } Kirigami.LinkButton { visible: reviewsModel.count > visibleReviews text: i18np("Show %1 review...", "Show all %1 reviews...", reviewsModel.count) Layout.alignment: Qt.AlignRight | Qt.AlignBottom onClicked: { reviewsSheet.open() } } } Rectangle { color: Kirigami.Theme.linkColor Layout.fillWidth: true height: 1 visible: rep.count > 0 } Repeater { id: rep model: PaginateModel { sourceModel: reviewsSheet.model pageSize: visibleReviews } delegate: ReviewDelegate { Layout.topMargin: Kirigami.Units.largeSpacing separator: false compact: true Layout.bottomMargin: Kirigami.Units.largeSpacing } } Kirigami.LinkButton { function writeReviewText() { if (appInfo.application.isInstalled) { if (reviewsModel.count > 0) { return i18n("Write a review!") } else { return i18n("Be the first to write a review!") } // App not installed } else { if (reviewsModel.count > 0) { return i18n("Install this app to write a review!") } else { return i18n("Install this app and be the first to write a review!") } } } text: writeReviewText() Layout.alignment: Qt.AlignCenter onClicked: reviewsSheet.openReviewDialog() enabled: appInfo.application.isInstalled visible: reviewsModel.backend && reviewsModel.backend.isResourceSupported(appInfo.application) Layout.topMargin: Kirigami.Units.largeSpacing Layout.bottomMargin: Kirigami.Units.largeSpacing } Repeater { model: application.objects delegate: Loader { property QtObject resource: appInfo.application source: modelData } } Item { height: addonsButton.height width: 1 } // Details/metadata Rectangle { color: Kirigami.Theme.linkColor Layout.fillWidth: true height: 1 Layout.bottomMargin: Kirigami.Units.largeSpacing } GridLayout { rowSpacing: 0 columns: 2 // Category row Label { visible: categoryLabel.visible Layout.alignment: Qt.AlignRight text: i18n("Category:") } Label { id: categoryLabel visible: text.length > 0 Layout.fillWidth: true elide: Text.ElideRight text: appInfo.application.categoryDisplay } // Version row Label { visible: versionLabel.visible Layout.alignment: Qt.AlignRight text: i18n("Version:") } Label { readonly property string version: appInfo.application.isInstalled ? appInfo.application.installedVersion : appInfo.application.availableVersion readonly property string releaseDate: appInfo.application.releaseDate.toLocaleString() function versionString() { if (version.length == 0) { return "" } else { if (releaseDate.length > 0) { return i18n("%1, released on %2", version, releaseDate) } else { return version } } } id: versionLabel visible: text.length > 0 Layout.fillWidth: true elide: Text.ElideRight text: versionString() } // Author row Label { Layout.alignment: Qt.AlignRight text: i18n("Author:") visible: authorLabel.visible } Label { id: authorLabel Layout.fillWidth: true elide: Text.ElideRight visible: text.length>0 text: appInfo.application.author } // Size row Label { Layout.alignment: Qt.AlignRight text: i18n("Size:") } Label { Layout.fillWidth: true elide: Text.ElideRight text: appInfo.application.sizeDescription } // Source row Label { Layout.alignment: Qt.AlignRight text: i18n("Source:") } Label { Layout.fillWidth: true horizontalAlignment: Text.AlignLeft text: appInfo.application.displayOrigin elide: Text.ElideRight } // License row Label { Layout.alignment: Qt.AlignRight text: i18n("License:") visible: appInfo.application.licenses.length>0 } RowLayout { visible: appInfo.application.licenses.length>0 Layout.fillWidth: true Repeater { model: appInfo.application.licenses delegate: Kirigami.UrlButton { horizontalAlignment: Text.AlignLeft ToolTip.text: i18n("See full license terms") ToolTip.visible: hovered text: modelData.name url: modelData.url enabled: url !== "" } } } // Homepage row Label { visible: homepageLink.visible Layout.alignment: Qt.AlignRight text: i18n("Homepage:") } Kirigami.UrlButton { id: homepageLink url: application.homepage Layout.fillWidth: true horizontalAlignment: Text.AlignLeft } // "User Guide" row Label { visible: docsLink.visible Layout.alignment: Qt.AlignRight text: i18n("User Guide:") } Kirigami.UrlButton { id: docsLink url: application.helpURL Layout.fillWidth: true horizontalAlignment: Text.AlignLeft } // Donate row Label { visible: donationLink.visible Layout.alignment: Qt.AlignRight text: i18n("Donate:") } Kirigami.UrlButton { id: donationLink url: application.donationURL Layout.fillWidth: true horizontalAlignment: Text.AlignLeft } // "Report a Problem" row Label { visible: bugLink.visible Layout.alignment: Qt.AlignRight text: i18n("Report a Problem:") } Kirigami.UrlButton { id: bugLink url: application.bugURL Layout.fillWidth: true horizontalAlignment: Text.AlignLeft } } } readonly property var addons: AddonsView { id: addonsView application: appInfo.application parent: overlay } } diff --git a/discover/qml/LinkLabel.qml b/discover/qml/LinkLabel.qml deleted file mode 100644 index 4b91aa17..00000000 --- a/discover/qml/LinkLabel.qml +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.5 -import QtQuick.Controls 2.3 - -/** - * A label which adds showing a hand cursor if there is a link being hovered in - * the text set on the label. - */ -Label { - // Since Text (and Label) lack cursor-changing abilities of their own, - // as suggested by QTBUG-30804, use a MouseAra to do our dirty work. - // See comment https://bugreports.qt.io/browse/QTBUG-30804?#comment-206287 - MouseArea { - anchors.fill: parent - cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor - acceptedButtons: Qt.NoButton // Not actually accepting clicks, just changing the cursor - } -} diff --git a/discover/qml/ReviewDelegate.qml b/discover/qml/ReviewDelegate.qml index f2d7d9f4..803284c8 100644 --- a/discover/qml/ReviewDelegate.qml +++ b/discover/qml/ReviewDelegate.qml @@ -1,89 +1,89 @@ /* * 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.1 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.1 import org.kde.discover 2.0 import org.kde.kirigami 2.0 as Kirigami ColumnLayout { id: item visible: model.shouldShow property bool compact: false property bool separator: true signal markUseful(bool useful) function usefulnessToString(favorable, total) { return total===0 ? i18n("Tell us about this review!") : i18n("%1 out of %2 people found this review useful", favorable, total) } RowLayout { Layout.fillWidth: true Label { id: content Layout.fillWidth: true elide: Text.ElideRight readonly property string author: reviewer ? reviewer : i18n("unknown reviewer") text: summary ? i18n("%1 by %2", summary, author) : i18n("Comment by %1", author) } Rating { id: rating rating: model.rating starSize: content.font.pointSize } } Label { Layout.fillWidth: true text: display maximumLineCount: item.compact ? 3 : undefined wrapMode: Text.Wrap } Label { visible: !item.compact text: usefulnessToString(usefulnessFavorable, usefulnessTotal) } - LinkLabel { + Label { visible: !item.compact Layout.alignment: Qt.AlignRight text: { switch(usefulChoice) { case ReviewsModel.Yes: i18n("Useful? Yes/No") break; case ReviewsModel.No: i18n("Useful? Yes/No") break; default: i18n("Useful? Yes/No") break; } } onLinkActivated: item.markUseful(link=='true') } Kirigami.Separator { visible: item.separator Layout.fillWidth: true } } diff --git a/discover/qml/UpdatesPage.qml b/discover/qml/UpdatesPage.qml index 4d563af4..0680dc0b 100644 --- a/discover/qml/UpdatesPage.qml +++ b/discover/qml/UpdatesPage.qml @@ -1,398 +1,398 @@ import QtQuick.Controls 2.3 import QtQuick.Layouts 1.1 import QtQuick 2.4 import org.kde.discover 2.0 import org.kde.discover.app 1.0 import "navigation.js" as Navigation import org.kde.kirigami 2.10 as Kirigami DiscoverPage { id: page title: i18n("Updates") property string footerLabel: "" property int footerProgress: 0 property bool isBusy: false ResourcesUpdatesModel { id: resourcesUpdatesModel onPassiveMessage: { desc.text += message + "
\n" sheet.sheetOpen = true } onIsProgressingChanged: { if (!isProgressing) { resourcesUpdatesModel.prepare() } } Component.onCompleted: { if (!isProgressing) { resourcesUpdatesModel.prepare() } } } Kirigami.OverlaySheet { id: sheet ColumnLayout { Label { id: desc Layout.fillWidth: true textFormat: Text.StyledText wrapMode: Text.WordWrap } Button { id: okButton Layout.alignment: Qt.AlignRight text: i18n("OK") icon.name: "dialog-ok" onClicked: { sheet.sheetOpen = false } } } onSheetOpenChanged: if(!sheetOpen) { desc.text = "" } else { okButton.focus = true } } UpdateModel { id: updateModel backend: resourcesUpdatesModel } Kirigami.Action { id: updateAction text: page.unselected>0 ? i18n("Update Selected") : i18n("Update All") visible: updateModel.toUpdateCount iconName: "update-none" enabled: !resourcesUpdatesModel.isProgressing && !ResourcesModel.isFetching onTriggered: resourcesUpdatesModel.updateAll() } footer: ScrollView { id: scv width: parent.width height: visible ? Kirigami.Units.gridUnit * 10 : 0 visible: log.contents.length > 0 TextArea { readOnly: true text: log.contents cursorPosition: text.length - 1 font.family: "monospace" ReadFile { id: log filter: ".*ALPM-SCRIPTLET\\] .*" path: "/var/log/pacman.log" } } } Kirigami.Action { id: cancelUpdateAction iconName: "dialog-cancel" text: i18n("Cancel") enabled: resourcesUpdatesModel.transaction && resourcesUpdatesModel.transaction.isCancellable onTriggered: resourcesUpdatesModel.transaction.cancel() } readonly property int unselected: (updateModel.totalUpdatesCount - updateModel.toUpdateCount) header: ToolBar { anchors.left: page.left anchors.right: page.right Kirigami.Theme.colorSet: Kirigami.Theme.Button Kirigami.Theme.inherit: false visible: (updateModel.totalUpdatesCount > 0 && resourcesUpdatesModel.isProgressing) || updateModel.hasUpdates CheckBox { anchors.left: parent.left anchors.leftMargin: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing anchors.right: parent.right text: page.unselected === 0 ? i18n("All updates selected (%1)", updateModel.updateSize) : i18np("%1/%2 update selected (%3)", "%1/%2 updates selected (%3)", updateModel.toUpdateCount, updateModel.totalUpdatesCount, updateModel.updateSize) enabled: updateAction.enabled && !resourcesUpdatesModel.isProgressing && !ResourcesModel.isFetching tristate: true checkState: updateModel.toUpdateCount === 0 ? Qt.Unchecked : updateModel.toUpdateCount === updateModel.totalUpdatesCount ? Qt.Checked : Qt.PartiallyChecked onClicked: { if (updateModel.toUpdateCount === 0) updateModel.checkAll() else updateModel.uncheckAll() } } } supportsRefreshing: true onRefreshingChanged: { ResourcesModel.updateAction.triggered() refreshing = false } readonly property Item report: ColumnLayout { parent: page anchors.fill: parent Item { Layout.fillHeight: true width: 1 } Kirigami.Heading { Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter horizontalAlignment: Text.AlignHCenter text: page.footerLabel level: 3 } ProgressBar { Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: Kirigami.Units.gridUnit * 20 value: page.footerProgress from: 0 to: 100 visible: page.isBusy } Kirigami.Icon { Layout.alignment: Qt.AlignHCenter visible: page.footerProgress === 0 && page.footerLabel !== "" && !page.isBusy source: "update-none" opacity: 0.1 width: Kirigami.Units.gridUnit * 8 height: width } Button { Layout.alignment: Qt.AlignHCenter text: i18n("Restart") visible: resourcesUpdatesModel.needsReboot onClicked: app.reboot() } Item { Layout.fillHeight: true width: 1 } } ListView { id: updatesView currentIndex: -1 displaced: Transition { YAnimator { duration: Kirigami.Units.longDuration easing.type: Easing.InOutQuad } } model: QSortFilterProxyModel { sourceModel: updateModel sortRole: UpdateModel.SectionResourceProgressRole } section { property: "section" delegate: Kirigami.ListSectionHeader { width: updatesView.width label: section } } delegate: Kirigami.AbstractListItem { id: listItem backgroundColor: Kirigami.Theme.backgroundColor highlighted: ListView.isCurrentItem onEnabledChanged: if (!enabled) { layout.extended = false; } visible: resourceState < 3 //3=AbstractBackendUpdater.Done Keys.onReturnPressed: { itemChecked.clicked() } Keys.onPressed: if (event.key===Qt.Key_Alt) layout.extended = true Keys.onReleased: if (event.key===Qt.Key_Alt) layout.extended = false ColumnLayout { id: layout property bool extended: false onExtendedChanged: if (extended) { updateModel.fetchUpdateDetails(index) } RowLayout { Layout.fillWidth: true Layout.fillHeight: true CheckBox { id: itemChecked Layout.leftMargin: Kirigami.Units.gridUnit Layout.alignment: Qt.AlignVCenter checked: model.checked === Qt.Checked onClicked: model.checked = (model.checked===Qt.Checked ? Qt.Unchecked : Qt.Checked) enabled: !resourcesUpdatesModel.isProgressing } Kirigami.Icon { width: Kirigami.Units.gridUnit * 2 Layout.preferredHeight: width source: decoration smooth: true } ColumnLayout { // App name Kirigami.Heading { Layout.fillWidth: true text: i18n("%1", display) level: 3 elide: Text.ElideRight } // Version numbers Label { Layout.fillWidth: true elide: truncated ? Text.ElideLeft : Text.ElideRight text: { if (installedVersion == availableVersion) { // Update of the same version; show when old and new are // the same (common with Flatpak runtimes) return i18n("Update to version %1", availableVersion); } else if (installedVersion && availableVersion) { // Old and new version numbers // This thing with \x9 is a fancy feature in QML text handling: // when the string will be elided, it shows the string after // the last \x9. This allows us to show a smaller string // when there's now enough room // All of this is mostly for the benefit of KDE Neon users, // since the version strings there are really really long return i18nc("Do not translate or alter \\x9", "%1 → %2\x9C%1 → %2\x9C%2", installedVersion, availableVersion); } else { // Available version only, for when the installed version // isn't available for some reason return availableVersion; } } opacity: listItem.hovered? 0.8 : 0.6 } } LabelBackground { Layout.minimumWidth: Kirigami.Units.gridUnit * 6 text: resourceState == 2 ? i18n("Installing") : size progress: resourceProgress/100 } } Frame { Layout.fillWidth: true implicitHeight: view.contentHeight visible: layout.extended && changelog.length>0 - LinkLabel { + Label { id: view anchors { right: parent.right left: parent.left } text: changelog textFormat: Text.StyledText wrapMode: Text.WordWrap onLinkActivated: Qt.openUrlExternally(link) } //This saves a binding loop on implictHeight, as the Label //height is updated twice (first time with the wrong value) Behavior on implicitHeight { PropertyAnimation { duration: Kirigami.Units.shortDuration } } } Button { Layout.alignment: Qt.AlignRight text: i18n("More Information...") visible: layout.extended enabled: !resourcesUpdatesModel.isProgressing onClicked: Navigation.openApplication(resource) } } onClicked: { layout.extended = !layout.extended } } } readonly property alias secSinceUpdate: resourcesUpdatesModel.secsToLastUpdate state: ( resourcesUpdatesModel.isProgressing ? "progressing" : ResourcesModel.isFetching ? "fetching" : updateModel.hasUpdates ? "has-updates" : resourcesUpdatesModel.needsReboot ? "reboot" : secSinceUpdate < 0 ? "unknown" : secSinceUpdate === 0 ? "now-uptodate" : secSinceUpdate < 1000 * 60 * 60 * 24 ? "uptodate" : secSinceUpdate < 1000 * 60 * 60 * 24 * 7 ? "medium" : "low" ) states: [ State { name: "fetching" PropertyChanges { target: page; footerLabel: i18nc("@info", "Fetching Updates...") } PropertyChanges { target: page; footerProgress: ResourcesModel.fetchingUpdatesProgress } PropertyChanges { target: page; isBusy: true } PropertyChanges { target: updatesView; opacity: 0 } }, State { name: "progressing" PropertyChanges { target: page; supportsRefreshing: false } PropertyChanges { target: page.actions; main: cancelUpdateAction } }, State { name: "has-updates" PropertyChanges { target: page; title: i18nc("@info", "Updates") } PropertyChanges { target: page.actions; main: updateAction } PropertyChanges { target: page.actions; left: refreshAction } }, State { name: "reboot" PropertyChanges { target: page; footerLabel: i18nc("@info", "The system requires a restart to apply updates") } }, State { name: "now-uptodate" PropertyChanges { target: page; footerLabel: i18nc("@info", "Up to Date") } PropertyChanges { target: page.actions; main: refreshAction } }, State { name: "uptodate" PropertyChanges { target: page; footerLabel: i18nc("@info", "Up to Date") } PropertyChanges { target: page.actions; main: refreshAction } }, State { name: "medium" PropertyChanges { target: page; title: i18nc("@info", "Up to Date") } PropertyChanges { target: page.actions; main: refreshAction } }, State { name: "low" PropertyChanges { target: page; title: i18nc("@info", "Should check for updates") } PropertyChanges { target: page.actions; main: refreshAction } }, State { name: "unknown" PropertyChanges { target: page; title: i18nc("@info", "It is unknown when the last check for updates was") } PropertyChanges { target: page.actions; main: refreshAction } } ] } diff --git a/discover/resources.qrc b/discover/resources.qrc index fa22914b..4422949d 100644 --- a/discover/resources.qrc +++ b/discover/resources.qrc @@ -1,39 +1,38 @@ qml/TopLevelPageData.qml qml/ApplicationsListPage.qml qml/ApplicationPage.qml qml/ReviewsPage.qml qml/AddonsView.qml qml/ApplicationDelegate.qml qml/InstallApplicationButton.qml qml/Rating.qml qml/UpdatesPage.qml qml/ReviewDialog.qml qml/ProgressView.qml qml/BrowsingPage.qml qml/InstalledPage.qml qml/SearchPage.qml qml/SourcesPage.qml qml/ReviewDelegate.qml qml/AddSourceDialog.qml qml/ConditionalLoader.qml qml/ConditionalObject.qml qml/ApplicationScreenshots.qml qml/LabelBackground.qml qml/DiscoverPage.qml qml/DiscoverWindow.qml qml/DiscoverDrawer.qml qml/ActionListItem.qml qml/LoadingPage.qml qml/SearchField.qml qml/Shadow.qml qml/DiscoverPopup.qml qml/AboutPage.qml - qml/LinkLabel.qml qml/Feedback.qml qml/navigation.js diff --git a/libdiscover/backends/KNSBackend/KNSBackend.cpp b/libdiscover/backends/KNSBackend/KNSBackend.cpp index a0b82237..22d5ba50 100644 --- a/libdiscover/backends/KNSBackend/KNSBackend.cpp +++ b/libdiscover/backends/KNSBackend/KNSBackend.cpp @@ -1,599 +1,605 @@ /*************************************************************************** * Copyright © 2012 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 . * ***************************************************************************/ // Qt includes #include #include #include #include #include #include // Attica includes #include #include // KDE includes #include #include #include #include #include #include // DiscoverCommon includes #include "Transaction/Transaction.h" #include "Transaction/TransactionModel.h" #include "Category/Category.h" // Own includes #include "KNSBackend.h" #include "KNSResource.h" #include "KNSReviews.h" #include #include "utils.h" class KNSBackendFactory : public AbstractResourcesBackendFactory { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.muon.AbstractResourcesBackendFactory") Q_INTERFACES(AbstractResourcesBackendFactory) public: KNSBackendFactory() { connect(KNSCore::QuestionManager::instance(), &KNSCore::QuestionManager::askQuestion, this, [](KNSCore::Question* q) { qWarning() << q->question() << q->questionType(); q->setResponse(KNSCore::Question::InvalidResponse); }); } QVector newInstance(QObject* parent, const QString &/*name*/) const override { QVector ret; #if KNEWSTUFFCORE_VERSION_MAJOR==5 && KNEWSTUFFCORE_VERSION_MINOR>=57 QStringList locations = KNSCore::Engine::configSearchLocations(); #else QStringList locations = QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation); #endif for (const QString &path: locations) { QDirIterator dirIt(path, {QStringLiteral("*.knsrc")}, QDir::Files); for(; dirIt.hasNext(); ) { dirIt.next(); auto bk = new KNSBackend(parent, QStringLiteral("plasma"), dirIt.filePath()); if (bk->isValid()) ret += bk; else delete bk; } } return ret; } }; Q_DECLARE_METATYPE(KNSCore::EntryInternal) KNSBackend::KNSBackend(QObject* parent, const QString& iconName, const QString &knsrc) : AbstractResourcesBackend(parent) , m_fetching(false) , m_isValid(true) , m_reviews(new KNSReviews(this)) , m_name(knsrc) , m_iconName(iconName) , m_updater(new StandardBackendUpdater(this)) { const QString fileName = QFileInfo(m_name).fileName(); setName(fileName); setObjectName(knsrc); const KConfig conf(m_name); if (!conf.hasGroup("KNewStuff3")) { markInvalid(QStringLiteral("Config group not found! Check your KNS3 installation.")); return; } m_categories = QStringList{ fileName }; const KConfigGroup group = conf.group("KNewStuff3"); m_extends = group.readEntry("Extends", QStringList()); m_reviews->setProviderUrl(QUrl(group.readEntry("ProvidersUrl", QString()))); setFetching(true); // This ensures we have something to track when checking after the initialization timeout connect(this, &KNSBackend::initialized, this, [this](){ m_initialized = true; }); // If we have not initialized in 60 seconds, consider this KNS backend invalid QTimer::singleShot(60000, this, [this](){ if(!m_initialized) { markInvalid(i18n("Backend %1 took too long to initialize", m_displayName)); m_responsePending = false; Q_EMIT searchFinished(); Q_EMIT availableForQueries(); } }); const QVector> filters = { {CategoryFilter, fileName } }; const QSet backendName = { name() }; m_displayName = group.readEntry("Name", QString()); if (m_displayName.isEmpty()) { m_displayName = fileName.mid(0, fileName.indexOf(QLatin1Char('.'))); m_displayName[0] = m_displayName[0].toUpper(); } m_hasApplications = group.readEntry("X-Discover-HasApplications", false); const QStringList cats = group.readEntry("Categories", QStringList{}); QVector categories; if (cats.count() > 1) { m_categories += cats; for(const auto &cat: cats) { if (m_hasApplications) categories << new Category(cat, QStringLiteral("applications-other"), { {CategoryFilter, cat } }, backendName, {}, {}, true); else categories << new Category(cat, QStringLiteral("plasma"), { {CategoryFilter, cat } }, backendName, {}, {}, true); } } QVector topCategories{categories}; for (const auto &cat: categories) { const QString catName = cat->name().append(QLatin1Char('/')); for (const auto& potentialSubCat: categories) { if(potentialSubCat->name().startsWith(catName)) { cat->addSubcategory(potentialSubCat); topCategories.removeOne(potentialSubCat); } } } m_engine = new KNSCore::Engine(this); connect(m_engine, &KNSCore::Engine::signalErrorCode, this, &KNSBackend::signalErrorCode); connect(m_engine, &KNSCore::Engine::signalEntriesLoaded, this, &KNSBackend::receivedEntries, Qt::QueuedConnection); connect(m_engine, &KNSCore::Engine::signalEntryChanged, this, &KNSBackend::statusChanged, Qt::QueuedConnection); - connect(m_engine, &KNSCore::Engine::signalEntryDetailsLoaded, this, &KNSBackend::statusChanged); + connect(m_engine, &KNSCore::Engine::signalEntryDetailsLoaded, this, &KNSBackend::detailsLoaded); connect(m_engine, &KNSCore::Engine::signalProvidersLoaded, this, &KNSBackend::fetchInstalled); connect(m_engine, &KNSCore::Engine::signalCategoriesMetadataLoded, this, [categories](const QList< KNSCore::Provider::CategoryMetadata>& categoryMetadatas){ for (const KNSCore::Provider::CategoryMetadata& category : categoryMetadatas) { for (Category* cat : categories) { if (cat->orFilters().count() > 0 && cat->orFilters().first().second == category.name) { cat->setName(category.displayName); break; } } } }); m_engine->setPageSize(100); m_engine->init(m_name); if(m_hasApplications) { auto actualCategory = new Category(m_displayName, QStringLiteral("applications-other"), filters, backendName, topCategories, QUrl(), false); auto applicationCategory = new Category(i18n("Applications"), QStringLiteral("applications-internet"), filters, backendName, { actualCategory }, QUrl(), false); applicationCategory->setAndFilter({ {CategoryFilter, QLatin1String("Application")} }); m_categories.append(applicationCategory->name()); m_rootCategories = { applicationCategory }; // Make sure we filter out any apps which won't run on the current system architecture QStringList tagFilter = m_engine->tagFilter(); if(QSysInfo::currentCpuArchitecture() == QLatin1String("arm")) { tagFilter << QLatin1String("application##architecture==armhf"); } else if(QSysInfo::currentCpuArchitecture() == QLatin1String("arm64")) { tagFilter << QLatin1String("application##architecture==arm64"); } else if(QSysInfo::currentCpuArchitecture() == QLatin1String("i386")) { tagFilter << QLatin1String("application##architecture==x86"); } else if(QSysInfo::currentCpuArchitecture() == QLatin1String("ia64")) { tagFilter << QLatin1String("application##architecture==x86-64"); } else if(QSysInfo::currentCpuArchitecture() == QLatin1String("x86_64")) { tagFilter << QLatin1String("application##architecture==x86"); tagFilter << QLatin1String("application##architecture==x86-64"); } m_engine->setTagFilter(tagFilter); } else { static const QSet knsrcPlasma = { QStringLiteral("aurorae.knsrc"), QStringLiteral("icons.knsrc"), QStringLiteral("kfontinst.knsrc"), QStringLiteral("lookandfeel.knsrc"), QStringLiteral("plasma-themes.knsrc"), QStringLiteral("plasmoids.knsrc"), QStringLiteral("wallpaper.knsrc"), QStringLiteral("xcursor.knsrc"), QStringLiteral("cgcgtk3.knsrc"), QStringLiteral("cgcicon.knsrc"), QStringLiteral("cgctheme.knsrc"), //GTK integration QStringLiteral("kwinswitcher.knsrc"), QStringLiteral("kwineffect.knsrc"), QStringLiteral("kwinscripts.knsrc"), //KWin QStringLiteral("comic.knsrc"), QStringLiteral("colorschemes.knsrc"), QStringLiteral("emoticons.knsrc"), QStringLiteral("plymouth.knsrc"), QStringLiteral("sddmtheme.knsrc"), QStringLiteral("wallpaperplugin.knsrc"), QStringLiteral("ksplash.knsrc"), QStringLiteral("window-decorations.knsrc") }; const auto iconName = knsrcPlasma.contains(fileName)? QStringLiteral("plasma") : QStringLiteral("applications-other"); auto actualCategory = new Category(m_displayName, iconName, filters, backendName, categories, QUrl(), true); const auto topLevelName = knsrcPlasma.contains(fileName)? i18n("Plasma Addons") : i18n("Application Addons"); auto addonsCategory = new Category(topLevelName, iconName, filters, backendName, {actualCategory}, QUrl(), true); m_rootCategories = { addonsCategory }; } } KNSBackend::~KNSBackend() { qDeleteAll(m_rootCategories); } void KNSBackend::markInvalid(const QString &message) { m_rootCategories.clear(); qWarning() << "invalid kns backend!" << m_name << "because:" << message; m_isValid = false; setFetching(false); Q_EMIT initialized(); } void KNSBackend::fetchInstalled() { auto search = new OneTimeAction([this]() { Q_EMIT startingSearch(); m_onePage = true; m_responsePending = true; m_engine->checkForInstalled(); }, this); if (m_responsePending) { connect(this, &KNSBackend::availableForQueries, search, &OneTimeAction::trigger, Qt::QueuedConnection); } else { search->trigger(); } } void KNSBackend::setFetching(bool f) { if(m_fetching!=f) { m_fetching = f; emit fetchingChanged(); if (!m_fetching) { Q_EMIT initialized(); } } } bool KNSBackend::isValid() const { return m_isValid; } KNSResource* KNSBackend::resourceForEntry(const KNSCore::EntryInternal& entry) { KNSResource* r = static_cast(m_resourcesByName.value(entry.uniqueId())); if (!r) { QStringList categories{name(), m_rootCategories.first()->name()}; const auto cats = m_engine->categoriesMetadata(); const int catIndex = kIndexOf(cats, [&entry](const KNSCore::Provider::CategoryMetadata& cat){ return entry.category() == cat.id; }); if (catIndex > -1) { categories << cats.at(catIndex).name; } if(m_hasApplications) { categories << QLatin1String("Application"); } r = new KNSResource(entry, categories, this); m_resourcesByName.insert(entry.uniqueId(), r); } else { r->setEntry(entry); } return r; } void KNSBackend::receivedEntries(const KNSCore::EntryInternal::List& entries) { m_responsePending = false; const auto filtered = kFilter(entries, [](const KNSCore::EntryInternal& entry){ return entry.isValid(); }); const auto resources = kTransform>(filtered, [this](const KNSCore::EntryInternal& entry){ return resourceForEntry(entry); }); if (!resources.isEmpty()) { Q_EMIT receivedResources(resources); } else { Q_EMIT searchFinished(); Q_EMIT availableForQueries(); setFetching(false); return; } // qDebug() << "received" << objectName() << this << m_resourcesByName.count(); if (m_onePage) { Q_EMIT availableForQueries(); setFetching(false); } } void KNSBackend::fetchMore() { if (m_responsePending) return; // We _have_ to set this first. If we do not, we may run into a situation where the // data request will conclude immediately, causing m_responsePending to remain true // for perpetuity as the slots will be called before the function returns. m_responsePending = true; m_engine->requestMoreData(); } void KNSBackend::statusChanged(const KNSCore::EntryInternal& entry) { resourceForEntry(entry); } void KNSBackend::signalErrorCode(const KNSCore::ErrorCode& errorCode, const QString& message, const QVariant& metadata) { QString error = message; qDebug() << "KNS error in" << m_displayName << ":" << errorCode << message << metadata; bool invalidFile = false; switch(errorCode) { case KNSCore::ErrorCode::UnknownError: // This is not supposed to be hit, of course, but any error coming to this point should be non-critical and safely ignored. break; case KNSCore::ErrorCode::NetworkError: // If we have a network error, we need to tell the user about it. This is almost always fatal, so mark invalid and tell the user. error = i18n("Network error in backend %1: %2", m_displayName, metadata.toInt()); markInvalid(error); invalidFile = true; break; case KNSCore::ErrorCode::OcsError: if(metadata.toInt() == 200) { // Too many requests, try again in a couple of minutes - perhaps we can simply postpone it automatically, and give a message? error = i18n("Too many requests sent to the server for backend %1. Please try again in a few minutes.", m_displayName); } else { // Unknown API error, usually something critical, mark as invalid and cry a lot error = i18n("Invalid %1 backend, contact your distributor.", m_displayName); markInvalid(error); invalidFile = true; } break; case KNSCore::ErrorCode::ConfigFileError: error = i18n("Invalid %1 backend, contact your distributor.", m_displayName); markInvalid(error); invalidFile = true; break; case KNSCore::ErrorCode::ProviderError: error = i18n("Invalid %1 backend, contact your distributor.", m_displayName); markInvalid(error); invalidFile = true; break; case KNSCore::ErrorCode::InstallationError: // This error is handled already, by forwarding the KNS engine's installer error message. break; case KNSCore::ErrorCode::ImageError: // Image fetching errors are not critical as such, but may lead to weird layout issues, might want handling... error = i18n("Could not fetch screenshot for the entry %1 in backend %2", metadata.toList().at(0).toString(), m_displayName); break; default: // Having handled all current error values, we should by all rights never arrive here, but for good order and future safety... error = i18n("Unhandled error in %1 backend. Contact your distributor.", m_displayName); break; } m_responsePending = false; Q_EMIT searchFinished(); Q_EMIT availableForQueries(); // Setting setFetching to false when we get an error ensures we don't end up in an eternally-fetching state this->setFetching(false); qWarning() << "kns error" << objectName() << error; if (!invalidFile) Q_EMIT passiveMessage(i18n("%1: %2", name(), error)); } class KNSTransaction : public Transaction { public: KNSTransaction(QObject* parent, KNSResource* res, Transaction::Role role) : Transaction(parent, res, role) , m_id(res->entry().uniqueId()) { setCancellable(false); auto manager = res->knsBackend()->engine(); connect(manager, &KNSCore::Engine::signalEntryChanged, this, &KNSTransaction::anEntryChanged); TransactionModel::global()->addTransaction(this); std::function actionFunction; auto engine = res->knsBackend()->engine(); if(role == RemoveRole) actionFunction = [res, engine]() { engine->uninstall(res->entry()); }; else if (res->linkIds().isEmpty()) actionFunction = [res, engine]() { engine->install(res->entry()); }; else actionFunction = [res, engine]() { const auto links = res->linkIds(); for(auto i : links) engine->install(res->entry(), i); }; QTimer::singleShot(0, res, actionFunction); } void anEntryChanged(const KNSCore::EntryInternal& entry) { if (entry.uniqueId() == m_id) { switch (entry.status()) { case KNS3::Entry::Invalid: qWarning() << "invalid status for" << entry.uniqueId() << entry.status(); break; case KNS3::Entry::Installing: case KNS3::Entry::Updating: setStatus(CommittingStatus); break; case KNS3::Entry::Downloadable: case KNS3::Entry::Installed: case KNS3::Entry::Deleted: case KNS3::Entry::Updateable: if (status() != DoneStatus) { setStatus(DoneStatus); } break; } } } void cancel() override {} private: const QString m_id; }; Transaction* KNSBackend::removeApplication(AbstractResource* app) { auto res = qobject_cast(app); return new KNSTransaction(this, res, Transaction::RemoveRole); } Transaction* KNSBackend::installApplication(AbstractResource* app) { auto res = qobject_cast(app); return new KNSTransaction(this, res, Transaction::InstallRole); } Transaction* KNSBackend::installApplication(AbstractResource* app, const AddonList& /*addons*/) { return installApplication(app); } int KNSBackend::updatesCount() const { return m_updater->updatesCount(); } AbstractReviewsBackend* KNSBackend::reviewsBackend() const { return m_reviews; } static ResultsStream* voidStream() { return new ResultsStream(QStringLiteral("KNS-void"), {}); } ResultsStream* KNSBackend::search(const AbstractResourcesBackend::Filters& filter) { if (!m_isValid || (!filter.resourceUrl.isEmpty() && filter.resourceUrl.scheme() != QLatin1String("kns")) || !filter.mimetype.isEmpty()) return voidStream(); if (filter.resourceUrl.scheme() == QLatin1String("kns")) { return findResourceByPackageName(filter.resourceUrl); } else if (filter.state >= AbstractResource::Installed) { auto stream = new ResultsStream(QStringLiteral("KNS-installed")); const auto start = [this, stream, filter]() { if (m_isValid) { auto filterFunction = [&filter](AbstractResource* r) { return r->state()>=filter.state && (r->name().contains(filter.search, Qt::CaseInsensitive) || r->comment().contains(filter.search, Qt::CaseInsensitive)); }; const auto ret = kFilter>(m_resourcesByName, filterFunction); if (!ret.isEmpty()) Q_EMIT stream->resourcesFound(ret); } stream->finish(); }; if (isFetching()) { connect(this, &KNSBackend::initialized, stream, start); } else { QTimer::singleShot(0, stream, start); } return stream; } else if ((m_hasApplications && !filter.category) // If there is no category defined, we are searching in the root, and should include only application results // If there /is/ a category, make sure we actually are one of those requested before searching || (filter.category && kContains(m_categories, [&filter](const QString& cat) { return filter.category->matchesCategoryName(cat); }))) { auto r = new ResultsStream(QLatin1String("KNS-search-")+name()); searchStream(r, filter.search); return r; } return voidStream(); } void KNSBackend::searchStream(ResultsStream* stream, const QString &searchText) { Q_EMIT startingSearch(); auto start = [this, stream, searchText]() { Q_ASSERT(!isFetching()); if (!m_isValid) { stream->finish(); return; } // No need to explicitly launch a search, setting the search term already does that for us m_engine->setSearchTerm(searchText); m_onePage = false; m_responsePending = true; connect(stream, &ResultsStream::fetchMore, this, &KNSBackend::fetchMore); connect(this, &KNSBackend::receivedResources, stream, &ResultsStream::resourcesFound); connect(this, &KNSBackend::searchFinished, stream, &ResultsStream::finish); connect(this, &KNSBackend::startingSearch, stream, &ResultsStream::finish); }; if (m_responsePending) { connect(this, &KNSBackend::availableForQueries, stream, start, Qt::QueuedConnection); } else if (isFetching()) { connect(this, &KNSBackend::initialized, stream, start); } else { QTimer::singleShot(0, stream, start); } } ResultsStream * KNSBackend::findResourceByPackageName(const QUrl& search) { if (search.scheme() != QLatin1String("kns") || search.host() != name()) return voidStream(); const auto pathParts = search.path().split(QLatin1Char('/'), QString::SkipEmptyParts); if (pathParts.size() != 2) { Q_EMIT passiveMessage(i18n("Wrong KNewStuff URI: %1", search.toString())); return voidStream(); } const auto providerid = pathParts.at(0); const auto entryid = pathParts.at(1); auto stream = new ResultsStream(QLatin1String("KNS-byname-")+entryid); auto start = [this, entryid, stream, providerid]() { m_responsePending = true; m_engine->fetchEntryById(entryid); m_onePage = false; connect(m_engine, &KNSCore::Engine::signalError, stream, &ResultsStream::finish); connect(m_engine, &KNSCore::Engine::signalEntryDetailsLoaded, stream, [this, stream, entryid, providerid](const KNSCore::EntryInternal &entry) { if (entry.uniqueId() == entryid && providerid == QUrl(entry.providerId()).host()) { Q_EMIT stream->resourcesFound({resourceForEntry(entry)}); } else qWarning() << "found invalid" << entryid << entry.uniqueId() << providerid << QUrl(entry.providerId()).host(); m_responsePending = false; QTimer::singleShot(0, this, &KNSBackend::availableForQueries); stream->finish(); }); }; if (m_responsePending) { connect(this, &KNSBackend::availableForQueries, stream, start); } else { start(); } return stream; } bool KNSBackend::isFetching() const { return m_fetching; } AbstractBackendUpdater* KNSBackend::backendUpdater() const { return m_updater; } QString KNSBackend::displayName() const { return QStringLiteral("KNewStuff"); } +void KNSBackend::detailsLoaded(const KNSCore::EntryInternal& entry) +{ + auto res = resourceForEntry(entry); + res->longDescriptionChanged(); +} + #include "KNSBackend.moc" diff --git a/libdiscover/backends/KNSBackend/KNSBackend.h b/libdiscover/backends/KNSBackend/KNSBackend.h index 235f50db..174411f2 100644 --- a/libdiscover/backends/KNSBackend/KNSBackend.h +++ b/libdiscover/backends/KNSBackend/KNSBackend.h @@ -1,107 +1,108 @@ /*************************************************************************** * Copyright © 2012 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 . * ***************************************************************************/ #ifndef KNSBACKEND_H #define KNSBACKEND_H #include #include #include #include "Transaction/AddonList.h" #include "discovercommon_export.h" class KNSReviews; class KNSResource; class StandardBackendUpdater; namespace KNSCore { class Engine; } class DISCOVERCOMMON_EXPORT KNSBackend : public AbstractResourcesBackend { Q_OBJECT public: explicit KNSBackend(QObject* parent, const QString& iconName, const QString &knsrc); ~KNSBackend() override; Transaction* removeApplication(AbstractResource* app) override; Transaction* installApplication(AbstractResource* app) override; Transaction* installApplication(AbstractResource* app, const AddonList& addons) override; int updatesCount() const override; AbstractReviewsBackend* reviewsBackend() const override; AbstractBackendUpdater* backendUpdater() const override; bool isFetching() const override; ResultsStream* search(const AbstractResourcesBackend::Filters & filter) override; ResultsStream* findResourceByPackageName(const QUrl & search); QVector category() const override { return m_rootCategories; } bool hasApplications() const override { return m_hasApplications; } bool isValid() const override; QStringList extends() const override { return m_extends; } QString iconName() const { return m_iconName; } KNSCore::Engine* engine() const { return m_engine; } void checkForUpdates() override {} QString displayName() const override; Q_SIGNALS: void receivedResources(const QVector &resources); void searchFinished(); void startingSearch(); void availableForQueries(); void initialized(); public Q_SLOTS: void receivedEntries(const KNSCore::EntryInternal::List& entries); void statusChanged(const KNSCore::EntryInternal& entry); + void detailsLoaded(const KNSCore::EntryInternal& entry); void signalErrorCode(const KNSCore::ErrorCode& errorCode, const QString& message, const QVariant& metadata); private: void fetchInstalled(); KNSResource* resourceForEntry(const KNSCore::EntryInternal& entry); void setFetching(bool f); void markInvalid(const QString &message); void searchStream(ResultsStream* stream, const QString &searchText); void fetchMore(); bool m_onePage = false; bool m_responsePending = false; bool m_fetching; bool m_isValid; KNSCore::Engine* m_engine; QHash m_resourcesByName; KNSReviews* const m_reviews; QString m_name; QString m_iconName; StandardBackendUpdater* const m_updater; QStringList m_extends; QStringList m_categories; QVector m_rootCategories; QString m_displayName; bool m_initialized = false; bool m_hasApplications = false; }; #endif // KNSBACKEND_H diff --git a/libdiscover/backends/KNSBackend/KNSResource.cpp b/libdiscover/backends/KNSBackend/KNSResource.cpp index a40b5ac8..e2b40a62 100644 --- a/libdiscover/backends/KNSBackend/KNSResource.cpp +++ b/libdiscover/backends/KNSBackend/KNSResource.cpp @@ -1,282 +1,282 @@ /*************************************************************************** * Copyright © 2012 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 . * ***************************************************************************/ #include "KNSResource.h" #include "KNSBackend.h" #include #include #include #include #include #include "ReviewsBackend/Rating.h" #include KNSResource::KNSResource(const KNSCore::EntryInternal& entry, QStringList categories, KNSBackend* parent) : AbstractResource(parent) , m_categories(std::move(categories)) , m_entry(entry) , m_lastStatus(entry.status()) { connect(this, &KNSResource::stateChanged, parent, &KNSBackend::updatesCountChanged); } KNSResource::~KNSResource() = default; AbstractResource::State KNSResource::state() { switch(m_entry.status()) { case KNS3::Entry::Invalid: return Broken; case KNS3::Entry::Downloadable: return None; case KNS3::Entry::Installed: return Installed; case KNS3::Entry::Updateable: return Upgradeable; case KNS3::Entry::Deleted: case KNS3::Entry::Installing: case KNS3::Entry::Updating: return None; } return None; } KNSBackend * KNSResource::knsBackend() const { return qobject_cast(parent()); } QVariant KNSResource::icon() const { const QString thumbnail = m_entry.previewUrl(KNSCore::EntryInternal::PreviewSmall1); return thumbnail.isEmpty() ? knsBackend()->iconName() : m_entry.previewUrl(KNSCore::EntryInternal::PreviewSmall1); } QString KNSResource::comment() { QString ret = m_entry.shortSummary(); if(ret.isEmpty()) { ret = m_entry.summary(); int newLine = ret.indexOf(QLatin1Char('\n')); if(newLine>0) { ret.truncate(newLine); } ret.remove(QRegularExpression(QStringLiteral("\\[\\/?[a-z]*\\]"))); ret.remove(QRegularExpression(QStringLiteral("<[^>]*>"))); } return ret; } QString KNSResource::longDescription() { QString ret = m_entry.summary(); if (m_entry.shortSummary().isEmpty()) { const int newLine = ret.indexOf(QLatin1Char('\n')); if (newLine<0) ret.clear(); else ret = ret.mid(newLine+1).trimmed(); } ret.remove(QLatin1Char('\r')); ret.replace(QStringLiteral("[li]"), QStringLiteral("\n* ")); // Get rid of all BBCode markup we don't handle above ret.remove(QRegularExpression(QStringLiteral("\\[\\/?[a-z]*\\]"))); // Find anything that looks like a link (but which also is not some html // tag value or another already) and make it a link static const QRegularExpression urlRegExp(QStringLiteral("(^|\\s)([-a-zA-Z0-9@:%_\\+.~#?&//=]{2,256}\\.[a-z]{2,4}\\b(\\/[-a-zA-Z0-9@:;%_\\+.~#?&//=]*)?)"), QRegularExpression::CaseInsensitiveOption); ret.replace(urlRegExp, QStringLiteral("\\2")); return ret; } QString KNSResource::name() const { return m_entry.name(); } QString KNSResource::packageName() const { return m_entry.uniqueId(); } QStringList KNSResource::categories() { return m_categories; } QUrl KNSResource::homepage() { return m_entry.homepage(); } void KNSResource::setEntry(const KNSCore::EntryInternal& entry) { const bool diff = entry.status() != m_lastStatus; m_entry = entry; if (diff) { m_lastStatus = entry.status(); Q_EMIT stateChanged(); } } KNSCore::EntryInternal KNSResource::entry() const { return m_entry; } QJsonArray KNSResource::licenses() { - return { QJsonObject{ {QStringLiteral("name"), m_entry.license()} } }; + return { QJsonObject{ {QStringLiteral("name"), m_entry.license()}, {QStringLiteral("url"), QString()} } }; } int KNSResource::size() { const auto downloadInfo = m_entry.downloadLinkInformationList(); return downloadInfo.isEmpty() ? 0 : downloadInfo.at(0).size; } QString KNSResource::installedVersion() const { return m_entry.version(); } QString KNSResource::availableVersion() const { return !m_entry.updateVersion().isEmpty() ? m_entry.updateVersion() : m_entry.version(); } QString KNSResource::origin() const { return m_entry.providerId(); } QString KNSResource::section() { return m_entry.category(); } static void appendIfValid(QList& list, const QUrl &value, const QUrl &fallback = {}) { if (!list.contains(value)) { if (value.isValid() && !value.isEmpty()) list << value; else if (!fallback.isEmpty()) appendIfValid(list, fallback); } } void KNSResource::fetchScreenshots() { QList preview; appendIfValid(preview, QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewSmall1))); appendIfValid(preview, QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewSmall2))); appendIfValid(preview, QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewSmall3))); QList screenshots; appendIfValid(screenshots, QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewBig1)), QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewSmall1))); appendIfValid(screenshots, QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewBig2)), QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewSmall2))); appendIfValid(screenshots, QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewBig3)), QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewSmall3))); emit screenshotsFetched(preview, screenshots); } void KNSResource::fetchChangelog() { emit changelogFetched(m_entry.changelog()); } QStringList KNSResource::extends() const { return knsBackend()->extends(); } QStringList KNSResource::executables() const { if (knsBackend()->engine()->hasAdoptionCommand()) return {knsBackend()->engine()->adoptionCommand(m_entry)}; else return {}; } QUrl KNSResource::url() const { return QUrl(QStringLiteral("kns://")+knsBackend()->name() + QLatin1Char('/') + QUrl(m_entry.providerId()).host() + QLatin1Char('/') + m_entry.uniqueId()); } void KNSResource::invokeApplication() const { QStringList exes = executables(); if(!exes.isEmpty()) { const QString exe = exes.constFirst(); auto args = KShell::splitArgs(exe); QProcess::startDetached(args.takeFirst(), args); } else { qWarning() << "cannot execute" << packageName(); } } QString KNSResource::executeLabel() const { if(knsBackend()->hasApplications()) { return i18n("Launch"); } return i18n("Use"); } QDate KNSResource::releaseDate() const { return m_entry.updateReleaseDate().isNull() ? m_entry.releaseDate() : m_entry.updateReleaseDate(); } QVector KNSResource::linkIds() const { QVector ids; const auto linkInfo = m_entry.downloadLinkInformationList(); for(const auto &e : linkInfo) { if (e.isDownloadtypeLink) ids << e.id; } return ids; } QUrl KNSResource::donationURL() { return QUrl(m_entry.donationLink()); } Rating * KNSResource::ratingInstance() { if (!m_rating) { const int noc = m_entry.numberOfComments(); const int rating = m_entry.rating(); Q_ASSERT(rating <= 100); return new Rating( packageName(), noc, rating / 10 ); } return m_rating; } QString KNSResource::author() const { return m_entry.author().name(); } diff --git a/libdiscover/resources/AbstractResource.h b/libdiscover/resources/AbstractResource.h index c1e5d7a3..b4507af3 100644 --- a/libdiscover/resources/AbstractResource.h +++ b/libdiscover/resources/AbstractResource.h @@ -1,244 +1,245 @@ /*************************************************************************** * Copyright © 2012 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 . * ***************************************************************************/ #ifndef ABSTRACTRESOURCE_H #define ABSTRACTRESOURCE_H #include #include #include #include #include #include #include #include #include #include "discovercommon_export.h" #include "PackageState.h" class Category; class Rating; class AbstractResourcesBackend; /** * \class AbstractResource AbstractResource.h "AbstractResource.h" * * \brief This is the base class of all resources. * * Each backend must reimplement its own resource class which needs to derive from this one. */ class DISCOVERCOMMON_EXPORT AbstractResource : public QObject { Q_OBJECT Q_PROPERTY(QString name READ name CONSTANT) Q_PROPERTY(QString packageName READ packageName CONSTANT) Q_PROPERTY(QString comment READ comment CONSTANT) Q_PROPERTY(QVariant icon READ icon NOTIFY iconChanged) Q_PROPERTY(bool canExecute READ canExecute CONSTANT) Q_PROPERTY(State state READ state NOTIFY stateChanged) Q_PROPERTY(QString status READ status NOTIFY stateChanged) Q_PROPERTY(QStringList category READ categories CONSTANT) Q_PROPERTY(QUrl homepage READ homepage CONSTANT) Q_PROPERTY(QUrl helpURL READ helpURL CONSTANT) Q_PROPERTY(QUrl bugURL READ bugURL CONSTANT) Q_PROPERTY(QUrl donationURL READ donationURL CONSTANT) Q_PROPERTY(bool canUpgrade READ canUpgrade NOTIFY stateChanged) Q_PROPERTY(bool isInstalled READ isInstalled NOTIFY stateChanged) Q_PROPERTY(QJsonArray licenses READ licenses CONSTANT) - Q_PROPERTY(QString longDescription READ longDescription CONSTANT) + Q_PROPERTY(QString longDescription READ longDescription NOTIFY longDescriptionChanged) Q_PROPERTY(QString origin READ origin CONSTANT) Q_PROPERTY(QString displayOrigin READ displayOrigin CONSTANT) Q_PROPERTY(int size READ size NOTIFY sizeChanged) Q_PROPERTY(QString sizeDescription READ sizeDescription NOTIFY sizeChanged) Q_PROPERTY(QString installedVersion READ installedVersion NOTIFY stateChanged) Q_PROPERTY(QString availableVersion READ availableVersion NOTIFY stateChanged) Q_PROPERTY(QString section READ section CONSTANT) Q_PROPERTY(QStringList mimetypes READ mimetypes CONSTANT) Q_PROPERTY(AbstractResourcesBackend* backend READ backend CONSTANT) Q_PROPERTY(Rating* rating READ rating NOTIFY ratingFetched) Q_PROPERTY(QString appstreamId READ appstreamId CONSTANT) Q_PROPERTY(QString categoryDisplay READ categoryDisplay CONSTANT) Q_PROPERTY(QUrl url READ url CONSTANT) Q_PROPERTY(QString executeLabel READ executeLabel CONSTANT) Q_PROPERTY(QString sourceIcon READ sourceIcon CONSTANT) Q_PROPERTY(QString author READ author CONSTANT) Q_PROPERTY(QDate releaseDate READ releaseDate NOTIFY stateChanged) public: /** * This describes the state of the resource */ enum State { /** * When the resource is somehow broken */ Broken, /** * This means that the resource is neither installed nor broken */ None, /** * The resource is installed and up-to-date */ Installed, /** * The resource is installed and an update is available */ Upgradeable }; Q_ENUM(State) /** * Constructs the AbstractResource with its corresponding backend */ explicit AbstractResource(AbstractResourcesBackend* parent); ~AbstractResource() override; ///used as internal identification of a resource virtual QString packageName() const = 0; ///resource name to be displayed virtual QString name() const = 0; ///short description of the resource virtual QString comment() = 0; ///xdg-compatible icon name to represent the resource, url or QIcon virtual QVariant icon() const = 0; ///@returns whether invokeApplication makes something /// false if not overridden virtual bool canExecute() const = 0; ///executes the resource, if applies. Q_SCRIPTABLE virtual void invokeApplication() const = 0; virtual State state() = 0; virtual QStringList categories() = 0; ///@returns a URL that points to the app's website virtual QUrl homepage(); ///@returns a URL that points to the app's online documentation virtual QUrl helpURL(); ///@returns a URL that points to the place where you can file a bug virtual QUrl bugURL(); ///@returns a URL that points to the place where you can donate money to the app developer virtual QUrl donationURL(); enum Type { Application, Addon, Technical }; Q_ENUM(Type); virtual Type type() const = 0; virtual int size() = 0; virtual QString sizeDescription(); ///@returns a list of pairs with the name of the license and a URL pointing at it virtual QJsonArray licenses() = 0; virtual QString installedVersion() const = 0; virtual QString availableVersion() const = 0; virtual QString longDescription() = 0; virtual QString origin() const = 0; QString displayOrigin() const; virtual QString section() = 0; virtual QString author() const = 0; ///@returns what kind of mime types the resource can consume virtual QStringList mimetypes() const; virtual QList addonsInformation() = 0; virtual QStringList extends() const; virtual QString appstreamId() const; void addMetadata(const QString &key, const QJsonValue &value); QJsonValue getMetadata(const QString &key); bool canUpgrade(); bool isInstalled(); ///@returns a user-readable explanation of the resource status ///by default, it will specify what state() is returning virtual QString status(); AbstractResourcesBackend* backend() const; /** * @returns a name sort key for faster sorting */ QCollatorSortKey nameSortKey(); /** * Convenience method to fetch the resource's rating * * @returns the rating for the resource or null if not available */ Rating* rating() const; /** * @returns a string defining the categories the resource belongs to */ QString categoryDisplay() const; bool categoryMatches(Category* cat); QSet categoryObjects(const QVector& cats) const; /** * @returns a url that uniquely identifies the application */ virtual QUrl url() const; virtual QString executeLabel() const; virtual QString sourceIcon() const = 0; /** * @returns the date of the resource's most recent release */ virtual QDate releaseDate() const = 0; public Q_SLOTS: virtual void fetchScreenshots(); virtual void fetchChangelog() = 0; virtual void fetchUpdateDetails() { fetchChangelog(); } Q_SIGNALS: void iconChanged(); void sizeChanged(); void stateChanged(); void ratingFetched(); + void longDescriptionChanged(); ///response to the fetchScreenshots method ///@p thumbnails and @p screenshots should have the same number of elements void screenshotsFetched(const QList& thumbnails, const QList& screenshots); void changelogFetched(const QString& changelog); private: void reportNewState(); // TODO: make it std::optional or make QCollatorSortKey() QScopedPointer m_collatorKey; QJsonObject m_metadata; }; Q_DECLARE_METATYPE(QVector) #endif // ABSTRACTRESOURCE_H