diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e906163..441226f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,79 +1,75 @@ project(discover) set(PROJECT_VERSION "5.8.90") set(PROJECT_VERSION_MAJOR 5) cmake_minimum_required(VERSION 2.8.12) find_package(ECM REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} "${CMAKE_SOURCE_DIR}/cmake") find_package(Qt5 5.2.0 REQUIRED CONFIG COMPONENTS Widgets Test Network Xml Concurrent DBus Quick) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMInstallIcons) include(ECMMarkAsTest) include(ECMAddTests) include(GenerateExportHeader) -find_package(KF5 REQUIRED CoreAddons Config Crash DBusAddons I18n Archive Declarative XmlGui) -find_package(KF5TextWidgets REQUIRED) +find_package(KF5 5.24 REQUIRED CoreAddons Config Crash DBusAddons I18n Archive Declarative XmlGui) +find_package(KF5Kirigami 1.0 REQUIRED) find_package(packagekitqt5) if (NOT packagekitqt5_FOUND) find_package(QApt 3.0.0) if(QApt_FOUND) find_package(DebconfKDE 1.0.0 REQUIRED) find_package(KF5 REQUIRED IconThemes Notifications KIO) endif() endif() -find_package(AppstreamQt 0.9.2) +find_package(AppstreamQt 0.10) find_package(KF5Attica 5.23) find_package(KF5NewStuff 5.23) -if (${KF5_VERSION} VERSION_GREATER "5.14.0") - add_definitions(-DWITH_KCRASH_INIT) -endif() - configure_file(DiscoverVersion.h.in DiscoverVersion.h) add_subdirectory(libdiscover) add_subdirectory(discover) add_subdirectory(exporter) option(WITH_NOTIFIER "Build and install the notifier plasmoid" ON) if(WITH_NOTIFIER) find_package(KF5 REQUIRED Notifications KIO) add_subdirectory(notifier) endif() set_package_properties(QApt PROPERTIES DESCRIPTION "Qt wrapper around the libapt-pkg library" PURPOSE "Used to support apt-based distribution systems" TYPE OPTIONAL) set_package_properties(KF5Attica PROPERTIES DESCRIPTION "KDE Framework that implements the Open Collaboration Services API" PURPOSE "Required to build the KNewStuff3 backend" TYPE OPTIONAL) set_package_properties(KF5NewStuff PROPERTIES DESCRIPTION "Qt library that allows to interact with KNewStuff implementations" PURPOSE "Required to build the KNS backend" TYPE OPTIONAL) set_package_properties(Bodega PROPERTIES DESCRIPTION "Library that exposes Bodega resources" PURPOSE "Required to build the Bodega backend" TYPE OPTIONAL) set_package_properties(packagekitqt5 PROPERTIES DESCRIPTION "Library that exposes PackageKit resources" URL "http://www.packagekit.org" PURPOSE "Required to build the PackageKit backend" TYPE OPTIONAL) set_package_properties(AppstreamQt PROPERTIES DESCRIPTION "Library that lists Appstream resources" URL "http://www.freedesktop.org" PURPOSE "Required to build the PackageKit backend" TYPE OPTIONAL) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/discover/autotests/DiscoverTest.qml b/discover/autotests/DiscoverTest.qml index a45b32ec..2e763a4d 100644 --- a/discover/autotests/DiscoverTest.qml +++ b/discover/autotests/DiscoverTest.qml @@ -1,105 +1,106 @@ import QtQuick 2.1 import QtQuick.Controls 1.4 import QtTest 1.1 import org.kde.discover.app 1.0 Item { id: testRoot signal reset() property QtObject appRoot StackViewDelegate { id: noTransitionsDelegate popTransition: StackViewTransition { immediate: true } pushTransition: StackViewTransition { immediate: true } replaceTransition: StackViewTransition { immediate: true } } function verify(condition, msg) { if (!condition) { console.trace(); var e = new Error(condition + (msg ? (": " + msg) : "")) e.object = testRoot; throw e; } } function compare(valA, valB, msg) { if (valA !== valB) { console.trace(); var e = new Error(valA + " !== " + valB + (msg ? (": " + msg) : "")) e.object = testRoot; throw e; } } function typeName(obj) { var name = obj.toString(); var idx = name.indexOf("_QMLTYPE_"); return name.substring(0, idx); } function isType(obj, typename) { return obj && obj.toString().indexOf(typename+"_QMLTYPE_") == 0 } function findChild(obj, typename) { if (isType(obj, typename)) return obj; for(var v in obj.data) { var v = findChild(obj.data[v], typename) if (v) return v } return null } SignalSpy { id: spy } function waitForSignal(object, name, timeout) { if (!timeout) timeout = 5000; spy.signalName = "" spy.target = object; spy.signalName = name; verify(spy); + verify(spy.valid); try { spy.wait(timeout); } catch (e) { - console.warn("wait for signal unsuccessful") + console.warn("wait for signal '"+name+"' unsuccessful") return false; } return spy.count>0; } function waitForRendering() { return waitForSignal(Helpers.mainWindow, "frameSwapped") } property string currentTest: "" onCurrentTestChanged: console.log("changed to test", currentTest) Connections { target: ResourcesModel property bool done: false onIsFetchingChanged: { if (ResourcesModel.isFetching) return; done = true; for(var v in testRoot) { if (v.indexOf("test_") == 0) { testRoot.currentTest = v; testRoot.reset(); testRoot[v](); } } Qt.quit(); } } } diff --git a/discover/main.cpp b/discover/main.cpp index 777968dd..eb987938 100644 --- a/discover/main.cpp +++ b/discover/main.cpp @@ -1,148 +1,146 @@ /* * 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 "DiscoverMainWindow.h" #include #include "DiscoverVersion.h" #include #include DiscoverMainWindow::CompactMode decodeCompactMode(const QString &str) { if (str == QLatin1String("auto")) return DiscoverMainWindow::Auto; else if (str == QLatin1String("compact")) return DiscoverMainWindow::Compact; else if (str == QLatin1String("full")) return DiscoverMainWindow::Full; return DiscoverMainWindow::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("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; } bool processArgs(QCommandLineParser* parser, DiscoverMainWindow* mainWindow) { if(parser->isSet(QStringLiteral("application"))) mainWindow->openApplication(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"))); foreach(const QString &arg, parser->positionalArguments()) { QUrl url(arg); if (url.scheme() == QLatin1String("appstream")) { mainWindow->openApplication(url.host()); } else { QTextStream(stdout) << "unrecognized url" << url.toDisplayString() << '\n'; return true; } } return false; } 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); -#ifdef WITH_KCRASH_INIT KCrash::initialize(); -#endif KLocalizedString::setApplicationDomain("plasma-discover"); KAboutData about(QStringLiteral("discover"), i18n("Discover"), version, i18n("An application explorer"), KAboutLicense::GPL, i18n("© 2010-2016 Plasma Development Team")); about.addAuthor(i18n("Aleix Pol Gonzalez"), QString(), QStringLiteral("aleixpol@blue-systems.com")); about.addAuthor(i18n("Jonathan Thomas"), QString(), QStringLiteral("echidnaman@kubuntu.org")); about.setProductName("discover/discover"); KAboutData::setApplicationData(about); DiscoverMainWindow *mainWindow = nullptr; { QScopedPointer parser(createParser()); parser->process(app); about.processCommandLine(parser.data()); DiscoverBackendsFactory::processCommandLine(parser.data(), parser->isSet(QStringLiteral("test"))); if (parser->isSet(QStringLiteral("test"))) { QStandardPaths::setTestModeEnabled(true); } KDBusService* service = new KDBusService(KDBusService::Unique, &app); mainWindow = new DiscoverMainWindow(decodeCompactMode(parser->value(QStringLiteral("compact")))); QObject::connect(&app, &QApplication::aboutToQuit, mainWindow, &DiscoverMainWindow::deleteLater); QObject::connect(service, &KDBusService::activateRequested, mainWindow, [mainWindow](const QStringList &arguments, const QString &/*workingDirectory*/){ mainWindow->rootObject()->raise(); if (arguments.isEmpty()) return; QScopedPointer parser(createParser()); parser->process(arguments); processArgs(parser.data(), mainWindow); }); if (processArgs(parser.data(), mainWindow)) return 1; if(parser->isSet(QStringLiteral("listmodes"))) { QTextStream(stdout) << i18n("Available modes:\n"); foreach(const QString& mode, mainWindow->modes()) QTextStream(stdout) << " * " << mode << '\n'; 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/DiscoverDrawer.qml b/discover/qml/DiscoverDrawer.qml index cf0b5827..9800771e 100644 --- a/discover/qml/DiscoverDrawer.qml +++ b/discover/qml/DiscoverDrawer.qml @@ -1,191 +1,207 @@ /*************************************************************************** * Copyright © 2015 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ -import QtQuick 2.6 +import QtQuick 2.5 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.1 import org.kde.discover 1.0 import org.kde.discover.app 1.0 import org.kde.kirigami 1.0 as Kirigami import "navigation.js" as Navigation Kirigami.GlobalDrawer { id: drawer bannerImageSource: "qrc:/icons/banner.svg" topPadding: -50 leftPadding: 0 rightPadding: 0 bottomPadding: 0 resetMenuOnTriggered: false - readonly property var currentRootCategory: window.leftPage ? rootCategory(window.leftPage.category) : null + onBannerClicked: { + Navigation.openHome(); + } + + onCurrentSubMenuChanged: { + if (currentSubMenu) + currentSubMenu.trigger() + else if (searchField.text !== "") + window.leftPage.category = null + else + Navigation.openHome() + + } topContent: TextField { id: searchField Layout.fillWidth: true Layout.leftMargin: Kirigami.Units.smallSpacing Layout.rightMargin: Kirigami.Units.smallSpacing enabled: window.leftPage && (window.leftPage.searchFor != null || window.leftPage.hasOwnProperty("search")) Component.onCompleted: { searchField.forceActiveFocus() } Shortcut { sequence: "Ctrl+F" onActivated: { searchField.forceActiveFocus() } } placeholderText: (!enabled || window.leftPage.title.length === 0) ? i18n("Search...") : i18n("Search in '%1'...", window.leftPage.title) onTextChanged: searchTimer.running = true Connections { ignoreUnknownSignals: true target: window.leftPage onClearSearch: { searchField.text = "" // console.log("search cleared") } } Timer { id: searchTimer running: false repeat: false interval: 200 onTriggered: { var curr = window.leftPage; - if (!curr.hasOwnProperty("search")) + if (!curr.hasOwnProperty("search")) { + Navigation.clearStack() Navigation.openApplicationList( { search: parent.text }) - else + } else curr.search = parent.text; } } } ColumnLayout { spacing: 0 Layout.fillWidth: true Kirigami.Separator { Layout.fillWidth: true } ProgressView { separatorVisible: false } Kirigami.BasicListItem { checked: installedAction.checked icon: installedAction.iconName label: installedAction.text separatorVisible: false onClicked: { installedAction.trigger() drawer.resetMenu() } } Kirigami.BasicListItem { checked: settingsAction.checked icon: settingsAction.iconName label: settingsAction.text separatorVisible: false onClicked: { settingsAction.trigger() drawer.resetMenu() } } Kirigami.BasicListItem { enabled: updateAction.enabled checked: updateAction.checked icon: updateAction.iconName label: updateAction.text separatorVisible: false onClicked: { updateAction.trigger() drawer.resetMenu() } backgroundColor: enabled ? "orange" : Kirigami.Theme.viewBackgroundColor } } function rootCategory(cat) { var ret = null while (cat) { ret = cat cat = cat.parent } return ret } Component { id: categoryActionComponent Kirigami.Action { property QtObject category readonly property bool itsMe: window.leftPage && window.leftPage.hasOwnProperty("category") && (window.leftPage.category == category) text: category.name checkable: itsMe checked: itsMe visible: (!window.leftPage || !window.leftPage.subcategories || window.leftPage.subcategories === undefined || searchField.text.length === 0 || category.contains(window.leftPage.subcategories) ) onTriggered: { - Navigation.openCategory(category, searchField.text) + if (window.leftPage.category === undefined) + Navigation.openCategory(category, searchField.text) + else + window.leftPage.category = category } } } function createCategoryActions(parent, categories) { var actions = [] for(var i in categories) { var cat = categories[i]; var catAction = categoryActionComponent.createObject(parent, {category: cat}); catAction.children = createCategoryActions(catAction, cat.subcategories); actions.push(catAction) } return actions; } CategoryModel { id: rootCategories Component.onCompleted: { resetCategories(); drawer.actions = createCategoryActions(rootCategories, rootCategories.categories) } } modal: Helpers.isCompact handleVisible: Helpers.isCompact states: [ State { name: "full" when: !Helpers.isCompact PropertyChanges { target: drawer; opened: true } } ] } diff --git a/discover/qml/DiscoverWindow.qml b/discover/qml/DiscoverWindow.qml index 90a2f6a2..04ba7cb1 100644 --- a/discover/qml/DiscoverWindow.qml +++ b/discover/qml/DiscoverWindow.qml @@ -1,155 +1,156 @@ import QtQuick 2.1 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.1 import org.kde.discover 1.0 import org.kde.discover.app 1.0 import org.kde.kirigami 1.0 as Kirigami import "navigation.js" as Navigation Kirigami.ApplicationWindow { id: window readonly property Component applicationListComp: Qt.createComponent("qrc:/qml/ApplicationsListPage.qml") readonly property Component applicationComp: Qt.createComponent("qrc:/qml/ApplicationPage.qml") readonly property Component reviewsComp: Qt.createComponent("qrc:/qml/ReviewsPage.qml") //toplevels readonly property Component topBrowsingComp: Qt.createComponent("qrc:/qml/BrowsingPage.qml") readonly property Component topInstalledComp: Qt.createComponent("qrc:/qml/InstalledPage.qml") readonly property Component topUpdateComp: Qt.createComponent("qrc:/qml/UpdatesPage.qml") readonly property Component topSourcesComp: Qt.createComponent("qrc:/qml/SourcesPage.qml") readonly property QtObject stack: window.pageStack property Component currentTopLevel: defaultStartup ? topBrowsingComp : loadingComponent property bool defaultStartup: true property bool navigationEnabled: true objectName: "DiscoverMainWindow" title: leftPage ? leftPage.title : "" header: null visible: true minimumWidth: 300 minimumHeight: 300 pageStack.defaultColumnWidth: Kirigami.Units.gridUnit * 25 readonly property var leftPage: window.stack.depth>0 ? window.stack.get(0) : null Component.onCompleted: { Helpers.mainWindow = window if (app.isRoot) showPassiveNotification(i18n("Running as root is discouraged and unnecessary.")); } function clearSearch() { if (loader.item) loader.item.clearSearch(); } Component { id: loadingComponent Kirigami.Page { title: label.text Label { id: label text: i18n("Loading...") font.pointSize: 52 anchors.centerIn: parent } } } ExclusiveGroup { id: appTabs } property list awesome: [ TopLevelPageData { iconName: "tools-wizard" text: i18n("Discover") component: topBrowsingComp objectName: "discover" shortcut: "Alt+D" } ] TopLevelPageData { id: installedAction text: TransactionModel.count == 0 ? i18n("Installed") : i18n("Installing...") component: topInstalledComp objectName: "installed" shortcut: "Alt+I" } TopLevelPageData { id: updateAction iconName: enabled ? "update-low" : "update-none" text: !enabled ? i18n("No Updates") : i18nc("Update section name", "Update (%1)", ResourcesModel.updatesCount) enabled: ResourcesModel.updatesCount>0 component: topUpdateComp objectName: "update" shortcut: "Alt+U" } TopLevelPageData { id: settingsAction iconName: "settings" text: i18n("Settings") component: topSourcesComp objectName: "settings" shortcut: "Alt+S" } TopLevelPageData { id: sources text: i18n("Configure Sources...") iconName: "repository" shortcut: "Alt+S" component: topSourcesComp } Connections { target: app onOpenApplicationInternal: { currentTopLevel = topBrowsingComp; Navigation.openApplication(app) } onListMimeInternal: { currentTopLevel = topBrowsingComp; Navigation.openApplicationMime(mime) } onListCategoryInternal: { currentTopLevel = topBrowsingComp; Navigation.openCategory(cat, "") } onPreventedClose: showPassiveNotification(i18n("Could not close the application, there are tasks that need to be done."), 3000) onUnableToFind: { showPassiveNotification(i18n("Unable to find resource: %1", resid)); Navigation.openHome() } } globalDrawer: DiscoverDrawer {} onCurrentTopLevelChanged: { if(currentTopLevel && currentTopLevel.status==Component.Error) { console.log("status error: "+currentTopLevel.errorString()) } var stackView = window.pageStack; stackView.clear() if (currentTopLevel) stackView.push(currentTopLevel, {}, window.status!=Component.Ready) } -// ColumnLayout { -// spacing: 0 -// anchors.fill: parent -// -// Repeater { -// model: MessageActionsModel { -// filterPriority: QAction.HighPriority -// } -// delegate: MessageAction { -// Layout.fillWidth: true -// height: Layout.minimumHeight -// theAction: action -// } -// } -// } + Menu { + id: actionsMenu + } + + Instantiator { + model: MessageActionsModel {} + delegate: MenuItem { + action: ActionBridge { action: model.action } + } + onObjectAdded: { + actionsMenu.insertItem(index, object) + } + onObjectRemoved: { + object.destroy() + } + } } diff --git a/discover/qml/SourcesPage.qml b/discover/qml/SourcesPage.qml index 0ef9f910..13d1a28c 100644 --- a/discover/qml/SourcesPage.qml +++ b/discover/qml/SourcesPage.qml @@ -1,148 +1,159 @@ import QtQuick 2.1 import QtQuick.Controls 1.1 import QtQuick.Layouts 1.1 import org.kde.discover 1.0 import org.kde.discover.app 1.0 import org.kde.kquickcontrolsaddons 2.0 import org.kde.kirigami 1.0 as Kirigami import "navigation.js" as Navigation DiscoverPage { id: page clip: true title: i18n("Settings") ListView { model: SourcesModel Menu { id: sourcesMenu } header: PageHeader { anchors { left: parent.left right: parent.right } RowLayout { Layout.fillWidth: true Layout.leftMargin: Kirigami.Units.gridUnit Layout.rightMargin: Kirigami.Units.gridUnit Layout.topMargin: Kirigami.Units.smallSpacing Layout.bottomMargin: Kirigami.Units.smallSpacing ToolButton { // iconName: "list-add" text: i18n("Add Source") enabled: sourcesMenu.items.count > 0 tooltip: text menu: sourcesMenu } Repeater { model: SourcesModel.actions delegate: RowLayout { QIconItem { icon: modelData.icon } ToolButton { height: parent.height action: Action { readonly property QtObject action: modelData text: action.text onTriggered: action.trigger() enabled: action.enabled } } } } + ToolButton { + text: i18n("More...") + menu: actionsMenu + enabled: actionsMenu.items.length>0 + } + ToolButton { text: i18n("Help...") menu: Menu { MenuItem { action: ActionBridge { action: app.action("help_about_app") } } MenuItem { action: ActionBridge { action: app.action("help_report_bug") } } } } } } delegate: ColumnLayout { id: sourceDelegate anchors { left: parent.left right: parent.right leftMargin: Kirigami.Units.largeSpacing rightMargin: Kirigami.Units.largeSpacing } property QtObject sourceBackend: model.sourceBackend AddSourceDialog { id: addSourceDialog source: sourceDelegate.sourceBackend } MenuItem { id: menuItem text: model.display onTriggered: { try { addSourceDialog.open() addSourceDialog.visible = true } catch (e) { console.log("error loading dialog:", e) } } } Component.onCompleted: { sourcesMenu.insertItem(0, menuItem) } Kirigami.Heading { Layout.topMargin: Kirigami.Units.largeSpacing Layout.bottomMargin: Kirigami.Units.smallSpacing text: sourceBackend.name } spacing: 0 Repeater { model: sourceBackend.sources delegate: Kirigami.SwipeListItem { enabled: display.length>0 onClicked: Navigation.openApplicationListSource(model.display) actions: [ Kirigami.Action { enabled: display.length>0 iconName: "view-filter" tooltip: i18n("Browse the origin's resources") onTriggered: Navigation.openApplicationListSource(model.display) }, Kirigami.Action { iconName: "edit-delete" tooltip: i18n("Delete the origin") onTriggered: sourceDelegate.sourceBackend.removeSource(model.display) } ] RowLayout { + anchors { + left: parent.left + right: parent.right + } CheckBox { id: enabledBox enabled: false //TODO: implement the application of this change checked: model.checked != Qt.Unchecked } Label { Layout.fillWidth: true - elide: Text.ElideRight text: model.display } Label { + Layout.fillWidth: true text: model.toolTip + elide: Text.ElideRight } } } } } } } diff --git a/discover/qml/UpdatesPage.qml b/discover/qml/UpdatesPage.qml index 2bae8b23..f6e2e49a 100644 --- a/discover/qml/UpdatesPage.qml +++ b/discover/qml/UpdatesPage.qml @@ -1,251 +1,251 @@ import QtQuick.Controls 1.2 import QtQuick.Layouts 1.1 import QtQuick 2.1 import org.kde.discover 1.0 import org.kde.discover.app 1.0 import org.kde.kquickcontrolsaddons 2.0 import org.kde.kcoreaddons 1.0 import "navigation.js" as Navigation import org.kde.kirigami 1.0 as Kirigami DiscoverPage { id: page title: i18n("Updates") function start() { resourcesUpdatesModel.updateAll() } property string footerLabel: "" //TODO: use supportsRefreshing to fetch updates ListView { id: updatesView ResourcesUpdatesModel { id: resourcesUpdatesModel onIsProgressingChanged: { window.navigationEnabled = !isProgressing if (!isProgressing) { resourcesUpdatesModel.prepare() } } Component.onCompleted: { if (!isProgressing) { resourcesUpdatesModel.prepare() } } } UpdateModel { id: updateModel backend: resourcesUpdatesModel } header: PageHeader { id: header anchors { left: parent.left right: parent.right } background: "qrc:/icons/updatescrop.jpg" RowLayout { Layout.fillWidth: true Layout.leftMargin: Kirigami.Units.gridUnit Layout.rightMargin: Kirigami.Units.gridUnit Layout.topMargin: Kirigami.Units.smallSpacing Layout.bottomMargin: Kirigami.Units.smallSpacing - enabled: !resourcesUpdatesModel.isProgressing visible: resourcesUpdatesModel.isProgressing || updateModel.hasUpdates LabelBackground { text: updateModel.toUpdateCount + " (" + updateModel.updateSize+")" } Label { text: i18n("updates selected") } LabelBackground { id: unselectedItem readonly property int unselected: (updateModel.totalUpdatesCount - updateModel.toUpdateCount) text: unselected visible: unselected>0 } Label { text: i18n("updates not selected") visible: unselectedItem.visible } Item { Layout.fillWidth: true} Button { id: startButton text: unselectedItem.visible ? i18n("Update Selected") : i18n("Update All") + enabled: !resourcesUpdatesModel.isProgressing onClicked: page.start() } } } footer: ColumnLayout { anchors.right: parent.right anchors.left: parent.left Kirigami.Heading { Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter horizontalAlignment: Text.AlignHCenter visible: page.footerLabel !== "" text: page.footerLabel } QIconItem { Layout.alignment: Qt.AlignHCenter visible: page.footerLabel !== "" icon: "update-none" opacity: 0.3 width: 200 height: 200 } Item { visible: page.footerLabel === "" height: Kirigami.Units.gridUnit width: 1 } } model: updateModel section { property: "section" delegate: Kirigami.Heading { x: Kirigami.Units.gridUnit level: 2 text: section } } spacing: Kirigami.Units.smallSpacing delegate: Kirigami.AbstractListItem { x: Kirigami.Units.gridUnit width: ListView.view.width - Kirigami.Units.gridUnit * 2 - enabled: !resourcesUpdatesModel.isProgressing onEnabledChanged: if (!enabled) { layout.extended = false; } ColumnLayout { id: layout anchors { left: parent.left right: parent.right } property bool extended: false RowLayout { Layout.fillWidth: true Layout.fillHeight: true CheckBox { anchors.verticalCenter: parent.verticalCenter checked: model.checked == Qt.Checked onClicked: model.checked = (model.checked==Qt.Checked ? Qt.Unchecked : Qt.Checked) } QIconItem { Layout.fillHeight: true Layout.preferredWidth: height icon: decoration } Label { Layout.fillWidth: true text: i18n("%1 (%2)", display, version) elide: Text.ElideRight } LabelBackground { Layout.minimumWidth: Kirigami.Units.gridUnit * 6 text: size progress: resourceProgress/100 } } ScrollView { id: view Layout.fillHeight: true Layout.fillWidth: true frameVisible: true visible: layout.extended && changelog.length>0 Label { width: view.viewport.width text: changelog textFormat: Text.RichText wrapMode: Text.WordWrap } } Button { text: i18n("Open") visible: layout.extended + enabled: !resourcesUpdatesModel.isProgressing onClicked: Navigation.openApplication(resource) } } onClicked: { updateModel.fetchChangelog(index) layout.extended = !layout.extended } } } readonly property var secSinceUpdate: resourcesUpdatesModel.secsToLastUpdate state: ( ResourcesModel.isFetching ? "fetching" : updateModel.hasUpdates ? "has-updates" : resourcesUpdatesModel.isProgressing ? "progressing" : 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; title: i18nc("@info", "Loading...") } }, State { name: "progressing" PropertyChanges { target: page; title: i18nc("@info", "Updating...") } PropertyChanges { target: page; footerLabel: resourcesUpdatesModel.progress<=0 ? i18nc("@info", "Fetching updates") : "" } }, State { name: "has-updates" PropertyChanges { target: page; title: i18nc("@info", "Updates") } }, State { name: "now-uptodate" PropertyChanges { target: page; title: i18nc("@info", "The system is up to date") } PropertyChanges { target: page; footerLabel: i18nc("@info", "No updates") } }, State { name: "uptodate" PropertyChanges { target: page; title: i18nc("@info", "The system is up to date") } }, State { name: "medium" PropertyChanges { target: page; title: i18nc("@info", "No updates are available") } }, State { name: "low" PropertyChanges { target: page; title: i18nc("@info", "Should check for updates") } }, State { name: "unknown" PropertyChanges { target: page; title: i18nc("@info", "It is unknown when the last check for updates was") } } ] } diff --git a/discover/qml/navigation.js b/discover/qml/navigation.js index 9f56f343..b4cedcfc 100644 --- a/discover/qml/navigation.js +++ b/discover/qml/navigation.js @@ -1,60 +1,62 @@ /* * 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. */ function clearStack() { window.currentTopLevel=null window.stack.clear(); } function openApplicationListSource(origin) { openApplicationList({ originFilter: origin, title: origin }) } function openApplicationMime(mime) { + clearStack() openApplicationList({ mimeTypeFilter: mime , title: i18n("Resources for '%1'", mime) }) } function openApplicationList(props) { - clearStack() var page = window.stack.push(applicationListComp, props) if (props.search === "") page.clearSearch() } function openCategory(cat, search) { + clearStack() openApplicationList({ category: cat, search: search }) } function openApplication(app) { window.stack.push(applicationComp, { application: app }) } function openReviews(model) { window.stack.push(reviewsComp, { model: model }) } function openExtends(ext) { window.stack.push(applicationListComp, { extend: ext, title: i18n("Extensions...") }) } function openHome() { + window.globalDrawer.resetMenu(); clearStack() window.stack.push(topBrowsingComp) } diff --git a/libdiscover/CMakeLists.txt b/libdiscover/CMakeLists.txt index 8b9f5a22..c8be46f4 100644 --- a/libdiscover/CMakeLists.txt +++ b/libdiscover/CMakeLists.txt @@ -1,61 +1,61 @@ add_definitions(-DTRANSLATION_DOMAIN=\"libdiscover\") add_subdirectory(backends) add_subdirectory(declarative) add_subdirectory(notifiers) add_subdirectory(tests) set(discovercommon_SRCS Category/Category.cpp Category/CategoryModel.cpp Category/CategoriesReader.cpp ReviewsBackend/AbstractReviewsBackend.cpp ReviewsBackend/Rating.cpp ReviewsBackend/Review.cpp ReviewsBackend/AbstractLoginBackend.cpp ReviewsBackend/ReviewsModel.cpp ReviewsBackend/PopConParser.cpp Transaction/AddonList.cpp Transaction/Transaction.cpp Transaction/TransactionListener.cpp Transaction/TransactionModel.cpp UpdateModel/UpdateItem.cpp UpdateModel/UpdateModel.cpp resources/ResourcesModel.cpp resources/ResourcesProxyModel.cpp resources/PackageState.cpp resources/ResourcesUpdatesModel.cpp resources/StandardBackendUpdater.cpp resources/SourcesModel.cpp resources/AbstractResourcesBackend.cpp resources/AbstractResource.cpp resources/AbstractBackendUpdater.cpp resources/AbstractSourcesBackend.cpp MessageActionsModel DiscoverBackendsFactory.cpp ScreenshotsModel.cpp ApplicationAddonsModel.cpp ) kconfig_add_kcfg_files(discovercommon_SRCS GENERATE_MOC MuonDataSources.kcfgc) add_library(DiscoverCommon ${discovercommon_SRCS}) target_link_libraries(DiscoverCommon LINK_PUBLIC Qt5::Core + Qt5::Qml Qt5::Widgets KF5::I18n LINK_PRIVATE - Qt5::Qml Qt5::Xml KF5::XmlGui KF5::CoreAddons ) add_library(Discover::Common ALIAS DiscoverCommon) generate_export_header(DiscoverCommon) target_include_directories(DiscoverCommon PRIVATE ${PHONON_INCLUDES} PUBLIC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) install(TARGETS DiscoverCommon DESTINATION ${CMAKE_INSTALL_LIBDIR}/plasma-discover) install(FILES resources/discoverabstractnotifier.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR}) diff --git a/libdiscover/Category/Category.h b/libdiscover/Category/Category.h index 5f2ccce3..403df06e 100644 --- a/libdiscover/Category/Category.h +++ b/libdiscover/Category/Category.h @@ -1,89 +1,89 @@ /*************************************************************************** * Copyright © 2010 Jonathan Thomas * * * * 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 CATEGORY_H #define CATEGORY_H #include #include #include #include #include #include "discovercommon_export.h" class QDomNode; enum FilterType { InvalidFilter, CategoryFilter, PkgSectionFilter, PkgWildcardFilter, PkgNameFilter }; class DISCOVERCOMMON_EXPORT Category : public QObject { Q_OBJECT public: Q_PROPERTY(QString name READ name CONSTANT) Q_PROPERTY(QString icon READ icon CONSTANT) Q_PROPERTY(bool shouldShowTechnical READ shouldShowTechnical CONSTANT) Q_PROPERTY(QObject* parent READ parent CONSTANT) Q_PROPERTY(QUrl decoration READ decoration CONSTANT) Q_PROPERTY(QVariantList subcategories READ subCategoriesVariant CONSTANT) explicit Category(QSet pluginNames, QObject* parent = nullptr); ~Category() override; QString name() const; QString icon() const; QVector > andFilters() const; QVector > orFilters() const; QVector > notFilters() const; bool shouldShowTechnical() const; QVector subCategories() const; QVariantList subCategoriesVariant() const; static void addSubcategory(QVector& list, Category* cat); void parseData(const QString& path, const QDomNode& data); bool blacklistPlugins(const QSet& pluginName); bool isAddons() const { return m_isAddons; } QUrl decoration() const; Q_SCRIPTABLE bool contains(Category* cat) const; Q_SCRIPTABLE bool contains(const QVariantList &cats) const; private: QString m_name; QString m_iconString; QUrl m_decoration; QVector > m_andFilters; QVector > m_orFilters; QVector > m_notFilters; bool m_showTechnical; QVector m_subCategories; QVector > parseIncludes(const QDomNode &data); QSet m_plugins; bool m_isAddons = false; }; -Q_DECLARE_METATYPE(QList); +Q_DECLARE_METATYPE(QList) #endif diff --git a/libdiscover/backends/ApplicationBackend/Application.cpp b/libdiscover/backends/ApplicationBackend/Application.cpp index 2df0bbb6..ae1d95ed 100644 --- a/libdiscover/backends/ApplicationBackend/Application.cpp +++ b/libdiscover/backends/ApplicationBackend/Application.cpp @@ -1,545 +1,546 @@ /*************************************************************************** * Copyright © 2010-2011 Jonathan Thomas * * * * 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 "Application.h" // Qt includes #include #include #include #include #include +#include #include // KDE includes #include #include #include #include #include #include #include #include // QApt includes #include #include #include #include #include #include "ApplicationBackend.h" #include "resources/PackageState.h" Application::Application(const Appstream::Component &component, QApt::Backend* backend) : AbstractResource(nullptr) , m_data(component) , m_package(nullptr) , m_isValid(true) , m_isTechnical(component.kind() != Appstream::Component::KindDesktop) , m_isExtrasApp(false) , m_sourceHasScreenshot(true) { static QByteArray currentDesktop = qgetenv("XDG_CURRENT_DESKTOP"); Q_ASSERT(component.packageNames().count() == 1); if (!component.packageNames().isEmpty()) m_packageName = component.packageNames().at(0); m_package = backend->package(packageName()); m_isValid = bool(m_package); } Application::Application(QApt::Package* package, QApt::Backend* backend) : AbstractResource(nullptr) , m_package(package) , m_packageName(m_package->name()) , m_isValid(true) , m_isTechnical(true) , m_isExtrasApp(false) { QString arch = m_package->architecture(); if (arch != backend->nativeArchitecture() && arch != QLatin1String("all")) { m_packageName.append(QLatin1Char(':')); m_packageName.append(arch); } if (m_package->origin() == QLatin1String("LP-PPA-app-review-board")) { if (!m_package->controlField(QLatin1String("Appname")).isEmpty()) { m_isExtrasApp = true; m_isTechnical = false; } } } QString Application::name() { QString name = m_data.isValid() ? m_data.name() : QString(); if (name.isEmpty() && package()) { // extras.ubuntu.com packages can have this if (m_isExtrasApp) name = m_package->controlField(QLatin1String("Appname")); else name = m_package->name(); } if (package() && m_package->isForeignArch()) name = i18n("%1 (%2)", name, m_package->architecture()); return name; } QString Application::comment() { QString comment = m_data.isValid() ? m_data.summary() : QString(); if (comment.isEmpty()) { return package()->shortDescription(); } return i18n(comment.toUtf8().constData()); } QString Application::packageName() const { return m_packageName; } QApt::Package *Application::package() { if (!m_package && parent()) { m_package = backend()->package(packageName()); Q_EMIT stateChanged(); } // Packages removed from archive will remain in app-install-data until the // next refresh, so we can have valid .desktops with no package if (!m_package) { m_isValid = false; } return m_package; } -QString Application::icon() const +QVariant Application::icon() const { QIcon ret; const auto icons = m_appdata.iconUrls(); if (icons.isEmpty()) return m_appdata.name(); else { for (auto it = icons.constBegin(), itEnd = icons.constEnd(); it!=itEnd; ++it) { if (it->isLocalFile()) ret.addFile(it->toLocalFile(), it.key()); } } return ret; } QStringList Application::findProvides(Appstream::Provides::Kind kind) const { QStringList ret; Q_FOREACH (Appstream::Provides p, m_data.provides()) if (p.kind() == kind) ret += p.value(); return ret; } QStringList Application::mimetypes() const { return findProvides(Appstream::Provides::KindMimetype); } QStringList Application::categories() { QStringList categories = m_data.isValid() ? m_data.categories() : QStringList(); if (categories.isEmpty()) { // extras.ubuntu.com packages can have this field if (m_isExtrasApp) categories = package()->controlField(QLatin1String("Category")).split(QLatin1Char(';')); } return categories; } QUrl Application::thumbnailUrl() { QUrl url(package()->controlField(QLatin1String("Thumbnail-Url"))); if(m_sourceHasScreenshot) { url = QUrl(MuonDataSources::screenshotsSource().toString() + QStringLiteral("/thumbnail/") + packageName()); } return url; } QUrl Application::screenshotUrl() { QUrl url(package()->controlField(QLatin1String("Screenshot-Url"))); if(m_sourceHasScreenshot) { url = QUrl(MuonDataSources::screenshotsSource().toString() + QStringLiteral("/screenshot/") + packageName()); } return url; } QString Application::license() { QString component = package()->component(); if (component == QLatin1String("main") || component == QLatin1String("universe")) { return i18nc("@info license", "Open Source"); } else if (component == QLatin1String("restricted")) { return i18nc("@info license", "Proprietary"); } else { return i18nc("@info license", "Unknown"); } } QApt::PackageList Application::addons() { QApt::PackageList addons; QApt::Package *pkg = package(); if (!pkg) { return addons; } QStringList tempList; // Only add recommends or suggests to the list if they aren't already going to be // installed if (!backend()->config()->readEntry(QStringLiteral("APT::Install-Recommends"), true)) { tempList << m_package->recommendsList(); } if (!backend()->config()->readEntry(QStringLiteral("APT::Install-Suggests"), false)) { tempList << m_package->suggestsList(); } tempList << m_package->enhancedByList(); QStringList languagePackages; QFile l10nFilterFile(QStringLiteral("/usr/share/language-selector/data/pkg_depends")); if (l10nFilterFile.open(QFile::ReadOnly)) { QString contents = QString::fromLatin1(l10nFilterFile.readAll()); foreach (const QString &line, contents.split(QLatin1Char('\n'))) { if (line.startsWith(QLatin1Char('#'))) { continue; } languagePackages << line.split(QLatin1Char(':')).last(); } languagePackages.removeAll(QString()); } foreach (const QString &addon, tempList) { bool shouldShow = true; QApt::Package *package = backend()->package(addon); if (!package || QString(package->section()).contains(QLatin1String("lib")) || addons.contains(package)) { continue; } foreach (const QString &langpack, languagePackages) { if (addon.contains(langpack)) { shouldShow = false; break; } } if (shouldShow) { addons << package; } } return addons; } QList Application::addonsInformation() { QList ret; QApt::PackageList pkgs = addons(); foreach(QApt::Package* p, pkgs) { ret += PackageState(p->name(), p->shortDescription(), p->isInstalled()); } return ret; } bool Application::isValid() const { return m_isValid; } bool Application::isTechnical() const { return m_isTechnical; } QUrl Application::homepage() { if(!m_package) return QUrl(); return QUrl(m_package->homepage()); } QString Application::origin() const { if(!m_package) return QString(); return m_package->origin(); } QString Application::longDescription() { const QString comment = m_data.isValid() ? m_data.description() : QString(); if(!comment.isEmpty()) return comment; if(m_package) return QString(); return m_package->longDescription(); } QString Application::availableVersion() const { if(!m_package) return QString(); return m_package->availableVersion(); } QString Application::installedVersion() const { if(!m_package) return QString(); return m_package->installedVersion(); } QString Application::sizeDescription() { KFormat f; if (!isInstalled()) { return i18nc("@info app size", "%1 to download, %2 on disk", f.formatByteSize(package()->downloadSize()), f.formatByteSize(package()->availableInstalledSize())); } else { return i18nc("@info app size", "%1 on disk", f.formatByteSize(package()->currentInstalledSize())); } } int Application::size() { return m_package->downloadSize(); } void Application::clearPackage() { m_package = nullptr; } QVector Application::findExecutables() const { QVector ret; if (!m_package) { qWarning() << "trying to find the executables for an uninitialized package!" << packageName(); return ret; } QRegExp rx(QStringLiteral(".+\\.desktop$"), Qt::CaseSensitive); foreach (const QString &desktop, m_package->installedFilesList().filter(rx)) { // Important to use serviceByStorageId to ensure we get a service even // if the KSycoca database doesn't have our .desktop file yet. KService::Ptr service = KService::serviceByStorageId(desktop); if (service && service->isApplication() && !service->noDisplay() && !service->exec().isEmpty()) { ret << service; } } return ret; } void Application::emitStateChanged() { emit stateChanged(); } void Application::invokeApplication() const { QVector< KService::Ptr > execs = findExecutables(); Q_ASSERT(!execs.isEmpty()); KToolInvocation::startServiceByDesktopName(execs.first()->desktopEntryName()); } bool Application::canExecute() const { return !findExecutables().isEmpty(); } QString Application::section() { return package()->section(); } AbstractResource::State Application::state() { if (!package()) return Broken; int s = package()->state(); if (s & QApt::Package::Upgradeable) { #if QAPT_VERSION >= QT_VERSION_CHECK(3, 1, 0) if (package()->isInUpdatePhase()) return Upgradeable; #else return Upgradeable; #endif } if (s & QApt::Package::Installed) { return Installed; } return None; // Actually: none of interest to us here in plasma-discover. } void Application::fetchScreenshots() { if(!m_sourceHasScreenshot) return; QString dest = QStandardPaths::locate(QStandardPaths::TempLocation, QStringLiteral("screenshots.")+m_packageName); const QUrl packageUrl(MuonDataSources::screenshotsSource().toString() + QStringLiteral("/json/package/")+m_packageName); KIO::StoredTransferJob* job = KIO::storedGet(packageUrl, KIO::NoReload, KIO::HideProgressInfo); connect(job, &KIO::StoredTransferJob::finished, this, &Application::downloadingScreenshotsFinished); } void Application::downloadingScreenshotsFinished(KJob* j) { KIO::StoredTransferJob* job = qobject_cast< KIO::StoredTransferJob* >(j); bool done = false; if(job) { QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(job->data(), &error); if(error.error != QJsonParseError::NoError) { QVariantMap values = doc.toVariant().toMap(); QVariantList screenshots = values[QStringLiteral("screenshots")].toList(); QList thumbnailUrls, screenshotUrls; foreach(const QVariant& screenshot, screenshots) { QVariantMap s = screenshot.toMap(); thumbnailUrls += s[QStringLiteral("small_image_url")].toUrl(); screenshotUrls += s[QStringLiteral("large_image_url")].toUrl(); } emit screenshotsFetched(thumbnailUrls, screenshotUrls); done = true; } } if(!done) { QList thumbnails, screenshots; if(!thumbnailUrl().isEmpty()) { thumbnails += thumbnailUrl(); screenshots += screenshotUrl(); } emit screenshotsFetched(thumbnails, screenshots); } } void Application::setHasScreenshot(bool has) { m_sourceHasScreenshot = has; } QStringList Application::executables() const { QStringList ret; const QVector exes = findExecutables(); for(KService::Ptr exe : exes) { ret += exe->exec(); } return ret; } bool Application::isFromSecureOrigin() const { Q_FOREACH (const QString &archive, m_package->archives()) { if (archive.contains(QLatin1String("security"))) { return true; } } return false; } void Application::fetchChangelog() { KIO::StoredTransferJob* getJob = KIO::storedGet(m_package->changelogUrl(), KIO::NoReload, KIO::HideProgressInfo); connect(getJob, &KIO::StoredTransferJob::result, this, &Application::processChangelog); } void Application::processChangelog(KJob* j) { KIO::StoredTransferJob* job = qobject_cast(j); if (!m_package || !job) { return; } QString changelog; if(j->error()==0) changelog = buildDescription(job->data(), m_package->sourcePackage()); if (changelog.isEmpty()) { if (m_package->origin() == QStringLiteral("Ubuntu")) { changelog = xi18nc("@info/rich", "The list of changes is not yet available. " "Please use Launchpad instead.", QStringLiteral("http://launchpad.net/ubuntu/+source/") + m_package->sourcePackage()); } else { changelog = xi18nc("@info", "The list of changes is not yet available."); } } emit changelogFetched(changelog); } QString Application::buildDescription(const QByteArray& data, const QString& source) { QApt::Changelog changelog(QString::fromLatin1(data), source); QString description; QApt::ChangelogEntryList entries = changelog.newEntriesSince(m_package->installedVersion()); if (entries.size() < 1) { return description; } foreach(const QApt::ChangelogEntry &entry, entries) { description += i18nc("@info:label Refers to a software version, Ex: Version 1.2.1:", "Version %1:", entry.version()); KFormat f; QString issueDate = entry.issueDateTime().toString(Qt::DefaultLocaleShortDate); description += QLatin1String("

") + i18nc("@info:label", "This update was issued on %1", issueDate) + QLatin1String("

"); QString updateText = entry.description(); updateText.replace(QLatin1Char('\n'), QLatin1String("
")); description += QLatin1String("

") + updateText + QLatin1String("

"); } return description; } QApt::Backend *Application::backend() const { return qobject_cast(parent())->backend(); } diff --git a/libdiscover/backends/ApplicationBackend/CMakeLists.txt b/libdiscover/backends/ApplicationBackend/CMakeLists.txt index 0c0eda63..cc7b35fd 100644 --- a/libdiscover/backends/ApplicationBackend/CMakeLists.txt +++ b/libdiscover/backends/ApplicationBackend/CMakeLists.txt @@ -1,51 +1,53 @@ # we will have our own fork of the library now, because they haven't still made their mind out of Qt5 # find_package(QtOAuth REQUIRED) add_subdirectory(qoauth) add_subdirectory(libmuonapt) include_directories(.) add_subdirectory(tests) set(appsbackend_SRCS ApplicationBackend.cpp Application.cpp ApplicationUpdates.cpp ReviewsBackend.cpp #TODO: rename to AptReviewsBackend UbuntuLoginBackend.cpp AptSourcesBackend.cpp ) qt5_add_dbus_interface(appsbackend_SRCS ubuntu_sso_dbus_interface.xml ubuntu_sso OPTIONS -i "LoginMetaTypes.h") add_library(qapt-backend MODULE ${appsbackend_SRCS}) target_link_libraries(qapt-backend Qt5::Widgets Qt5::DBus Qt5::Concurrent KF5::Archive KF5::KIOWidgets KF5::XmlGui DebconfKDE::Main KF5::IconThemes AppstreamQt Muon::QOAuth QApt::Main Discover::Common MuonApt ) target_include_directories(qapt-backend PRIVATE /usr/include/Qca-qt5/QtCrypto) install(TARGETS qapt-backend DESTINATION ${PLUGIN_INSTALL_DIR}/discover) install(FILES qapt-backend.desktop DESTINATION ${DATA_INSTALL_DIR}/libdiscover/backends) -install(FILES qapt-backend-categories.xml DESTINATION ${DATA_INSTALL_DIR}/libdiscover/categories) install(FILES distupgradeevent/releasechecker DESTINATION ${DATA_INSTALL_DIR}/libdiscover/applicationsbackend/ PERMISSIONS OWNER_EXECUTE OWNER_READ OWNER_WRITE GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ ) foreach(testing IN ITEMS ON OFF) set(name MuonApplicationNotifier) set(type MODULE) if(${testing}) set(name MuonApplicationNotifierTestLib) set(type STATIC) endif() add_library(${name} ${type} ApplicationNotifier.cpp) target_compile_definitions(${name} PRIVATE -DCMAKE_INSTALL_FULL_LIBEXECDIR_KF5=\"${CMAKE_INSTALL_FULL_LIBEXECDIR_KF5}\") target_link_libraries(${name} KF5::CoreAddons KF5::I18n KF5::Notifications KF5::IconThemes Discover::Notifiers) endforeach() install(TARGETS MuonApplicationNotifier DESTINATION ${PLUGIN_INSTALL_DIR}/discover-notifier) install(FILES muonapplicationnotifier.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR}) + +install(FILES ../PackageKitBackend/packagekit-backend-categories.xml DESTINATION ${DATA_INSTALL_DIR}/libdiscover/categories/ RENAME qapt-backend-categories.xml) +# add_subdirectory(${CMAKE_SOURCE_DIR}/libdiscover/backends/PackageKitBackend/categoryimages) diff --git a/libdiscover/backends/ApplicationBackend/tests/ApplicationBackendTest.cpp b/libdiscover/backends/ApplicationBackend/tests/ApplicationBackendTest.cpp index 68206c79..b17c15e8 100644 --- a/libdiscover/backends/ApplicationBackend/tests/ApplicationBackendTest.cpp +++ b/libdiscover/backends/ApplicationBackend/tests/ApplicationBackendTest.cpp @@ -1,156 +1,154 @@ /*************************************************************************** * 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 "ApplicationBackendTest.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QTEST_MAIN( ApplicationBackendTest ) QString getCodename(const QString& value) { QString ret; QFile f(QStringLiteral("/etc/os-release")); if(f.open(QIODevice::ReadOnly|QIODevice::Text)){ QRegExp rx(QStringLiteral("%1=(.+)\n").arg(value)); while(!f.atEnd()) { QString line = QString::fromUtf8(f.readLine()); if(rx.exactMatch(line)) { ret = rx.cap(1); break; } } } return ret; } AbstractResourcesBackend* backendByName(ResourcesModel* m, const QString& name) { QVector backends = m->backends(); foreach(AbstractResourcesBackend* backend, backends) { if(QString::fromLatin1(backend->metaObject()->className())==name) { return backend; } } return nullptr; } ApplicationBackendTest::ApplicationBackendTest() { QString ratingsDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)+QStringLiteral("/libdiscover/ratings.txt"); QFile testRatings(QStringLiteral("~/.kde-unit-test/share/apps/libdiscover/ratings.txt")); QFile ratings(ratingsDir); QString codeName = getCodename(QStringLiteral("ID")); if(!testRatings.exists()) { if(ratings.exists()) { ratings.copy(testRatings.fileName()); } else { ratings.close(); if(codeName.toLower() == QLatin1String("ubuntu")) { ratingsDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)+QStringLiteral("/libdiscover/rnrtestratings.txt"); } else { ratingsDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)+QStringLiteral("/libdiscover/popcontestratings.txt"); } ratings.setFileName(ratingsDir); if(ratings.exists()) { ratings.copy(testRatings.fileName()); } } testRatings.close(); ratings.close(); } ResourcesModel* m = new ResourcesModel(QStringLiteral("qapt-backend"), this); m_window = new KActionCollection(this, QStringLiteral("ApplicationBackendTest")); m->integrateActions(m_window); new ModelTest(m,m); m_appBackend = backendByName(m, QStringLiteral("ApplicationBackend")); QVERIFY(m_appBackend); //TODO: test all backends QSignalSpy s(m, SIGNAL(allInitialized())); QVERIFY(s.wait()); } ApplicationBackendTest::~ApplicationBackendTest() { delete m_window; } void ApplicationBackendTest::testReload() { ResourcesModel* model = ResourcesModel::global(); QVector apps = m_appBackend->allResources(); QCOMPARE(apps.count(), model->rowCount()); QVector appNames(apps.size()); for(int i=0; irowCount(); ++i) { AbstractResource* app = apps[i]; appNames[i]=app->property("packageName"); } bool b = QMetaObject::invokeMethod(m_appBackend, "reload"); QVERIFY(b); m_appBackend->updatesCount(); QCOMPARE(apps, m_appBackend->allResources() ); QVERIFY(!apps.isEmpty()); QCOMPARE(apps.count(), model->rowCount()); for(int i=0; irowCount(); ++i) { AbstractResource* app = apps[i]; QCOMPARE(appNames[i], app->property("packageName")); // QCOMPARE(m_model->data(m_model->index(i), ResourcesModel::NameRole).toString(), app->name()); } } void ApplicationBackendTest::testCategories() { ResourcesProxyModel* proxy = new ResourcesProxyModel(this); - CategoryModel* categoryModel = new CategoryModel(proxy); - categoryModel->setDisplayedCategory(nullptr); - for(int i=0; irowCount(); ++i) { - Category* cat = categoryModel->categoryForRow(i); + const auto cats = CategoryModel::rootCategories(); + for(auto cat: cats) { proxy->setFiltersFromCategory(cat); } } void ApplicationBackendTest::testRefreshUpdates() { ResourcesModel* m = ResourcesModel::global(); QSignalSpy spy(m, SIGNAL(fetchingChanged())); m_window->action(QStringLiteral("update"))->trigger(); // QTest::kWaitForSignal(m, SLOT(fetchingChanged())); QVERIFY(!m->isFetching()); qDebug() << spy.count(); } diff --git a/libdiscover/backends/PackageKitBackend/CMakeLists.txt b/libdiscover/backends/PackageKitBackend/CMakeLists.txt index 43c7541f..e31ea869 100644 --- a/libdiscover/backends/PackageKitBackend/CMakeLists.txt +++ b/libdiscover/backends/PackageKitBackend/CMakeLists.txt @@ -1,43 +1,26 @@ find_package(KF5KIO REQUIRED) find_package(KF5Archive REQUIRED) add_library(packagekit-backend MODULE PackageKitBackend.cpp PackageKitResource.cpp AppPackageKitResource.cpp PKTransaction.cpp PackageKitUpdater.cpp PackageKitMessages.cpp PackageKitSourcesBackend.cpp AppstreamReviews.cpp ) target_link_libraries(packagekit-backend PRIVATE Discover::Common Qt5::Core PK::packagekitqt5 KF5::ConfigGui KF5::Service KF5::KIOWidgets KF5::Archive AppstreamQt) install(TARGETS packagekit-backend DESTINATION ${PLUGIN_INSTALL_DIR}/discover) install(FILES packagekit-backend.desktop DESTINATION ${DATA_INSTALL_DIR}/libdiscover/backends) -install(FILES packagekit-backend-categories.xml DESTINATION ${DATA_INSTALL_DIR}/libdiscover/categories) #notifier add_library(DiscoverPackageKitNotifier MODULE PackageKitNotifier.cpp) target_link_libraries(DiscoverPackageKitNotifier PRIVATE PK::packagekitqt5 Discover::Notifiers) set_target_properties(DiscoverPackageKitNotifier PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/plasma-discover) install(TARGETS DiscoverPackageKitNotifier DESTINATION ${PLUGIN_INSTALL_DIR}/discover-notifier) -install( FILES - categoryimages/educationcrop.jpg - categoryimages/fontcrop.jpg - categoryimages/gamecrop.jpg - categoryimages/graphiccrop.jpg -# categoryimages/multimedia2.jpg - categoryimages/multimediacrop.jpg -# categoryimages/music.jpe - categoryimages/officecrop.jpg - categoryimages/sciencecrop.jpg - categoryimages/accesscropped.jpg - categoryimages/toolcrop.jpg - categoryimages/accesscropped.jpg - categoryimages/accessoriescrop.jpg - categoryimages/applicationcrop.jpg - categoryimages/internetcrop.jpg - categoryimages/settingscrop.jpg - DESTINATION ${KDE_INSTALL_FULL_DATAROOTDIR}/discover/pkcategories) +install(FILES packagekit-backend-categories.xml DESTINATION ${DATA_INSTALL_DIR}/libdiscover/categories) +add_subdirectory(categoryimages) diff --git a/libdiscover/backends/PackageKitBackend/categoryimages/CMakeLists.txt b/libdiscover/backends/PackageKitBackend/categoryimages/CMakeLists.txt new file mode 100644 index 00000000..aa72a703 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/categoryimages/CMakeLists.txt @@ -0,0 +1,18 @@ +install( FILES + educationcrop.jpg + fontcrop.jpg + gamecrop.jpg + graphiccrop.jpg +# multimedia2.jpg + multimediacrop.jpg +# music.jpe + officecrop.jpg + sciencecrop.jpg + accesscropped.jpg + toolcrop.jpg + accesscropped.jpg + accessoriescrop.jpg + applicationcrop.jpg + internetcrop.jpg + settingscrop.jpg + DESTINATION ${KDE_INSTALL_FULL_DATAROOTDIR}/discover/pkcategories)